Merge pull request #192 from ClusterCockpit/31_141_apply_uplot_update

31 141 apply uplot update
This commit is contained in:
Jan Eitzinger 2023-08-11 10:24:32 +02:00 committed by GitHub
commit 6834f07df3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 403 additions and 99 deletions

View File

@ -1,10 +1,11 @@
<script>
import { init } from './utils.js'
import { init, convert2uplot } from './utils.js'
import { getContext, onMount } from 'svelte'
import { queryStore, gql, getContextClient } from '@urql/svelte'
import { Row, Col, Spinner, Card, Table } from 'sveltestrap'
import Filters from './filters/Filters.svelte'
import PlotSelection from './PlotSelection.svelte'
import Histogramuplot from './plots/Histogramuplot.svelte'
import Histogram, { binsFromFootprint } from './plots/Histogram.svelte'
import ScatterPlot from './plots/Scatter.svelte'
import PlotTable from './PlotTable.svelte'
@ -169,22 +170,26 @@
</div>
<div class="col-3">
{#key $statsQuery.data.stats[0].histDuration}
<h4>Duration Distribution</h4>
<Histogram
<Histogramuplot
data={convert2uplot($statsQuery.data.stats[0].histDuration)}
width={colWidth - 25}
data={$statsQuery.data.stats[0].histDuration}
xlabel="Current Runtimes [h]"
ylabel="Number of Jobs"/>
title="Duration Distribution"
xlabel="Current Runtimes"
xunit="Hours"
ylabel="Number of Jobs"
yunit="Jobs"/>
{/key}
</div>
<div class="col-3">
{#key $statsQuery.data.stats[0].histNumNodes}
<h4>Number of Nodes Distribution</h4>
<Histogram
<Histogramuplot
data={convert2uplot($statsQuery.data.stats[0].histNumNodes)}
width={colWidth - 25}
data={$statsQuery.data.stats[0].histNumNodes}
xlabel="Allocated Nodes [#]"
ylabel="Number of Jobs" />
title="Number of Nodes Distribution"
xlabel="Allocated Nodes"
xunit="Nodes"
ylabel="Number of Jobs"
yunit="Jobs"/>
{/key}
</div>
<div class="col-3">
@ -233,15 +238,16 @@
$footprintsQuery.data.footprints.metrics.find(f => f.metric == metric).data, numBins) }))}
itemsPerRow={ccconfig.plot_view_plotsPerRow}>
<h4>Average Distribution of '{item.metric}'</h4>
<Histogram
<Histogramuplot
data={convert2uplot(item.bins)}
width={width} height={250}
min={item.min} max={item.max}
data={item.bins}
label={item.label}
xlabel={`${item.metric} Average [${(metricConfig(cluster.name, item.metric)?.unit?.prefix ? metricConfig(cluster.name, item.metric)?.unit?.prefix : '') +
title="Average Distribution of '{item.metric}'"
xlabel={`${item.metric} average [${(metricConfig(cluster.name, item.metric)?.unit?.prefix ? metricConfig(cluster.name, item.metric)?.unit?.prefix : '') +
(metricConfig(cluster.name, item.metric)?.unit?.base ? metricConfig(cluster.name, item.metric)?.unit?.base : '')}]`}
ylabel="Node Hours [h]" />
xunit={`${(metricConfig(cluster.name, item.metric)?.unit?.prefix ? metricConfig(cluster.name, item.metric)?.unit?.prefix : '') +
(metricConfig(cluster.name, item.metric)?.unit?.base ? metricConfig(cluster.name, item.metric)?.unit?.base : '')}`}
ylabel="Node Hours"
yunit="Hours"/>
</PlotTable>
</Col>
</Row>

View File

@ -290,9 +290,9 @@
</Button>
{/if}
</Col>
<Col xs="auto">
<!-- <Col xs="auto">
<Zoom timeseriesPlots={plots} />
</Col>
</Col> -->
</Row>
<br />
<Row>
@ -329,6 +329,7 @@
scopes={item.data.map((x) => x.scope)}
{width}
isShared={$initq.data.job.exclusive != 1}
resources={$initq.data.job.resources}
/>
{:else}
<Card body color="warning"

View File

@ -89,6 +89,7 @@
timestep={data.timestep}
scope={selectedScope} metric={metricName}
series={series}
isShared={isShared} />
isShared={isShared}
resources={job.resources} />
{/if}
{/key}

View File

@ -203,6 +203,7 @@
subCluster={$nodeMetricsData.data.nodeMetrics[0]
.subCluster}
series={item.metric.series}
resources={[{hostname: hostname}]}
/>
{:else if item.disabled === true && item.metric}
<Card

View File

@ -2,8 +2,9 @@
import Refresher from './joblist/Refresher.svelte'
import Roofline, { transformPerNodeData } from './plots/Roofline.svelte'
import Histogram from './plots/Histogram.svelte'
import Histogramuplot from './plots/Histogramuplot.svelte'
import { Row, Col, Spinner, Card, CardHeader, CardTitle, CardBody, Table, Progress, Icon } from 'sveltestrap'
import { init } from './utils.js'
import { init, convert2uplot } from './utils.js'
import { scaleNumbers } from './units.js'
import { queryStore, gql, getContextClient } from '@urql/svelte'
@ -202,27 +203,32 @@
</Table>
</Col>
</Row>
<Row cols={2} class="mt-3">
<hr class="my-2"/>
<Row cols={2}>
<Col class="p-2">
<div bind:clientWidth={colWidth2}>
<h4 class="mb-3 text-center">Duration Distribution</h4>
{#key $mainQuery.data.stats}
<Histogram
<Histogramuplot
data={convert2uplot($mainQuery.data.stats[0].histDuration)}
width={colWidth2 - 25}
data={$mainQuery.data.stats[0].histDuration}
xlabel="Current Runtimes [h]"
ylabel="Number of Jobs" />
title="Duration Distribution"
xlabel="Current Runtimes"
xunit="Hours"
ylabel="Number of Jobs"
yunit="Jobs"/>
{/key}
</div>
</Col>
<Col class="p-2">
<h4 class="mb-3 text-center">Number of Nodes Distribution</h4>
{#key $mainQuery.data.stats}
<Histogram
<Histogramuplot
data={convert2uplot($mainQuery.data.stats[0].histNumNodes)}
width={colWidth2 - 25}
data={$mainQuery.data.stats[0].histNumNodes}
xlabel="Allocated Nodes [#]"
ylabel="Number of Jobs" />
title="Number of Nodes Distribution"
xlabel="Allocated Nodes"
xunit="Nodes"
ylabel="Number of Jobs"
yunit="Jobs"/>
{/key}
</Col>
</Row>

View File

@ -136,7 +136,8 @@
series={item.data.metric.series}
metric={item.data.name}
cluster={clusters.find(c => c.name == cluster)}
subCluster={item.subCluster} />
subCluster={item.subCluster}
resources={[{hostname: item.host}]}/>
{:else if item.disabled === true && item.data}
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="info">Metric disabled for subcluster <code>{selectedMetric}:{item.subCluster}</code></Card>
{:else}

View File

@ -1,13 +1,13 @@
<script>
import { onMount, getContext } from 'svelte'
import { init } from './utils.js'
import { init, convert2uplot } from './utils.js'
import { Table, Row, Col, Button, Icon, Card, Spinner, Input } from 'sveltestrap'
import { queryStore, gql, getContextClient } from '@urql/svelte'
import Filters from './filters/Filters.svelte'
import JobList from './joblist/JobList.svelte'
import Sorting from './joblist/SortSelection.svelte'
import Refresher from './joblist/Refresher.svelte'
import Histogram from './plots/Histogram.svelte'
import Histogramuplot from './plots/Histogramuplot.svelte'
import MetricSelection from './MetricSelection.svelte'
import { scramble, scrambleNames } from './joblist/JobInfo.svelte'
@ -25,13 +25,6 @@
let metrics = ccconfig.plot_list_selectedMetrics, isMetricsSelectionOpen = false
let w1, w2, histogramHeight = 250
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null
let resize = false
/* Resize Context
* A) Each viewport change triggers histogram rerender due to variable dimensions clearing canvas if not rerendered
* B) Opening filters (and some other things) triggers small change in viewport dimensions (Fix here?)
* A+B) Histogram rerenders if filters opened, high performance impact if dataload heavy
* Solution: Default to fixed histogram dimensions, allow user to enable automatic resizing
*/
const client = getContextClient();
$: stats = queryStore({
@ -137,47 +130,31 @@
<th scope="row">Total Core Hours</th>
<td>{$stats.data.jobsStatistics[0].totalCoreHours}</td>
</tr>
<!-- <tr>
<th scope="row">Toggle Histogram Resizing</th>
<td><Input id="c3" value={resize} type="switch" on:change={() => (resize = !resize)}/></td>
</tr> -->
</tbody>
</Table>
</Col>
<div class="col-4" style="text-align: center;" bind:clientWidth={w1}>
<b>Duration Distribution</b>
<div class="col-4 text-center" bind:clientWidth={w1}>
{#key $stats.data.jobsStatistics[0].histDuration}
{#if resize == true}
<Histogram
data={$stats.data.jobsStatistics[0].histDuration}
width={w1 - 25} height={histogramHeight}
xlabel="Current Runtimes [h]"
ylabel="Number of Jobs"/>
{:else}
<Histogram
data={$stats.data.jobsStatistics[0].histDuration}
width={400} height={250}
xlabel="Current Runtimes [h]"
ylabel="Number of Jobs"/>
{/if}
<Histogramuplot
data={convert2uplot($stats.data.jobsStatistics[0].histDuration)}
width={w2 - 25} height={histogramHeight}
title="Duration Distribution"
xlabel="Current Runtimes"
xunit="Hours"
ylabel="Number of Jobs"
yunit="Jobs"/>
{/key}
</div>
<div class="col-4" style="text-align: center;" bind:clientWidth={w2}>
<b>Number of Nodes Distribution</b>
<div class="col-4 text-center" bind:clientWidth={w2}>
{#key $stats.data.jobsStatistics[0].histNumNodes}
{#if resize == true}
<Histogram
data={$stats.data.jobsStatistics[0].histNumNodes}
<Histogramuplot
data={convert2uplot($stats.data.jobsStatistics[0].histNumNodes)}
width={w2 - 25} height={histogramHeight}
xlabel="Allocated Nodes [#]"
ylabel="Number of Jobs" />
{:else}
<Histogram
data={$stats.data.jobsStatistics[0].histNumNodes}
width={400} height={250}
xlabel="Allocated Nodes [#]"
ylabel="Number of Jobs" />
{/if}
title="Number of Nodes Distribution"
xlabel="Allocated Nodes"
xunit="Nodes"
ylabel="Number of Jobs"
yunit="Jobs"/>
{/key}
</div>
{/if}

View File

@ -136,6 +136,7 @@
{cluster}
subCluster={job.subCluster}
isShared={(job.exclusive != 1)}
resources={job.resources}
/>
{:else if metric.disabled == true && metric.data}
<Card body color="info">Metric disabled for subcluster <code>{metric.data.name}:{job.subCluster}</code></Card>

View File

@ -0,0 +1,216 @@
<!--
@component
Properties:
- Todo
-->
<script>
import uPlot from 'uplot'
import { formatNumber } from '../units.js'
import { onMount, onDestroy } from 'svelte'
import { Card } from 'sveltestrap'
export let data
export let width = 500
export let height = 300
export let title = ''
export let xlabel = ''
export let xunit = 'X'
export let ylabel = ''
export let yunit = 'Y'
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,
},
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}

View File

@ -26,16 +26,17 @@
import { getContext, onMount, onDestroy } from 'svelte'
import { Card } from '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 metric
export let useStatsSeries = null
export let scope = 'node'
export let isShared = false
if (useStatsSeries == null)
@ -53,6 +54,70 @@
const backgroundColors = { normal: 'rgba(255, 255, 255, 1.0)', caution: 'rgba(255, 128, 0, 0.3)', alert: 'rgba(255, 0, 0, 0.3)' }
const thresholds = findThresholds(metricConfig, scope, typeof subCluster == 'string' ? cluster.subClusters.find(sc => sc.name == subCluster) : subCluster)
// 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/Avg 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
@ -93,7 +158,7 @@
? (statisticsSeries.max.reduce((max, x) => Math.max(max, x), thresholds.normal) || thresholds.normal)
: (series.reduce((max, series) => Math.max(max, series.statistics?.max), thresholds.normal) || thresholds.normal)
: null
const plotSeries = [{}]
const plotSeries = [{label: 'Runtime', value: (u, ts, sidx, didx) => didx == null ? null : formatTime(ts)}]
const plotData = [new Array(longestSeries)]
for (let i = 0; i < longestSeries; i++) // TODO: Cache/Reuse this array?
plotData[0][i] = i * timestep
@ -103,9 +168,9 @@
plotData.push(statisticsSeries.min)
plotData.push(statisticsSeries.max)
plotData.push(statisticsSeries.mean)
plotSeries.push({ scale: 'y', width: lineWidth, stroke: 'red' })
plotSeries.push({ scale: 'y', width: lineWidth, stroke: 'green' })
plotSeries.push({ scale: 'y', width: lineWidth, stroke: 'black' })
plotSeries.push({ label: 'min', scale: 'y', width: lineWidth, stroke: 'red' })
plotSeries.push({ label: 'max', scale: 'y', width: lineWidth, stroke: 'green' })
plotSeries.push({ label: 'mean', scale: 'y', width: lineWidth, stroke: 'black' })
plotBands = [
{ series: [2,3], fill: 'rgba(0,255,0,0.1)' },
{ series: [3,1], fill: 'rgba(255,0,0,0.1)' }
@ -114,6 +179,9 @@
for (let i = 0; i < series.length; i++) {
plotData.push(series[i].data)
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)
@ -124,6 +192,9 @@
const opts = {
width,
height,
plugins: [
legendAsTooltipPlugin()
],
series: plotSeries,
axes: [
{
@ -177,8 +248,11 @@
x: { time: false },
y: maxY ? { range: [0., maxY * 1.1] } : {}
},
cursor: { show: false },
legend: { show: false, live: false }
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 } }
}
// console.log(opts)
@ -249,16 +323,21 @@
}
</script>
<script context="module">
export function formatTime(t) {
let h = Math.floor(t / 3600)
let m = Math.floor((t % 3600) / 60)
if (h == 0)
return `${m}m`
else if (m == 0)
return `${h}h`
else
return `${h}:${m}h`
if (t !== null) {
if (isNaN(t)) {
return t
} else {
let h = Math.floor(t / 3600)
let m = Math.floor((t % 3600) / 60)
if (h == 0)
return `${m}m`
else if (m == 0)
return `${h}h`
else
return `${h}:${m}h`
}
}
}
export function timeIncrs(timestep, maxX) {
@ -323,8 +402,9 @@
{#if series[0].data.length > 0}
<div bind:this={plotWrapper} class="cc-plot"></div>
{:else}
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="warning">Cannot render plot: No series data returned for <code>{metric}</code></Card>
<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;

View File

@ -6,11 +6,15 @@ const power = [1, 1e3, 1e6, 1e9, 1e12, 1e15, 1e18, 1e21]
const prefix = ['', 'K', 'M', 'G', 'T', 'P', 'E']
export function formatNumber(x) {
for (let i = 0; i < prefix.length; i++)
if (power[i] <= x && x < power[i+1])
return `${Math.round((x / power[i]) * 100) / 100} ${prefix[i]}`
if ( isNaN(x) ) {
return x // Return if String , used in Histograms
} else {
for (let i = 0; i < prefix.length; i++)
if (power[i] <= x && x < power[i+1])
return `${Math.round((x / power[i]) * 100) / 100} ${prefix[i]}`
return Math.abs(x) >= 1000 ? x.toExponential() : x.toString()
return Math.abs(x) >= 1000 ? x.toExponential() : x.toString()
}
}
export function scaleNumbers(x, y , p = '') {

View File

@ -313,3 +313,13 @@ export function checkMetricDisabled(m, c, s) { //[m]etric, [c]luster, [s]ubclust
}
return false;
}
export function convert2uplot(canvasData) {
// initial use: Canvas Histogram Data to Uplot
let uplotData = [[],[]] // [X, Y1, Y2, ...]
canvasData.forEach( pair => {
uplotData[0].push(pair.value)
uplotData[1].push(pair.count)
})
return uplotData
}