mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-07-27 06:36:07 +02:00
Restructure frontend svelte file src folder
- Goal: Dependency structure mirrored in file structure
This commit is contained in:
235
web/frontend/src/generic/plots/Histogram.svelte
Normal file
235
web/frontend/src/generic/plots/Histogram.svelte
Normal file
@@ -0,0 +1,235 @@
|
||||
<!--
|
||||
@component Histogram Plot based on uPlot Bars
|
||||
|
||||
Properties:
|
||||
- `data [[],[]]`: uPlot data structure array ( [[],[]] == [X, Y] )
|
||||
- `usesBins Bool?`: If X-Axis labels are bins ("XX-YY") [Default: false]
|
||||
- `width Number?`: Plot width (reactively adaptive) [Default: 500]
|
||||
- `height Number?`: Plot height (reactively adaptive) [Default: 300]
|
||||
- `title String?`: Plot title [Default: ""]
|
||||
- `xlabel String?`: Plot X axis label [Default: ""]
|
||||
- `xunit String?`: Plot X axis unit [Default: ""]
|
||||
- `ylabel String?`: Plot Y axis label [Default: ""]
|
||||
- `yunit String?`: Plot Y axis unit [Default: ""]
|
||||
-->
|
||||
|
||||
<script>
|
||||
import uPlot from "uplot";
|
||||
import { formatNumber } from "../units.js";
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { Card } from "@sveltestrap/sveltestrap";
|
||||
|
||||
export let data;
|
||||
export let usesBins = false;
|
||||
export let width = 500;
|
||||
export let height = 300;
|
||||
export let title = "";
|
||||
export let xlabel = "";
|
||||
export let xunit = "";
|
||||
export let ylabel = "";
|
||||
export let yunit = "";
|
||||
|
||||
const { bars } = uPlot.paths;
|
||||
|
||||
const drawStyles = {
|
||||
bars: 1,
|
||||
points: 2,
|
||||
};
|
||||
|
||||
function paths(u, seriesIdx, idx0, idx1, extendGap, buildClip) {
|
||||
let s = u.series[seriesIdx];
|
||||
let style = s.drawStyle;
|
||||
|
||||
let renderer = // If bars to wide, change here
|
||||
style == drawStyles.bars ? bars({ size: [0.75, 100] }) : () => null;
|
||||
|
||||
return renderer(u, seriesIdx, idx0, idx1, extendGap, buildClip);
|
||||
}
|
||||
|
||||
// converts the legend into a simple tooltip
|
||||
function legendAsTooltipPlugin({
|
||||
className,
|
||||
style = { backgroundColor: "rgba(255, 249, 196, 0.92)", color: "black" },
|
||||
} = {}) {
|
||||
let legendEl;
|
||||
|
||||
function init(u, opts) {
|
||||
legendEl = u.root.querySelector(".u-legend");
|
||||
|
||||
legendEl.classList.remove("u-inline");
|
||||
className && legendEl.classList.add(className);
|
||||
|
||||
uPlot.assign(legendEl.style, {
|
||||
textAlign: "left",
|
||||
pointerEvents: "none",
|
||||
display: "none",
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
zIndex: 100,
|
||||
boxShadow: "2px 2px 10px rgba(0,0,0,0.5)",
|
||||
...style,
|
||||
});
|
||||
|
||||
// hide series color markers
|
||||
const idents = legendEl.querySelectorAll(".u-marker");
|
||||
|
||||
for (let i = 0; i < idents.length; i++) idents[i].style.display = "none";
|
||||
|
||||
const overEl = u.over;
|
||||
overEl.style.overflow = "visible";
|
||||
|
||||
// move legend into plot bounds
|
||||
overEl.appendChild(legendEl);
|
||||
|
||||
// show/hide tooltip on enter/exit
|
||||
overEl.addEventListener("mouseenter", () => {
|
||||
legendEl.style.display = null;
|
||||
});
|
||||
overEl.addEventListener("mouseleave", () => {
|
||||
legendEl.style.display = "none";
|
||||
});
|
||||
|
||||
// let tooltip exit plot
|
||||
// overEl.style.overflow = "visible";
|
||||
}
|
||||
|
||||
function update(u) {
|
||||
const { left, top } = u.cursor;
|
||||
legendEl.style.transform =
|
||||
"translate(" + (left + 15) + "px, " + (top + 15) + "px)";
|
||||
}
|
||||
|
||||
return {
|
||||
hooks: {
|
||||
init: init,
|
||||
setCursor: update,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
let plotWrapper = null;
|
||||
let uplot = null;
|
||||
let timeoutId = null;
|
||||
|
||||
function render() {
|
||||
let opts = {
|
||||
width: width,
|
||||
height: height,
|
||||
title: title,
|
||||
plugins: [legendAsTooltipPlugin()],
|
||||
cursor: {
|
||||
points: {
|
||||
size: (u, seriesIdx) => u.series[seriesIdx].points.size * 2.5,
|
||||
width: (u, seriesIdx, size) => size / 4,
|
||||
stroke: (u, seriesIdx) =>
|
||||
u.series[seriesIdx].points.stroke(u, seriesIdx) + "90",
|
||||
fill: (u, seriesIdx) => "#fff",
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
time: false,
|
||||
},
|
||||
},
|
||||
axes: [
|
||||
{
|
||||
stroke: "#000000",
|
||||
// scale: 'x',
|
||||
label: xlabel,
|
||||
labelGap: 10,
|
||||
size: 25,
|
||||
incrs: [1, 2, 5, 6, 10, 12, 50, 100, 500, 1000, 5000, 10000],
|
||||
border: {
|
||||
show: true,
|
||||
stroke: "#000000",
|
||||
},
|
||||
ticks: {
|
||||
width: 1 / devicePixelRatio,
|
||||
size: 5 / devicePixelRatio,
|
||||
stroke: "#000000",
|
||||
},
|
||||
values: (_, t) => t.map((v) => formatNumber(v)),
|
||||
},
|
||||
{
|
||||
stroke: "#000000",
|
||||
// scale: 'y',
|
||||
label: ylabel,
|
||||
labelGap: 10,
|
||||
size: 35,
|
||||
border: {
|
||||
show: true,
|
||||
stroke: "#000000",
|
||||
},
|
||||
ticks: {
|
||||
width: 1 / devicePixelRatio,
|
||||
size: 5 / devicePixelRatio,
|
||||
stroke: "#000000",
|
||||
},
|
||||
values: (_, t) => t.map((v) => formatNumber(v)),
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
label: xunit !== "" ? xunit : null,
|
||||
value: (u, ts, sidx, didx) => {
|
||||
if (usesBins) {
|
||||
const min = u.data[sidx][didx - 1] ? u.data[sidx][didx - 1] : 0;
|
||||
const max = u.data[sidx][didx];
|
||||
ts = min + " - " + max; // narrow spaces
|
||||
}
|
||||
return ts;
|
||||
},
|
||||
},
|
||||
Object.assign(
|
||||
{
|
||||
label: yunit !== "" ? yunit : null,
|
||||
width: 1 / devicePixelRatio,
|
||||
drawStyle: drawStyles.points,
|
||||
lineInterpolation: null,
|
||||
paths,
|
||||
},
|
||||
{
|
||||
drawStyle: drawStyles.bars,
|
||||
lineInterpolation: null,
|
||||
stroke: "#85abce",
|
||||
fill: "#85abce", // + "1A", // Transparent Fill
|
||||
},
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
uplot = new uPlot(opts, data, plotWrapper);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
render();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (uplot) uplot.destroy();
|
||||
|
||||
if (timeoutId != null) clearTimeout(timeoutId);
|
||||
});
|
||||
|
||||
function sizeChanged() {
|
||||
if (timeoutId != null) clearTimeout(timeoutId);
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
timeoutId = null;
|
||||
if (uplot) uplot.destroy();
|
||||
|
||||
render();
|
||||
}, 200);
|
||||
}
|
||||
|
||||
$: sizeChanged(width, height);
|
||||
</script>
|
||||
|
||||
{#if data.length > 0}
|
||||
<div bind:this={plotWrapper} />
|
||||
{:else}
|
||||
<Card class="mx-4" body color="warning"
|
||||
>Cannot render histogram: No data!</Card
|
||||
>
|
||||
{/if}
|
516
web/frontend/src/generic/plots/MetricPlot.svelte
Normal file
516
web/frontend/src/generic/plots/MetricPlot.svelte
Normal file
@@ -0,0 +1,516 @@
|
||||
<!--
|
||||
@component Main plot component, based on uPlot; metricdata values by time
|
||||
|
||||
Only width/height should change reactively.
|
||||
|
||||
Properties:
|
||||
- `metric String`: The metric name
|
||||
- `scope String?`: Scope of the displayed data [Default: node]
|
||||
- `resources [GraphQL.Resource]`: List of resources used for parent job
|
||||
- `width Number`: The plot width
|
||||
- `height Number`: The plot height
|
||||
- `timestep Number`: The timestep used for X-axis rendering
|
||||
- `series [GraphQL.Series]`: The metric data object
|
||||
- `useStatsSeries Bool?`: If this plot uses the statistics Min/Max/Median representation; automatically set to according bool [Default: null]
|
||||
- `statisticsSeries [GraphQL.StatisticsSeries]?`: Min/Max/Median representation of metric data [Default: null]
|
||||
- `cluster GraphQL.Cluster`: Cluster Object of the parent job
|
||||
- `subCluster String`: Name of the subCluster of the parent job
|
||||
- `isShared Bool?`: If this job used shared resources; will adapt threshold indicators accordingly [Default: false]
|
||||
- `forNode Bool?`: If this plot is used for node data display; will render x-axis as negative time with $now as maximum [Default: false]
|
||||
- `numhwthreads Number?`: Number of job HWThreads [Default: 0]
|
||||
- `numaccs Number?`: Number of job Accelerators [Default: 0]
|
||||
-->
|
||||
|
||||
<script context="module">
|
||||
function formatTime(t, forNode = false) {
|
||||
if (t !== null) {
|
||||
if (isNaN(t)) {
|
||||
return t;
|
||||
} else {
|
||||
const tAbs = Math.abs(t);
|
||||
const h = Math.floor(tAbs / 3600);
|
||||
const m = Math.floor((tAbs % 3600) / 60);
|
||||
// Re-Add "negativity" to time ticks only as string, so that if-cases work as intended
|
||||
if (h == 0) return `${forNode && m != 0 ? "-" : ""}${m}m`;
|
||||
else if (m == 0) return `${forNode ? "-" : ""}${h}h`;
|
||||
else return `${forNode ? "-" : ""}${h}:${m}h`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function timeIncrs(timestep, maxX, forNode) {
|
||||
if (forNode === true) {
|
||||
return [60, 300, 900, 1800, 3600, 7200, 14400, 21600]; // forNode fixed increments
|
||||
} else {
|
||||
let incrs = [];
|
||||
for (let t = timestep; t < maxX; t *= 10)
|
||||
incrs.push(t, t * 2, t * 3, t * 5);
|
||||
|
||||
return incrs;
|
||||
}
|
||||
}
|
||||
|
||||
// removed arg "subcluster": input metricconfig and topology now directly derived from subcluster
|
||||
function findThresholds(
|
||||
subClusterTopology,
|
||||
metricConfig,
|
||||
scope,
|
||||
isShared,
|
||||
numhwthreads,
|
||||
numaccs
|
||||
) {
|
||||
|
||||
if (!subClusterTopology || !metricConfig || !scope) {
|
||||
console.warn("Argument missing for findThresholds!");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
(scope == "node" && isShared == false) ||
|
||||
metricConfig?.aggregation == "avg"
|
||||
) {
|
||||
return {
|
||||
normal: metricConfig.normal,
|
||||
caution: metricConfig.caution,
|
||||
alert: metricConfig.alert,
|
||||
peak: metricConfig.peak,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
if (metricConfig?.aggregation == "sum") {
|
||||
let divisor = 1
|
||||
if (isShared == true) { // Shared
|
||||
if (numaccs > 0) divisor = subClusterTopology.accelerators.length / numaccs;
|
||||
else if (numhwthreads > 0) divisor = subClusterTopology.node.length / numhwthreads;
|
||||
}
|
||||
else if (scope == 'socket') divisor = subClusterTopology.socket.length;
|
||||
else if (scope == "core") divisor = subClusterTopology.core.length;
|
||||
else if (scope == "accelerator")
|
||||
divisor = subClusterTopology.accelerators.length;
|
||||
else if (scope == "hwthread") divisor = subClusterTopology.node.length;
|
||||
else {
|
||||
// console.log('TODO: how to calc thresholds for ', scope)
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
peak: metricConfig.peak / divisor,
|
||||
normal: metricConfig.normal / divisor,
|
||||
caution: metricConfig.caution / divisor,
|
||||
alert: metricConfig.alert / divisor,
|
||||
};
|
||||
}
|
||||
|
||||
console.warn(
|
||||
"Missing or unkown aggregation mode (sum/avg) for metric:",
|
||||
metricConfig,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import uPlot from "uplot";
|
||||
import { formatNumber } from "../units.js";
|
||||
import { getContext, onMount, onDestroy } from "svelte";
|
||||
import { Card } from "@sveltestrap/sveltestrap";
|
||||
|
||||
export let metric;
|
||||
export let scope = "node";
|
||||
export let resources = [];
|
||||
export let width;
|
||||
export let height;
|
||||
export let timestep;
|
||||
export let series;
|
||||
export let useStatsSeries = null;
|
||||
export let statisticsSeries = null;
|
||||
export let cluster;
|
||||
export let subCluster;
|
||||
export let isShared = false;
|
||||
export let forNode = false;
|
||||
export let numhwthreads = 0;
|
||||
export let numaccs = 0;
|
||||
|
||||
if (useStatsSeries == null) useStatsSeries = statisticsSeries != null;
|
||||
|
||||
if (useStatsSeries == false && series == null) useStatsSeries = true;
|
||||
|
||||
const subClusterTopology = getContext("getHardwareTopology")(cluster, subCluster);
|
||||
const metricConfig = getContext("getMetricConfig")(cluster, subCluster, 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(
|
||||
subClusterTopology,
|
||||
metricConfig,
|
||||
scope,
|
||||
isShared,
|
||||
numhwthreads,
|
||||
numaccs
|
||||
);
|
||||
|
||||
// converts the legend into a simple tooltip
|
||||
function legendAsTooltipPlugin({
|
||||
className,
|
||||
style = { backgroundColor: "rgba(255, 249, 196, 0.92)", color: "black" },
|
||||
} = {}) {
|
||||
let legendEl;
|
||||
const dataSize = series.length;
|
||||
|
||||
function init(u, opts) {
|
||||
legendEl = u.root.querySelector(".u-legend");
|
||||
|
||||
legendEl.classList.remove("u-inline");
|
||||
className && legendEl.classList.add(className);
|
||||
|
||||
uPlot.assign(legendEl.style, {
|
||||
textAlign: "left",
|
||||
pointerEvents: "none",
|
||||
display: "none",
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
zIndex: 100,
|
||||
boxShadow: "2px 2px 10px rgba(0,0,0,0.5)",
|
||||
...style,
|
||||
});
|
||||
|
||||
// conditional hide series color markers:
|
||||
if (
|
||||
useStatsSeries === true || // Min/Max/Median Self-Explanatory
|
||||
dataSize === 1 || // Only one Y-Dataseries
|
||||
dataSize > 6
|
||||
) {
|
||||
// More than 6 Y-Dataseries
|
||||
const idents = legendEl.querySelectorAll(".u-marker");
|
||||
for (let i = 0; i < idents.length; i++)
|
||||
idents[i].style.display = "none";
|
||||
}
|
||||
|
||||
const overEl = u.over;
|
||||
overEl.style.overflow = "visible";
|
||||
|
||||
// move legend into plot bounds
|
||||
overEl.appendChild(legendEl);
|
||||
|
||||
// show/hide tooltip on enter/exit
|
||||
overEl.addEventListener("mouseenter", () => {
|
||||
legendEl.style.display = null;
|
||||
});
|
||||
overEl.addEventListener("mouseleave", () => {
|
||||
legendEl.style.display = "none";
|
||||
});
|
||||
|
||||
// let tooltip exit plot
|
||||
// overEl.style.overflow = "visible";
|
||||
}
|
||||
|
||||
function update(u) {
|
||||
const { left, top } = u.cursor;
|
||||
const width = u.over.querySelector(".u-legend").offsetWidth;
|
||||
legendEl.style.transform =
|
||||
"translate(" + (left - width - 15) + "px, " + (top + 15) + "px)";
|
||||
}
|
||||
|
||||
if (dataSize <= 12 || useStatsSeries === true) {
|
||||
return {
|
||||
hooks: {
|
||||
init: init,
|
||||
setCursor: update,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// Setting legend-opts show/live as object with false here will not work ...
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
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.median.length
|
||||
: series.reduce((n, series) => Math.max(n, series.data.length), 0);
|
||||
const maxX = longestSeries * timestep;
|
||||
let maxY = null;
|
||||
|
||||
if (thresholds !== null) {
|
||||
maxY = 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;
|
||||
|
||||
if (maxY >= 10 * thresholds.peak) {
|
||||
// Hard y-range render limit if outliers in series data
|
||||
maxY = 10 * thresholds.peak;
|
||||
}
|
||||
}
|
||||
|
||||
const plotSeries = [
|
||||
{
|
||||
label: "Runtime",
|
||||
value: (u, ts, sidx, didx) =>
|
||||
didx == null ? null : formatTime(ts, forNode),
|
||||
},
|
||||
];
|
||||
const plotData = [new Array(longestSeries)];
|
||||
|
||||
if (forNode === true) {
|
||||
// Negative Timestamp Buildup
|
||||
for (let i = 0; i <= longestSeries; i++) {
|
||||
plotData[0][i] = (longestSeries - i) * timestep * -1;
|
||||
}
|
||||
} else {
|
||||
// Positive Timestamp Buildup
|
||||
for (
|
||||
let j = 0;
|
||||
j < longestSeries;
|
||||
j++ // TODO: Cache/Reuse this array?
|
||||
)
|
||||
plotData[0][j] = j * timestep;
|
||||
}
|
||||
|
||||
let plotBands = undefined;
|
||||
if (useStatsSeries) {
|
||||
plotData.push(statisticsSeries.min);
|
||||
plotData.push(statisticsSeries.max);
|
||||
plotData.push(statisticsSeries.median);
|
||||
// plotData.push(statisticsSeries.mean);
|
||||
|
||||
if (forNode === true) {
|
||||
// timestamp 0 with null value for reversed time axis
|
||||
if (plotData[1].length != 0) plotData[1].push(null);
|
||||
if (plotData[2].length != 0) plotData[2].push(null);
|
||||
if (plotData[3].length != 0) plotData[3].push(null);
|
||||
// if (plotData[4].length != 0) plotData[4].push(null);
|
||||
}
|
||||
|
||||
plotSeries.push({
|
||||
label: "min",
|
||||
scale: "y",
|
||||
width: lineWidth,
|
||||
stroke: "red",
|
||||
});
|
||||
plotSeries.push({
|
||||
label: "max",
|
||||
scale: "y",
|
||||
width: lineWidth,
|
||||
stroke: "green",
|
||||
});
|
||||
plotSeries.push({
|
||||
label: "median",
|
||||
scale: "y",
|
||||
width: lineWidth,
|
||||
stroke: "black",
|
||||
});
|
||||
// plotSeries.push({
|
||||
// label: "mean",
|
||||
// scale: "y",
|
||||
// width: lineWidth,
|
||||
// stroke: "blue",
|
||||
// });
|
||||
|
||||
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);
|
||||
if (forNode === true && plotData[1].length != 0) plotData[1].push(null); // timestamp 0 with null value for reversed time axis
|
||||
plotSeries.push({
|
||||
label:
|
||||
scope === "node"
|
||||
? resources[i].hostname
|
||||
: // scope === 'accelerator' ? resources[0].accelerators[i] :
|
||||
scope + " #" + (i + 1),
|
||||
scale: "y",
|
||||
width: lineWidth,
|
||||
stroke: lineColor(i, series.length),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const opts = {
|
||||
width,
|
||||
height,
|
||||
plugins: [legendAsTooltipPlugin()],
|
||||
series: plotSeries,
|
||||
axes: [
|
||||
{
|
||||
scale: "x",
|
||||
space: 35,
|
||||
incrs: timeIncrs(timestep, maxX, forNode),
|
||||
values: (_, vals) => vals.map((v) => formatTime(v, forNode)),
|
||||
},
|
||||
{
|
||||
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 textl = `${scope}${plotSeries.length > 2 ? "s" : ""}${
|
||||
useStatsSeries
|
||||
? ": min/median/max"
|
||||
: metricConfig != null && scope != metricConfig.scope
|
||||
? ` (${metricConfig.aggregation})`
|
||||
: ""
|
||||
}`;
|
||||
let textr = `${isShared && scope != "core" && scope != "accelerator" ? "[Shared]" : ""}`;
|
||||
u.ctx.save();
|
||||
u.ctx.textAlign = "start"; // 'end'
|
||||
u.ctx.fillStyle = "black";
|
||||
u.ctx.fillText(textl, u.bbox.left + 10, u.bbox.top + 10);
|
||||
u.ctx.textAlign = "end";
|
||||
u.ctx.fillStyle = "black";
|
||||
u.ctx.fillText(
|
||||
textr,
|
||||
u.bbox.left + u.bbox.width - 10,
|
||||
u.bbox.top + 10,
|
||||
);
|
||||
// u.ctx.fillText(text, u.bbox.left + u.bbox.width - 10, u.bbox.top + u.bbox.height - 10) // Recipe for bottom right
|
||||
|
||||
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 ? { min: 0, max: (maxY * 1.1) } : {auto: true}, // Add some space to upper render limit
|
||||
},
|
||||
legend: {
|
||||
// Display legend until max 12 Y-dataseries
|
||||
show: series.length <= 12 || useStatsSeries === true ? true : false,
|
||||
live: series.length <= 12 || useStatsSeries === true ? true : false,
|
||||
},
|
||||
cursor: { drag: { x: true, y: true } },
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
$: if (series[0].data.length > 0) {
|
||||
onSizeChange(width, height);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (series[0].data.length > 0) {
|
||||
plotWrapper.style.backgroundColor = backgroundColor();
|
||||
render();
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (uplot) uplot.destroy();
|
||||
|
||||
if (timeoutId != null) clearTimeout(timeoutId);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if series[0].data.length > 0}
|
||||
<div bind:this={plotWrapper} class="cc-plot"></div>
|
||||
{:else}
|
||||
<Card class="mx-4" body color="warning"
|
||||
>Cannot render plot: No series data returned for <code>{metric}</code></Card
|
||||
>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.cc-plot {
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
95
web/frontend/src/generic/plots/Pie.svelte
Normal file
95
web/frontend/src/generic/plots/Pie.svelte
Normal file
@@ -0,0 +1,95 @@
|
||||
<!--
|
||||
@component Pie Plot based on uPlot Pie
|
||||
|
||||
Properties:
|
||||
- `size Number`: X and Y size of the plot, for square shape
|
||||
- `sliceLabel String`: Label used in segment legends
|
||||
- `quantities [Number]`: Data values
|
||||
- `entities [String]`: Data identifiers
|
||||
- `displayLegend?`: Display uPlot legend [Default: false]
|
||||
|
||||
Exported:
|
||||
- `colors ['rgb(x,y,z)', ...]`: Color range used for segments; upstream used for legend
|
||||
-->
|
||||
|
||||
<script context="module">
|
||||
// http://tsitsul.in/blog/coloropt/ : 12 colors normal
|
||||
export const colors = [
|
||||
'rgb(235,172,35)',
|
||||
'rgb(184,0,88)',
|
||||
'rgb(0,140,249)',
|
||||
'rgb(0,110,0)',
|
||||
'rgb(0,187,173)',
|
||||
'rgb(209,99,230)',
|
||||
'rgb(178,69,2)',
|
||||
'rgb(255,146,135)',
|
||||
'rgb(89,84,214)',
|
||||
'rgb(0,198,248)',
|
||||
'rgb(135,133,0)',
|
||||
'rgb(0,167,108)',
|
||||
'rgb(189,189,189)'
|
||||
]
|
||||
</script>
|
||||
<script>
|
||||
import { Pie } from 'svelte-chartjs';
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler,
|
||||
ArcElement,
|
||||
CategoryScale
|
||||
} from 'chart.js';
|
||||
|
||||
ChartJS.register(
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler,
|
||||
ArcElement,
|
||||
CategoryScale
|
||||
);
|
||||
|
||||
export let size
|
||||
export let sliceLabel
|
||||
export let quantities
|
||||
export let entities
|
||||
export let displayLegend = false
|
||||
|
||||
$: data = {
|
||||
labels: entities,
|
||||
datasets: [
|
||||
{
|
||||
label: sliceLabel,
|
||||
data: quantities,
|
||||
fill: 1,
|
||||
backgroundColor: colors.slice(0, quantities.length)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const options = {
|
||||
maintainAspectRatio: false,
|
||||
animation: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: displayLegend
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="chart-container" style="--container-width: {size}; --container-height: {size}">
|
||||
<Pie {data} {options}/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.chart-container {
|
||||
position: relative;
|
||||
margin: auto;
|
||||
height: var(--container-height);
|
||||
width: var(--container-width);
|
||||
}
|
||||
</style>
|
124
web/frontend/src/generic/plots/Polar.svelte
Normal file
124
web/frontend/src/generic/plots/Polar.svelte
Normal file
@@ -0,0 +1,124 @@
|
||||
<!--
|
||||
@component Polar Plot based on chartJS Radar
|
||||
|
||||
Properties:
|
||||
- `metrics [String]`: Metric names to display as polar plot
|
||||
- `cluster GraphQL.Cluster`: Cluster Object of the parent job
|
||||
- `subCluster GraphQL.SubCluster`: SubCluster Object of the parent job
|
||||
- `jobMetrics [GraphQL.JobMetricWithName]`: Metric data
|
||||
- `height Number?`: Plot height [Default: 365]
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { getContext } from 'svelte'
|
||||
import { Radar } from 'svelte-chartjs';
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler,
|
||||
PointElement,
|
||||
RadialLinearScale,
|
||||
LineElement
|
||||
} from 'chart.js';
|
||||
|
||||
ChartJS.register(
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler,
|
||||
PointElement,
|
||||
RadialLinearScale,
|
||||
LineElement
|
||||
);
|
||||
|
||||
export let metrics
|
||||
export let cluster
|
||||
export let subCluster
|
||||
export let jobMetrics
|
||||
export let height = 365
|
||||
|
||||
const getMetricConfig = getContext("getMetricConfig")
|
||||
|
||||
const labels = metrics.filter(name => {
|
||||
if (!jobMetrics.find(m => m.name == name && m.scope == "node")) {
|
||||
console.warn(`PolarPlot: No metric data for '${name}'`)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
const getValuesForStat = (getStat) => labels.map(name => {
|
||||
const peak = getMetricConfig(cluster, subCluster, name).peak
|
||||
const metric = jobMetrics.find(m => m.name == name && m.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 = {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Max',
|
||||
data: getValuesForStat(getMax),
|
||||
fill: 1,
|
||||
backgroundColor: 'rgba(0, 102, 255, 0.25)',
|
||||
borderColor: 'rgb(0, 102, 255)',
|
||||
pointBackgroundColor: 'rgb(0, 102, 255)',
|
||||
pointBorderColor: '#fff',
|
||||
pointHoverBackgroundColor: '#fff',
|
||||
pointHoverBorderColor: 'rgb(0, 102, 255)'
|
||||
},
|
||||
{
|
||||
label: 'Avg',
|
||||
data: getValuesForStat(getAvg),
|
||||
fill: true,
|
||||
backgroundColor: 'rgba(255, 153, 0, 0.25)',
|
||||
borderColor: 'rgb(255, 153, 0)',
|
||||
pointBackgroundColor: 'rgb(255, 153, 0)',
|
||||
pointBorderColor: '#fff',
|
||||
pointHoverBackgroundColor: '#fff',
|
||||
pointHoverBorderColor: 'rgb(255, 153, 0)'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// No custom defined options but keep for clarity
|
||||
const options = {
|
||||
maintainAspectRatio: false,
|
||||
animation: false,
|
||||
scales: { // fix scale
|
||||
r: {
|
||||
suggestedMin: 0.0,
|
||||
suggestedMax: 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="chart-container">
|
||||
<Radar {data} {options} {height}/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.chart-container {
|
||||
margin: auto;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
372
web/frontend/src/generic/plots/Roofline.svelte
Normal file
372
web/frontend/src/generic/plots/Roofline.svelte
Normal file
@@ -0,0 +1,372 @@
|
||||
<!--
|
||||
@component Roofline Model Plot based on uPlot
|
||||
|
||||
Properties:
|
||||
- `data [null, [], []]`: Roofline Data Structure, see below for details [Default: null]
|
||||
- `renderTime Bool?`: If time information should be rendered as colored dots [Default: false]
|
||||
- `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: 350]
|
||||
|
||||
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";
|
||||
|
||||
export let data = null;
|
||||
export let renderTime = false;
|
||||
export let allowSizeChange = false;
|
||||
export let subCluster = null;
|
||||
export let width = 600;
|
||||
export let height = 350;
|
||||
|
||||
let plotWrapper = null;
|
||||
let uplot = null;
|
||||
let timeoutId = null;
|
||||
|
||||
const lineWidth = clusterCockpitConfig.plot_general_lineWidth;
|
||||
|
||||
|
||||
|
||||
// Helpers
|
||||
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 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),
|
||||
};
|
||||
}
|
||||
// End Helpers
|
||||
|
||||
// Dot Renderers
|
||||
const drawColorPoints = (u, seriesIdx, idx0, idx1) => {
|
||||
const size = 5 * devicePixelRatio;
|
||||
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 deg360 = 2 * Math.PI;
|
||||
for (let i = 0; i < d[0].length; i++) {
|
||||
let p = new Path2D();
|
||||
let xVal = d[0][i];
|
||||
let yVal = d[1][i];
|
||||
u.ctx.strokeStyle = getRGB(u.data[2][i]);
|
||||
u.ctx.fillStyle = getRGB(u.data[2][i]);
|
||||
if (
|
||||
xVal >= scaleX.min &&
|
||||
xVal <= scaleX.max &&
|
||||
yVal >= scaleY.min &&
|
||||
yVal <= scaleY.max
|
||||
) {
|
||||
let cx = valToPosX(xVal, scaleX, xDim, xOff);
|
||||
let cy = valToPosY(yVal, scaleY, yDim, yOff);
|
||||
|
||||
p.moveTo(cx + size / 2, cy);
|
||||
arc(p, cx, cy, size / 2, 0, deg360);
|
||||
}
|
||||
u.ctx.fill(p);
|
||||
}
|
||||
},
|
||||
);
|
||||
return null;
|
||||
};
|
||||
|
||||
const drawPoints = (u, seriesIdx, idx0, idx1) => {
|
||||
const size = 5 * devicePixelRatio;
|
||||
uPlot.orient(
|
||||
u,
|
||||
seriesIdx,
|
||||
(
|
||||
series,
|
||||
dataX,
|
||||
dataY,
|
||||
scaleX,
|
||||
scaleY,
|
||||
valToPosX,
|
||||
valToPosY,
|
||||
xOff,
|
||||
yOff,
|
||||
xDim,
|
||||
yDim,
|
||||
moveTo,
|
||||
lineTo,
|
||||
rect,
|
||||
arc,
|
||||
) => {
|
||||
let d = u.data[seriesIdx];
|
||||
u.ctx.strokeStyle = getRGB(0);
|
||||
u.ctx.fillStyle = getRGB(0);
|
||||
let deg360 = 2 * Math.PI;
|
||||
let p = new Path2D();
|
||||
for (let i = 0; i < d[0].length; i++) {
|
||||
let xVal = d[0][i];
|
||||
let yVal = d[1][i];
|
||||
if (
|
||||
xVal >= scaleX.min &&
|
||||
xVal <= scaleX.max &&
|
||||
yVal >= scaleY.min &&
|
||||
yVal <= scaleY.max
|
||||
) {
|
||||
let cx = valToPosX(xVal, scaleX, xDim, xOff);
|
||||
let cy = valToPosY(yVal, scaleY, yDim, yOff);
|
||||
p.moveTo(cx + size / 2, cy);
|
||||
arc(p, cx, cy, size / 2, 0, deg360);
|
||||
}
|
||||
}
|
||||
u.ctx.fill(p);
|
||||
},
|
||||
);
|
||||
return null;
|
||||
};
|
||||
|
||||
// Main Function
|
||||
function render(plotData) {
|
||||
if (plotData) {
|
||||
const opts = {
|
||||
title: "",
|
||||
mode: 2,
|
||||
width: width,
|
||||
height: height,
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
cursor: { drag: { x: false, y: false } },
|
||||
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: [
|
||||
1.0,
|
||||
subCluster?.flopRateSimd?.value
|
||||
? nearestThousand(subCluster.flopRateSimd.value)
|
||||
: 10000,
|
||||
],
|
||||
distr: 3, // Render as log
|
||||
log: 10, // log exp
|
||||
},
|
||||
},
|
||||
series: [{}, { paths: renderTime ? drawColorPoints : drawPoints }],
|
||||
hooks: {
|
||||
drawClear: [
|
||||
(u) => {
|
||||
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;
|
||||
}
|
||||
if (renderTime) {
|
||||
// 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(15000.0, "y", true)
|
||||
u.ctx.fillStyle = 'black'
|
||||
u.ctx.fillText('Start', 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('End', posXLimit + 23, posY)
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
// cursor: { drag: { x: true, y: true } } // Activate zoom
|
||||
};
|
||||
uplot = new uPlot(opts, plotData, plotWrapper);
|
||||
} else {
|
||||
// console.log("No data for roofline!");
|
||||
}
|
||||
}
|
||||
|
||||
// Svelte and Sizechange
|
||||
onMount(() => {
|
||||
render(data);
|
||||
});
|
||||
onDestroy(() => {
|
||||
if (uplot) uplot.destroy();
|
||||
|
||||
if (timeoutId != null) clearTimeout(timeoutId);
|
||||
});
|
||||
function sizeChanged() {
|
||||
if (timeoutId != null) clearTimeout(timeoutId);
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
timeoutId = null;
|
||||
if (uplot) uplot.destroy();
|
||||
render(data);
|
||||
}, 200);
|
||||
}
|
||||
$: if (allowSizeChange) sizeChanged(width, height);
|
||||
</script>
|
||||
|
||||
{#if data != null}
|
||||
<div bind:this={plotWrapper} />
|
||||
{:else}
|
||||
<Card class="mx-4" body color="warning">Cannot render roofline: No data!</Card
|
||||
>
|
||||
{/if}
|
||||
|
246
web/frontend/src/generic/plots/RooflineHeatmap.svelte
Normal file
246
web/frontend/src/generic/plots/RooflineHeatmap.svelte
Normal file
@@ -0,0 +1,246 @@
|
||||
<!--
|
||||
@component Roofline Model Plot as Heatmap of multiple Jobs based on Canvas
|
||||
|
||||
Properties:
|
||||
- `subCluster GraphQL.SubCluster?`: SubCluster Object; contains required topology information [Default: null]
|
||||
- **Note**: Object of first subCluster is used, how to handle multiple topologies within one cluster? [TODO]
|
||||
- `tiles [[Float!]!]?`: Data tiles to be rendered [Default: null]
|
||||
- `maxY Number?`: maximum flopRateSimd of all subClusters [Default: null]
|
||||
- `width Number?`: Plot width (reactively adaptive) [Default: 500]
|
||||
- `height Number?`: Plot height (reactively adaptive) [Default: 300]
|
||||
-->
|
||||
|
||||
<script context="module">
|
||||
const axesColor = '#aaaaaa'
|
||||
const tickFontSize = 10
|
||||
const labelFontSize = 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 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)
|
||||
}
|
||||
}
|
||||
|
||||
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, subCluster, width, height, defaultMaxY) {
|
||||
if (width <= 0)
|
||||
return
|
||||
|
||||
const [minX, maxX, minY, maxY] = [0.01, 1000, 1., subCluster?.flopRateSimd?.value || 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 = `${tickFontSize}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 + tickFontSize + 5)
|
||||
ctx.moveTo(px, paddingTop - 5)
|
||||
ctx.lineTo(px, height - paddingBottom + 5)
|
||||
|
||||
x *= axisStepFactor(i, w)
|
||||
}
|
||||
if (data.xLabel) {
|
||||
ctx.font = `${labelFontSize}px ${fontFamily}`
|
||||
let textWidth = ctx.measureText(data.xLabel).width
|
||||
ctx.fillText(data.xLabel, Math.floor((width / 2) - (textWidth / 2)), height - 20)
|
||||
}
|
||||
|
||||
ctx.textAlign = 'center'
|
||||
ctx.font = `${tickFontSize}px ${fontFamily}`
|
||||
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.font = `${labelFontSize}px ${fontFamily}`
|
||||
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.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 (subCluster != null) {
|
||||
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 = getCanvasX(scalarKnee),
|
||||
simdKneeX = getCanvasX(simdKnee),
|
||||
flopRateScalarY = getCanvasY(subCluster.flopRateScalar.value),
|
||||
flopRateSimdY = getCanvasY(subCluster.flopRateSimd.value)
|
||||
|
||||
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()
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { onMount } from 'svelte'
|
||||
import { formatNumber } from '../units.js'
|
||||
|
||||
export let subCluster = null
|
||||
export let tiles = null
|
||||
export let maxY = null
|
||||
export let width = 500
|
||||
export let height = 300
|
||||
|
||||
console.assert(tiles, "you must provide tiles!")
|
||||
|
||||
let ctx, canvasElement, prevWidth = width, prevHeight = height
|
||||
const data = {
|
||||
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, subCluster, width, height, 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, subCluster, width, height, maxY)
|
||||
}, 250)
|
||||
}
|
||||
|
||||
$: sizeChanged(width, height)
|
||||
</script>
|
||||
|
||||
<div class="cc-plot">
|
||||
<canvas bind:this={canvasElement} width="{prevWidth}" height="{prevHeight}"></canvas>
|
||||
</div>
|
185
web/frontend/src/generic/plots/Scatter.svelte
Normal file
185
web/frontend/src/generic/plots/Scatter.svelte
Normal file
@@ -0,0 +1,185 @@
|
||||
<!--
|
||||
@component Scatter plot of two metrics at identical timesteps, based on canvas
|
||||
|
||||
Properties:
|
||||
- `X [Number]`: Data from first selected metric as X-values
|
||||
- `Y [Number]`: Data from second selected metric as Y-values
|
||||
- `S GraphQl.TimeWeights.X?`: Float to scale the data with [Default: null]
|
||||
- `color String`: Color of the drawn scatter circles
|
||||
- `width Number`:
|
||||
- `height Number`:
|
||||
- `xLabel String`:
|
||||
- `yLabel String`:
|
||||
-->
|
||||
|
||||
<script context="module">
|
||||
import { formatNumber } from '../units.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>
|
||||
|
||||
<div class="cc-plot">
|
||||
<canvas bind:this={canvasElement} width="{width}" height="{height}"></canvas>
|
||||
</div>
|
Reference in New Issue
Block a user