mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-08-25 03:22:59 +02:00
Build new statusDash, refine newRoofline data render
This commit is contained in:
@@ -112,6 +112,7 @@ func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
for key, vals := range r.URL.Query() {
|
for key, vals := range r.URL.Query() {
|
||||||
switch key {
|
switch key {
|
||||||
|
// TODO: add project filter
|
||||||
case "state":
|
case "state":
|
||||||
for _, s := range vals {
|
for _, s := range vals {
|
||||||
state := schema.JobState(s)
|
state := schema.JobState(s)
|
||||||
@@ -124,7 +125,7 @@ func (api *RestApi) getJobs(rw http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
case "cluster":
|
case "cluster":
|
||||||
filter.Cluster = &model.StringInput{Eq: &vals[0]}
|
filter.Cluster = &model.StringInput{Eq: &vals[0]}
|
||||||
case "start-time":
|
case "start-time": // ?startTime=1753707480-1754053139
|
||||||
st := strings.Split(vals[0], "-")
|
st := strings.Split(vals[0], "-")
|
||||||
if len(st) != 2 {
|
if len(st) != 2 {
|
||||||
handleError(fmt.Errorf("invalid query parameter value: startTime"),
|
handleError(fmt.Errorf("invalid query parameter value: startTime"),
|
||||||
|
@@ -381,7 +381,7 @@ func (auth *Authentication) AuthUserApi(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
case len(user.Roles) >= 2:
|
case len(user.Roles) >= 2:
|
||||||
if user.HasRole(schema.RoleApi) && user.HasAnyRole([]schema.Role{schema.RoleUser, schema.RoleManager, schema.RoleAdmin}) {
|
if user.HasRole(schema.RoleApi) && user.HasAnyRole([]schema.Role{schema.RoleUser, schema.RoleManager, schema.RoleSupport, schema.RoleAdmin}) {
|
||||||
ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
|
ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
|
||||||
onsuccess.ServeHTTP(rw, r.WithContext(ctx))
|
onsuccess.ServeHTTP(rw, r.WithContext(ctx))
|
||||||
return
|
return
|
||||||
@@ -473,6 +473,7 @@ func securedCheck(user *schema.User, r *http.Request) error {
|
|||||||
IPAddress = r.RemoteAddr
|
IPAddress = r.RemoteAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: IPV6 not handled
|
||||||
if strings.Contains(IPAddress, ":") {
|
if strings.Contains(IPAddress, ":") {
|
||||||
IPAddress = strings.Split(IPAddress, ":")[0]
|
IPAddress = strings.Split(IPAddress, ":")[0]
|
||||||
}
|
}
|
||||||
|
@@ -35,6 +35,7 @@
|
|||||||
cluster = null,
|
cluster = null,
|
||||||
subCluster = null,
|
subCluster = null,
|
||||||
allowSizeChange = false,
|
allowSizeChange = false,
|
||||||
|
useColors = true,
|
||||||
width = 600,
|
width = 600,
|
||||||
height = 380,
|
height = 380,
|
||||||
} = $props();
|
} = $props();
|
||||||
@@ -243,7 +244,7 @@
|
|||||||
// Dot Renderer
|
// Dot Renderer
|
||||||
const makeDrawPoints = (opts) => {
|
const makeDrawPoints = (opts) => {
|
||||||
let {/*size, disp,*/ transparentFill, each = () => {}} = opts;
|
let {/*size, disp,*/ transparentFill, each = () => {}} = opts;
|
||||||
const sizeBase = 5 * pxRatio;
|
const sizeBase = 6 * pxRatio;
|
||||||
|
|
||||||
return (u, seriesIdx, idx0, idx1) => {
|
return (u, seriesIdx, idx0, idx1) => {
|
||||||
uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect, arc) => {
|
uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect, arc) => {
|
||||||
@@ -266,27 +267,34 @@
|
|||||||
let filtTop = u.posToVal(-maxSize / 2, scaleY.key);
|
let filtTop = u.posToVal(-maxSize / 2, scaleY.key);
|
||||||
|
|
||||||
for (let i = 0; i < d[0].length; i++) {
|
for (let i = 0; i < d[0].length; i++) {
|
||||||
|
if (useColors) {
|
||||||
|
u.ctx.strokeStyle = "rgb(0, 0, 0)";
|
||||||
// Jobs: Color based on Duration
|
// Jobs: Color based on Duration
|
||||||
if (jobsData) {
|
if (jobsData) {
|
||||||
u.ctx.strokeStyle = getRGB(u.data[2][i]);
|
//u.ctx.strokeStyle = getRGB(u.data[2][i]);
|
||||||
u.ctx.fillStyle = getRGB(u.data[2][i], transparentFill);
|
u.ctx.fillStyle = getRGB(u.data[2][i], transparentFill);
|
||||||
// Nodes: Color based on Idle vs. Allocated
|
// Nodes: Color based on Idle vs. Allocated
|
||||||
} else if (nodesData) {
|
} else if (nodesData) {
|
||||||
// console.log('In Plot Handler NodesData', nodesData)
|
// console.log('In Plot Handler NodesData', nodesData)
|
||||||
if (nodesData[i]?.nodeState == "idle") {
|
if (nodesData[i]?.nodeState == "idle") {
|
||||||
u.ctx.strokeStyle = "rgb(0, 0, 255)";
|
//u.ctx.strokeStyle = "rgb(0, 0, 255)";
|
||||||
u.ctx.fillStyle = "rgba(0, 0, 255, 0.5)";
|
u.ctx.fillStyle = "rgba(0, 0, 255, 0.5)";
|
||||||
} else if (nodesData[i]?.nodeState == "allocated") {
|
} else if (nodesData[i]?.nodeState == "allocated") {
|
||||||
u.ctx.strokeStyle = "rgb(0, 255, 0)";
|
//u.ctx.strokeStyle = "rgb(0, 255, 0)";
|
||||||
u.ctx.fillStyle = "rgba(0, 255, 0, 0.5)";
|
u.ctx.fillStyle = "rgba(0, 255, 0, 0.5)";
|
||||||
} else if (nodesData[i]?.nodeState == "notindb") {
|
} else if (nodesData[i]?.nodeState == "notindb") {
|
||||||
u.ctx.strokeStyle = "rgb(0, 0, 0)";
|
//u.ctx.strokeStyle = "rgb(0, 0, 0)";
|
||||||
u.ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
|
u.ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
|
||||||
} else { // Fallback: All other DEFINED states
|
} else { // Fallback: All other DEFINED states
|
||||||
u.ctx.strokeStyle = "rgb(255, 0, 0)";
|
//u.ctx.strokeStyle = "rgb(255, 0, 0)";
|
||||||
u.ctx.fillStyle = "rgba(255, 0, 0, 0.5)";
|
u.ctx.fillStyle = "rgba(255, 0, 0, 0.5)";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// No Colors: Use Black
|
||||||
|
u.ctx.strokeStyle = "rgb(0, 0, 0)";
|
||||||
|
u.ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
|
||||||
|
}
|
||||||
|
|
||||||
// Get Values
|
// Get Values
|
||||||
let xVal = d[0][i];
|
let xVal = d[0][i];
|
||||||
@@ -297,10 +305,15 @@
|
|||||||
|
|
||||||
// Jobs: Size based on Resourcecount
|
// Jobs: Size based on Resourcecount
|
||||||
if (jobsData) {
|
if (jobsData) {
|
||||||
size = sizeBase + (jobsData[i]?.numAcc ? jobsData[i].numAcc / 2 : jobsData[i].numNodes)
|
const scaling = jobsData[i].numNodes > 12
|
||||||
|
? 24 // Capped Dot Size
|
||||||
|
: jobsData[i].numNodes > 1
|
||||||
|
? jobsData[i].numNodes * 2 // MultiNode Scaling
|
||||||
|
: jobsData[i]?.numAcc ? jobsData[i].numAcc : jobsData[i].numNodes * 2 // Single Node or Scale by Accs
|
||||||
|
size = sizeBase + scaling
|
||||||
// Nodes: Size based on Jobcount
|
// Nodes: Size based on Jobcount
|
||||||
} else if (nodesData) {
|
} else if (nodesData) {
|
||||||
size = sizeBase + nodesData[i]?.numJobs
|
size = sizeBase + (nodesData[i]?.numJobs * 1.5) // Max Jobs Scale: 8 * 1.5 = 12
|
||||||
};
|
};
|
||||||
|
|
||||||
if (xVal >= filtLft && xVal <= filtRgt && yVal >= filtBtm && yVal <= filtTop) {
|
if (xVal >= filtLft && xVal <= filtRgt && yVal >= filtBtm && yVal <= filtTop) {
|
||||||
@@ -377,7 +390,7 @@
|
|||||||
tooltip.style.fontSize = "10pt";
|
tooltip.style.fontSize = "10pt";
|
||||||
tooltip.style.position = "absolute";
|
tooltip.style.position = "absolute";
|
||||||
tooltip.style.background = "#fcfcfc";
|
tooltip.style.background = "#fcfcfc";
|
||||||
tooltip.style.display = "nonde";
|
tooltip.style.display = "none";
|
||||||
tooltip.style.border = "2px solid black";
|
tooltip.style.border = "2px solid black";
|
||||||
tooltip.style.padding = "4px";
|
tooltip.style.padding = "4px";
|
||||||
tooltip.style.pointerEvents = "none";
|
tooltip.style.pointerEvents = "none";
|
||||||
@@ -417,7 +430,7 @@
|
|||||||
tooltip.style.top = (tooltipTopOffset + top + shiftX) + "px";
|
tooltip.style.top = (tooltipTopOffset + top + shiftX) + "px";
|
||||||
tooltip.style.left = (tooltipLeftOffset + lft + shiftY) + "px";
|
tooltip.style.left = (tooltipLeftOffset + lft + shiftY) + "px";
|
||||||
|
|
||||||
|
if (useColors) {
|
||||||
// Jobs: Color based on Duration
|
// Jobs: Color based on Duration
|
||||||
if (jobsData) {
|
if (jobsData) {
|
||||||
tooltip.style.borderColor = getRGB(u.data[2][i]);
|
tooltip.style.borderColor = getRGB(u.data[2][i]);
|
||||||
@@ -433,17 +446,26 @@
|
|||||||
tooltip.style.borderColor = "rgb(255, 0, 0)";
|
tooltip.style.borderColor = "rgb(255, 0, 0)";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// No Colors: Use Black
|
||||||
|
tooltip.style.borderColor = "rgb(0, 0, 0)";
|
||||||
|
}
|
||||||
|
|
||||||
if (jobsData) {
|
if (jobsData) {
|
||||||
tooltip.textContent = (
|
tooltip.textContent = (
|
||||||
// Tooltip Content as String for Job
|
// Tooltip Content as String for Job
|
||||||
`Job ID: ${getLegendData(u, i).jobId}\nNodes: ${getLegendData(u, i).numNodes}${getLegendData(u, i)?.numAcc?`\nAccelerators: ${getLegendData(u, i).numAcc}`:''}`
|
`Job ID: ${getLegendData(u, i).jobId}\nRuntime: ${getLegendData(u, i).duration}\nNodes: ${getLegendData(u, i).numNodes}${getLegendData(u, i)?.numAcc?`\nAccelerators: ${getLegendData(u, i).numAcc}`:''}`
|
||||||
);
|
);
|
||||||
} else if (nodesData) {
|
} else if (nodesData && useColors) {
|
||||||
tooltip.textContent = (
|
tooltip.textContent = (
|
||||||
// Tooltip Content as String for Node
|
// Tooltip Content as String for Node
|
||||||
`Host: ${getLegendData(u, i).nodeName}\nState: ${getLegendData(u, i).nodeState}\nJobs: ${getLegendData(u, i).numJobs}`
|
`Host: ${getLegendData(u, i).nodeName}\nState: ${getLegendData(u, i).nodeState}\nJobs: ${getLegendData(u, i).numJobs}`
|
||||||
);
|
);
|
||||||
|
} else if (nodesData && !useColors) {
|
||||||
|
tooltip.textContent = (
|
||||||
|
// Tooltip Content as String for Node
|
||||||
|
`Host: ${getLegendData(u, i).nodeName}\nJobs: ${getLegendData(u, i).numJobs}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -570,7 +592,7 @@
|
|||||||
// return prox;
|
// return prox;
|
||||||
// },
|
// },
|
||||||
// },
|
// },
|
||||||
drag: { // Activates Zoom
|
drag: { // Activates Zoom: Only one Dimension; YX Breaks Zoom Reset (Reason TBD)
|
||||||
x: true,
|
x: true,
|
||||||
y: false
|
y: false
|
||||||
},
|
},
|
||||||
@@ -725,13 +747,15 @@
|
|||||||
u.ctx.lineWidth = 0.15;
|
u.ctx.lineWidth = 0.15;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Render Scales */
|
||||||
|
if (useColors) {
|
||||||
// Jobs: The Color Scale For Time Information
|
// Jobs: The Color Scale For Time Information
|
||||||
if (jobsData) {
|
if (jobsData) {
|
||||||
const posX = u.valToPos(0.1, "x", true)
|
const posX = u.valToPos(0.1, "x", true)
|
||||||
const posXLimit = u.valToPos(100, "x", true)
|
const posXLimit = u.valToPos(100, "x", true)
|
||||||
const posY = u.valToPos(14000.0, "y", true)
|
const posY = u.valToPos(17500.0, "y", true)
|
||||||
u.ctx.fillStyle = 'black'
|
u.ctx.fillStyle = 'black'
|
||||||
u.ctx.fillText('Short', posX, posY)
|
u.ctx.fillText('0 Hours', posX, posY)
|
||||||
const start = posX + 10
|
const start = posX + 10
|
||||||
for (let x = start; x < posXLimit; x += 10) {
|
for (let x = start; x < posXLimit; x += 10) {
|
||||||
let c = (x - start) / (posXLimit - start)
|
let c = (x - start) / (posXLimit - start)
|
||||||
@@ -741,15 +765,22 @@
|
|||||||
u.ctx.fill()
|
u.ctx.fill()
|
||||||
}
|
}
|
||||||
u.ctx.fillStyle = 'black'
|
u.ctx.fillStyle = 'black'
|
||||||
u.ctx.fillText('Long', posXLimit + 23, posY)
|
u.ctx.fillText('24 Hours', posXLimit + 55, posY)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nodes: The Colors Of NodeStates (Just 3)
|
// Nodes: The Colors Of NodeStates
|
||||||
if (nodesData) {
|
if (nodesData) {
|
||||||
const posY = u.valToPos(14000.0, "y", true)
|
const posY = u.valToPos(17500.0, "y", true)
|
||||||
|
|
||||||
const posAllocDot = u.valToPos(0.1, "x", true)
|
const posAllocDot = u.valToPos(0.03, "x", true)
|
||||||
const posAllocText = posAllocDot + 60
|
const posAllocText = posAllocDot + 60
|
||||||
|
const posIdleDot = u.valToPos(0.3, "x", true)
|
||||||
|
const posIdleText = posIdleDot + 30
|
||||||
|
const posOtherDot = u.valToPos(3, "x", true)
|
||||||
|
const posOtherText = posOtherDot + 40
|
||||||
|
const posMissingDot = u.valToPos(30, "x", true)
|
||||||
|
const posMissingText = posMissingDot + 80
|
||||||
|
|
||||||
u.ctx.fillStyle = "rgb(0, 255, 0)"
|
u.ctx.fillStyle = "rgb(0, 255, 0)"
|
||||||
u.ctx.beginPath()
|
u.ctx.beginPath()
|
||||||
u.ctx.arc(posAllocDot, posY, 3, 0, Math.PI * 2, false)
|
u.ctx.arc(posAllocDot, posY, 3, 0, Math.PI * 2, false)
|
||||||
@@ -757,8 +788,6 @@
|
|||||||
u.ctx.fillStyle = 'black'
|
u.ctx.fillStyle = 'black'
|
||||||
u.ctx.fillText('Allocated', posAllocText, posY)
|
u.ctx.fillText('Allocated', posAllocText, posY)
|
||||||
|
|
||||||
const posIdleDot = posAllocDot + 150
|
|
||||||
const posIdleText = posAllocText + 120
|
|
||||||
u.ctx.fillStyle = "rgb(0, 0, 255)"
|
u.ctx.fillStyle = "rgb(0, 0, 255)"
|
||||||
u.ctx.beginPath()
|
u.ctx.beginPath()
|
||||||
u.ctx.arc(posIdleDot, posY, 3, 0, Math.PI * 2, false)
|
u.ctx.arc(posIdleDot, posY, 3, 0, Math.PI * 2, false)
|
||||||
@@ -766,8 +795,6 @@
|
|||||||
u.ctx.fillStyle = 'black'
|
u.ctx.fillStyle = 'black'
|
||||||
u.ctx.fillText('Idle', posIdleText, posY)
|
u.ctx.fillText('Idle', posIdleText, posY)
|
||||||
|
|
||||||
const posOtherDot = posIdleDot + 150
|
|
||||||
const posOtherText = posIdleText + 160
|
|
||||||
u.ctx.fillStyle = "rgb(255, 0, 0)"
|
u.ctx.fillStyle = "rgb(255, 0, 0)"
|
||||||
u.ctx.beginPath()
|
u.ctx.beginPath()
|
||||||
u.ctx.arc(posOtherDot, posY, 3, 0, Math.PI * 2, false)
|
u.ctx.arc(posOtherDot, posY, 3, 0, Math.PI * 2, false)
|
||||||
@@ -775,14 +802,13 @@
|
|||||||
u.ctx.fillStyle = 'black'
|
u.ctx.fillStyle = 'black'
|
||||||
u.ctx.fillText('Other', posOtherText, posY)
|
u.ctx.fillText('Other', posOtherText, posY)
|
||||||
|
|
||||||
const posMissingDot = posOtherDot + 150
|
|
||||||
const posMissingText = posOtherText + 190
|
|
||||||
u.ctx.fillStyle = 'black'
|
u.ctx.fillStyle = 'black'
|
||||||
u.ctx.beginPath()
|
u.ctx.beginPath()
|
||||||
u.ctx.arc(posMissingDot, posY, 3, 0, Math.PI * 2, false)
|
u.ctx.arc(posMissingDot, posY, 3, 0, Math.PI * 2, false)
|
||||||
u.ctx.fill()
|
u.ctx.fill()
|
||||||
u.ctx.fillText('Missing in DB', posMissingText, posY)
|
u.ctx.fillText('Missing in DB', posMissingText, posY)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@@ -23,6 +23,7 @@
|
|||||||
//import Roofline from "../generic/plots/Roofline.svelte";
|
//import Roofline from "../generic/plots/Roofline.svelte";
|
||||||
import NewBubbleRoofline from "../generic/plots/NewBubbleRoofline.svelte";
|
import NewBubbleRoofline from "../generic/plots/NewBubbleRoofline.svelte";
|
||||||
import Pie, { colors } from "../generic/plots/Pie.svelte";
|
import Pie, { colors } from "../generic/plots/Pie.svelte";
|
||||||
|
import { formatTime } from "../generic/units.js";
|
||||||
|
|
||||||
/* Svelte 5 Props */
|
/* Svelte 5 Props */
|
||||||
let {
|
let {
|
||||||
@@ -131,7 +132,7 @@
|
|||||||
|
|
||||||
// Load for jobcount per node only -- might me required for total running jobs anyways in parent component!
|
// Load for jobcount per node only -- might me required for total running jobs anyways in parent component!
|
||||||
// Also, think about extra query with only TotalJobCount and Items [Resources, ...some meta infos], not including metric data
|
// Also, think about extra query with only TotalJobCount and Items [Resources, ...some meta infos], not including metric data
|
||||||
const paging = { itemsPerPage: 1500, page: 1 };
|
const paging = { itemsPerPage: -1, page: 1 };
|
||||||
const sorting = { field: "startTime", type: "col", order: "DESC" };
|
const sorting = { field: "startTime", type: "col", order: "DESC" };
|
||||||
const filter = [
|
const filter = [
|
||||||
{ cluster: { eq: cluster } },
|
{ cluster: { eq: cluster } },
|
||||||
@@ -281,7 +282,7 @@
|
|||||||
|
|
||||||
function transformJobsStatsToInfo(subclusterData) {
|
function transformJobsStatsToInfo(subclusterData) {
|
||||||
if (subclusterData) {
|
if (subclusterData) {
|
||||||
return subclusterData.map((sc) => { return {id: sc.id, jobId: sc.jobId, numNodes: sc.numNodes, numAcc: sc?.numAccelerators? sc.numAccelerators : 0} })
|
return subclusterData.map((sc) => { return {id: sc.id, jobId: sc.jobId, numNodes: sc.numNodes, numAcc: sc?.numAccelerators? sc.numAccelerators : 0, duration: formatTime(sc.duration)} })
|
||||||
} else {
|
} else {
|
||||||
console.warn("transformJobsStatsToInfo: jobInfo missing!")
|
console.warn("transformJobsStatsToInfo: jobInfo missing!")
|
||||||
return []
|
return []
|
||||||
|
@@ -15,7 +15,7 @@
|
|||||||
CardBody,
|
CardBody,
|
||||||
Table,
|
Table,
|
||||||
Progress,
|
Progress,
|
||||||
Icon,
|
// Icon,
|
||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
import {
|
import {
|
||||||
queryStore,
|
queryStore,
|
||||||
@@ -24,11 +24,11 @@
|
|||||||
} from "@urql/svelte";
|
} from "@urql/svelte";
|
||||||
import {
|
import {
|
||||||
init,
|
init,
|
||||||
transformPerNodeDataForRoofline,
|
// transformPerNodeDataForRoofline,
|
||||||
|
|
||||||
} from "../generic/utils.js";
|
} from "../generic/utils.js";
|
||||||
import { scaleNumbers } from "../generic/units.js";
|
import { scaleNumbers, formatTime } from "../generic/units.js";
|
||||||
import Roofline from "../generic/plots/Roofline.svelte";
|
import NewBubbleRoofline from "../generic/plots/NewBubbleRoofline.svelte";
|
||||||
|
|
||||||
/* Svelte 5 Props */
|
/* Svelte 5 Props */
|
||||||
let {
|
let {
|
||||||
@@ -68,9 +68,12 @@
|
|||||||
$metrics: [String!]
|
$metrics: [String!]
|
||||||
$from: Time!
|
$from: Time!
|
||||||
$to: Time!
|
$to: Time!
|
||||||
$filter: [JobFilter!]!
|
$jobFilter: [JobFilter!]!
|
||||||
|
$nodeFilter: [NodeFilter!]!
|
||||||
$paging: PageRequest!
|
$paging: PageRequest!
|
||||||
|
$sorting: OrderByInput!
|
||||||
) {
|
) {
|
||||||
|
# Node 5 Minute Averages for Roofline
|
||||||
nodeMetrics(
|
nodeMetrics(
|
||||||
cluster: $cluster
|
cluster: $cluster
|
||||||
metrics: $metrics
|
metrics: $metrics
|
||||||
@@ -81,27 +84,58 @@
|
|||||||
subCluster
|
subCluster
|
||||||
metrics {
|
metrics {
|
||||||
name
|
name
|
||||||
scope
|
|
||||||
metric {
|
metric {
|
||||||
timestep
|
|
||||||
unit {
|
|
||||||
base
|
|
||||||
prefix
|
|
||||||
}
|
|
||||||
series {
|
series {
|
||||||
data
|
statistics {
|
||||||
|
avg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
# Running Job Metric Average for Rooflines
|
||||||
|
jobsMetricStats(filter: $jobFilter, metrics: $metrics) {
|
||||||
|
id
|
||||||
|
jobId
|
||||||
|
duration
|
||||||
|
numNodes
|
||||||
|
numAccelerators
|
||||||
|
subCluster
|
||||||
|
stats {
|
||||||
|
name
|
||||||
|
data {
|
||||||
|
avg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# Get Jobs for Per-Node Counts
|
||||||
|
jobs(filter: $jobFilter, order: $sorting, page: $paging) {
|
||||||
|
items {
|
||||||
|
jobId
|
||||||
|
resources {
|
||||||
|
hostname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
count
|
||||||
|
}
|
||||||
# Only counts shared nodes once
|
# Only counts shared nodes once
|
||||||
allocatedNodes(cluster: $cluster) {
|
allocatedNodes(cluster: $cluster) {
|
||||||
name
|
name
|
||||||
count
|
count
|
||||||
}
|
}
|
||||||
|
# Get States for Node Roofline; $sorting unused in backend: Use placeholder
|
||||||
|
nodes(filter: $nodeFilter, order: $sorting) {
|
||||||
|
count
|
||||||
|
items {
|
||||||
|
hostname
|
||||||
|
cluster
|
||||||
|
subCluster
|
||||||
|
nodeState
|
||||||
|
}
|
||||||
|
}
|
||||||
# totalNodes includes multiples if shared jobs
|
# totalNodes includes multiples if shared jobs
|
||||||
jobsStatistics(
|
jobsStatistics(
|
||||||
filter: $filter
|
filter: $jobFilter
|
||||||
page: $paging
|
page: $paging
|
||||||
sortBy: TOTALJOBS
|
sortBy: TOTALJOBS
|
||||||
groupBy: SUBCLUSTER
|
groupBy: SUBCLUSTER
|
||||||
@@ -118,8 +152,10 @@
|
|||||||
metrics: ["flops_any", "mem_bw"], // Fixed names for roofline and status bars
|
metrics: ["flops_any", "mem_bw"], // Fixed names for roofline and status bars
|
||||||
from: from.toISOString(),
|
from: from.toISOString(),
|
||||||
to: to.toISOString(),
|
to: to.toISOString(),
|
||||||
filter: [{ state: ["running"] }, { cluster: { eq: cluster } }],
|
jobFilter: [{ state: ["running"] }, { cluster: { eq: cluster } }],
|
||||||
|
nodeFilter: { cluster: { eq: cluster }},
|
||||||
paging: { itemsPerPage: -1, page: 1 }, // Get all: -1
|
paging: { itemsPerPage: -1, page: 1 }, // Get all: -1
|
||||||
|
sorting: { field: "startTime", type: "col", order: "DESC" }
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -170,6 +206,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
/* Const Functions */
|
/* Const Functions */
|
||||||
|
// New: Sum Up Node Averages
|
||||||
const sumUp = (data, subcluster, metric) =>
|
const sumUp = (data, subcluster, metric) =>
|
||||||
data.reduce(
|
data.reduce(
|
||||||
(sum, node) =>
|
(sum, node) =>
|
||||||
@@ -177,20 +214,132 @@
|
|||||||
? sum +
|
? sum +
|
||||||
(node.metrics
|
(node.metrics
|
||||||
.find((m) => m.name == metric)
|
.find((m) => m.name == metric)
|
||||||
?.metric.series.reduce(
|
?.metric?.series[0]?.statistics?.avg || 0
|
||||||
(sum, series) => sum + series.data[series.data.length - 1],
|
)
|
||||||
0,
|
|
||||||
) || 0)
|
|
||||||
: sum,
|
: sum,
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Old: SumUp Metric Time Data
|
||||||
|
// const sumUp = (data, subcluster, metric) =>
|
||||||
|
// data.reduce(
|
||||||
|
// (sum, node) =>
|
||||||
|
// node.subCluster == subcluster
|
||||||
|
// ? sum +
|
||||||
|
// (node.metrics
|
||||||
|
// .find((m) => m.name == metric)
|
||||||
|
// ?.metric.series.reduce(
|
||||||
|
// (sum, series) => sum + series.data[series.data.length - 1],
|
||||||
|
// 0,
|
||||||
|
// ) || 0)
|
||||||
|
// : sum,
|
||||||
|
// 0,
|
||||||
|
// );
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
|
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("transformJobsStatsToData: 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 transformNodesStatsToData(subclusterData) {
|
||||||
|
let data = null
|
||||||
|
const x = [], y = []
|
||||||
|
|
||||||
|
if (subclusterData) {
|
||||||
|
for (let i = 0; i < subclusterData.length; i++) {
|
||||||
|
const flopsData = subclusterData[i].metrics.find((s) => s.name == "flops_any")
|
||||||
|
const memBwData = subclusterData[i].metrics.find((s) => s.name == "mem_bw")
|
||||||
|
|
||||||
|
const f = flopsData.metric.series[0].statistics.avg
|
||||||
|
const m = memBwData.metric.series[0].statistics.avg
|
||||||
|
|
||||||
|
let intensity = f / m
|
||||||
|
if (Number.isNaN(intensity) || !Number.isFinite(intensity)) {
|
||||||
|
// continue // Old: Introduces mismatch between Data and Info Arrays
|
||||||
|
intensity = 0.0 // New: Set to Float Zero: Will not show in Log-Plot (Always below render limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
x.push(intensity)
|
||||||
|
y.push(f)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// console.warn("transformNodesStatsToData: metrics for 'mem_bw' and/or 'flops_any' missing!")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x.length > 0 && y.length > 0) {
|
||||||
|
data = [null, [x, y]] // 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, duration: formatTime(sc.duration)} })
|
||||||
|
} else {
|
||||||
|
console.warn("transformJobsStatsToInfo: jobInfo missing!")
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformNodesStatsToInfo(subClusterData) {
|
||||||
|
let result = [];
|
||||||
|
if (subClusterData) { // && $nodesState?.data) {
|
||||||
|
// Use Nodes as Returned from CCMS, *NOT* as saved in DB via SlurmState-API!
|
||||||
|
for (let j = 0; j < subClusterData.length; j++) {
|
||||||
|
// nodesCounts[subClusterData[i].subCluster] = $nodesState.data.nodes.count; // Probably better as own derived!
|
||||||
|
|
||||||
|
const nodeName = subClusterData[j]?.host ? subClusterData[j].host : "unknown"
|
||||||
|
const nodeMatch = $statusQuery?.data?.nodes?.items?.find((n) => n.hostname == nodeName && n.subCluster == subClusterData[j].subCluster);
|
||||||
|
const nodeState = nodeMatch?.nodeState ? nodeMatch.nodeState : "notindb"
|
||||||
|
let numJobs = 0
|
||||||
|
|
||||||
|
if ($statusQuery?.data) {
|
||||||
|
const nodeJobs = $statusQuery?.data?.jobs?.items?.filter((job) => job.resources.find((res) => res.hostname == nodeName))
|
||||||
|
numJobs = nodeJobs?.length ? nodeJobs.length : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push({nodeName: nodeName, nodeState: nodeState, numJobs: numJobs})
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Gauges & Roofline per Subcluster-->
|
<!-- Gauges & Roofline per Subcluster-->
|
||||||
{#if $initq.data && $statusQuery.data}
|
{#if $initq.data && $statusQuery.data}
|
||||||
{#each $initq.data.clusters.find((c) => c.name == cluster).subClusters as subCluster, i}
|
{#each $initq.data.clusters.find((c) => c.name == cluster).subClusters as subCluster, i}
|
||||||
<Row cols={{ lg: 2, md: 1 , sm: 1}} class="mb-3 justify-content-center">
|
<Row cols={{ lg: 3, md: 1 , sm: 1}} class="mb-3 justify-content-center">
|
||||||
<Col class="px-3">
|
<Col class="px-3">
|
||||||
<Card class="h-auto mt-1">
|
<Card class="h-auto mt-1">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -204,6 +353,25 @@
|
|||||||
<td colspan="2" style="font-size:x-large;">{activeUsers[subCluster.name]} Active Users</td>
|
<td colspan="2" style="font-size:x-large;">{activeUsers[subCluster.name]} Active Users</td>
|
||||||
</tr>
|
</tr>
|
||||||
<hr class="my-1"/>
|
<hr class="my-1"/>
|
||||||
|
<tr class="pt-2">
|
||||||
|
<td style="font-size: large;">
|
||||||
|
Flop Rate (<span style="cursor: help;" title="Flops[Any] = (Flops[Double] x 2) + Flops[Single]">Any</span>)
|
||||||
|
</td>
|
||||||
|
<td colspan="2" style="font-size: large;">
|
||||||
|
Memory BW Rate
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="pb-2">
|
||||||
|
<td style="font-size:x-large;">
|
||||||
|
{flopRate[subCluster.name]}
|
||||||
|
{flopRateUnitPrefix[subCluster.name]}{flopRateUnitBase[subCluster.name]}
|
||||||
|
</td>
|
||||||
|
<td colspan="2" style="font-size:x-large;">
|
||||||
|
{memBwRate[subCluster.name]}
|
||||||
|
{memBwRateUnitPrefix[subCluster.name]}{memBwRateUnitBase[subCluster.name]}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<hr class="my-1"/>
|
||||||
<tr class="py-2">
|
<tr class="py-2">
|
||||||
<th scope="col">Allocated Nodes</th>
|
<th scope="col">Allocated Nodes</th>
|
||||||
<td style="min-width: 100px;"
|
<td style="min-width: 100px;"
|
||||||
@@ -236,7 +404,8 @@
|
|||||||
>
|
>
|
||||||
</tr>
|
</tr>
|
||||||
{/if}
|
{/if}
|
||||||
<tr class="py-2">
|
<hr class="my-1"/>
|
||||||
|
<!-- <tr class="py-2">
|
||||||
<th scope="col"
|
<th scope="col"
|
||||||
>Flop Rate (Any) <Icon
|
>Flop Rate (Any) <Icon
|
||||||
name="info-circle"
|
name="info-circle"
|
||||||
@@ -280,23 +449,49 @@
|
|||||||
memBwRateUnitPrefix[subCluster.name],
|
memBwRateUnitPrefix[subCluster.name],
|
||||||
)}{memBwRateUnitBase[subCluster.name]} [Max]
|
)}{memBwRateUnitBase[subCluster.name]} [Max]
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr> -->
|
||||||
</Table>
|
</Table>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col class="px-3 mt-2 mt-lg-0">
|
<Col class="px-3 mt-2 mt-lg-0">
|
||||||
<div bind:clientWidth={plotWidths[i]}>
|
<div bind:clientWidth={plotWidths[i]}>
|
||||||
{#key $statusQuery.data.nodeMetrics}
|
{#key $statusQuery?.data?.nodeMetrics}
|
||||||
<Roofline
|
<NewBubbleRoofline
|
||||||
|
useColors={true}
|
||||||
|
allowSizeChange
|
||||||
|
width={plotWidths[i] - 10}
|
||||||
|
height={300}
|
||||||
|
cluster={cluster}
|
||||||
|
subCluster={subCluster}
|
||||||
|
roofData={transformNodesStatsToData($statusQuery?.data?.nodeMetrics.filter(
|
||||||
|
(data) => data.subCluster == subCluster.name,
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
nodesData={transformNodesStatsToInfo($statusQuery?.data?.nodeMetrics.filter(
|
||||||
|
(data) => data.subCluster == subCluster.name,
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col class="px-3 mt-2 mt-lg-0">
|
||||||
|
<div bind:clientWidth={plotWidths[i]}>
|
||||||
|
{#key $statusQuery?.data?.jobsMetricStats}
|
||||||
|
<NewBubbleRoofline
|
||||||
|
useColors={true}
|
||||||
allowSizeChange
|
allowSizeChange
|
||||||
width={plotWidths[i] - 10}
|
width={plotWidths[i] - 10}
|
||||||
height={300}
|
height={300}
|
||||||
subCluster={subCluster}
|
subCluster={subCluster}
|
||||||
data={transformPerNodeDataForRoofline(
|
roofData={transformJobsStatsToData($statusQuery?.data?.jobsMetricStats.filter(
|
||||||
$statusQuery.data.nodeMetrics.filter(
|
|
||||||
(data) => data.subCluster == subCluster.name,
|
(data) => data.subCluster == subCluster.name,
|
||||||
),
|
)
|
||||||
|
)}
|
||||||
|
jobsData={transformJobsStatsToInfo($statusQuery?.data?.jobsMetricStats.filter(
|
||||||
|
(data) => data.subCluster == subCluster.name,
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
|
Reference in New Issue
Block a user