Merge branch 'master' into 71_improve_systemsview

This commit is contained in:
Christoph Kluge 2023-08-11 10:34:05 +02:00 committed by GitHub
commit a2cc1bd226
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 411 additions and 103 deletions

View File

@ -326,7 +326,6 @@ func (pdb *PrometheusDataRepository) LoadData(
Timestep: metricConfig.Timestep, Timestep: metricConfig.Timestep,
Series: make([]schema.Series, 0), Series: make([]schema.Series, 0),
} }
jobData[metric][scope] = jobMetric
} }
step := int64(metricConfig.Timestep) step := int64(metricConfig.Timestep)
steps := int64(to.Sub(from).Seconds()) / step steps := int64(to.Sub(from).Seconds()) / step
@ -335,6 +334,10 @@ func (pdb *PrometheusDataRepository) LoadData(
jobMetric.Series = append(jobMetric.Series, jobMetric.Series = append(jobMetric.Series,
pdb.RowToSeries(from, step, steps, row)) pdb.RowToSeries(from, step, steps, row))
} }
// only add metric if at least one host returned data
if !ok && len(jobMetric.Series) > 0{
jobData[metric][scope] = jobMetric
}
// sort by hostname to get uniform coloring // sort by hostname to get uniform coloring
sort.Slice(jobMetric.Series, func(i, j int) bool { sort.Slice(jobMetric.Series, func(i, j int) bool {
return (jobMetric.Series[i].Hostname < jobMetric.Series[j].Hostname) return (jobMetric.Series[i].Hostname < jobMetric.Series[j].Hostname)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,13 @@
<script> <script>
import { onMount, getContext } from 'svelte' 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 { Table, Row, Col, Button, Icon, Card, Spinner, Input } from 'sveltestrap'
import { queryStore, gql, getContextClient } from '@urql/svelte' import { queryStore, gql, getContextClient } from '@urql/svelte'
import Filters from './filters/Filters.svelte' import Filters from './filters/Filters.svelte'
import JobList from './joblist/JobList.svelte' import JobList from './joblist/JobList.svelte'
import Sorting from './joblist/SortSelection.svelte' import Sorting from './joblist/SortSelection.svelte'
import Refresher from './joblist/Refresher.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 MetricSelection from './MetricSelection.svelte'
import { scramble, scrambleNames } from './joblist/JobInfo.svelte' import { scramble, scrambleNames } from './joblist/JobInfo.svelte'
@ -25,13 +25,6 @@
let metrics = ccconfig.plot_list_selectedMetrics, isMetricsSelectionOpen = false let metrics = ccconfig.plot_list_selectedMetrics, isMetricsSelectionOpen = false
let w1, w2, histogramHeight = 250 let w1, w2, histogramHeight = 250
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null 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(); const client = getContextClient();
$: stats = queryStore({ $: stats = queryStore({
@ -137,47 +130,31 @@
<th scope="row">Total Core Hours</th> <th scope="row">Total Core Hours</th>
<td>{$stats.data.jobsStatistics[0].totalCoreHours}</td> <td>{$stats.data.jobsStatistics[0].totalCoreHours}</td>
</tr> </tr>
<!-- <tr>
<th scope="row">Toggle Histogram Resizing</th>
<td><Input id="c3" value={resize} type="switch" on:change={() => (resize = !resize)}/></td>
</tr> -->
</tbody> </tbody>
</Table> </Table>
</Col> </Col>
<div class="col-4" style="text-align: center;" bind:clientWidth={w1}> <div class="col-4 text-center" bind:clientWidth={w1}>
<b>Duration Distribution</b>
{#key $stats.data.jobsStatistics[0].histDuration} {#key $stats.data.jobsStatistics[0].histDuration}
{#if resize == true} <Histogramuplot
<Histogram data={convert2uplot($stats.data.jobsStatistics[0].histDuration)}
data={$stats.data.jobsStatistics[0].histDuration} width={w2 - 25} height={histogramHeight}
width={w1 - 25} height={histogramHeight} title="Duration Distribution"
xlabel="Current Runtimes [h]" xlabel="Current Runtimes"
ylabel="Number of Jobs"/> xunit="Hours"
{:else} ylabel="Number of Jobs"
<Histogram yunit="Jobs"/>
data={$stats.data.jobsStatistics[0].histDuration}
width={400} height={250}
xlabel="Current Runtimes [h]"
ylabel="Number of Jobs"/>
{/if}
{/key} {/key}
</div> </div>
<div class="col-4" style="text-align: center;" bind:clientWidth={w2}> <div class="col-4 text-center" bind:clientWidth={w2}>
<b>Number of Nodes Distribution</b>
{#key $stats.data.jobsStatistics[0].histNumNodes} {#key $stats.data.jobsStatistics[0].histNumNodes}
{#if resize == true} <Histogramuplot
<Histogram data={convert2uplot($stats.data.jobsStatistics[0].histNumNodes)}
data={$stats.data.jobsStatistics[0].histNumNodes}
width={w2 - 25} height={histogramHeight} width={w2 - 25} height={histogramHeight}
xlabel="Allocated Nodes [#]" title="Number of Nodes Distribution"
ylabel="Number of Jobs" /> xlabel="Allocated Nodes"
{:else} xunit="Nodes"
<Histogram ylabel="Number of Jobs"
data={$stats.data.jobsStatistics[0].histNumNodes} yunit="Jobs"/>
width={400} height={250}
xlabel="Allocated Nodes [#]"
ylabel="Number of Jobs" />
{/if}
{/key} {/key}
</div> </div>
{/if} {/if}

View File

@ -136,6 +136,7 @@
{cluster} {cluster}
subCluster={job.subCluster} subCluster={job.subCluster}
isShared={(job.exclusive != 1)} isShared={(job.exclusive != 1)}
resources={job.resources}
/> />
{:else if metric.disabled == true && metric.data} {:else if metric.disabled == true && metric.data}
<Card body color="info">Metric disabled for subcluster <code>{metric.data.name}:{job.subCluster}</code></Card> <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 { getContext, onMount, onDestroy } from 'svelte'
import { Card } from 'sveltestrap' import { Card } from 'sveltestrap'
export let metric
export let scope = 'node'
export let resources = []
export let width export let width
export let height export let height
export let timestep export let timestep
export let series export let series
export let useStatsSeries = null
export let statisticsSeries = null export let statisticsSeries = null
export let cluster export let cluster
export let subCluster export let subCluster
export let metric
export let useStatsSeries = null
export let scope = 'node'
export let isShared = false export let isShared = false
export let forNode = false export let forNode = false
@ -54,6 +55,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 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) 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() { function backgroundColor() {
if (clusterCockpitConfig.plot_general_colorBackground == false if (clusterCockpitConfig.plot_general_colorBackground == false
|| !thresholds || !thresholds
@ -94,7 +159,7 @@
? (statisticsSeries.max.reduce((max, x) => Math.max(max, x), thresholds.normal) || thresholds.normal) ? (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) : (series.reduce((max, series) => Math.max(max, series.statistics?.max), thresholds.normal) || thresholds.normal)
: null : null
const plotSeries = [{}] const plotSeries = [{label: 'Runtime', value: (u, ts, sidx, didx) => didx == null ? null : formatTime(ts)}]
const plotData = [new Array(longestSeries)] const plotData = [new Array(longestSeries)]
if (forNode === true) { if (forNode === true) {
@ -113,14 +178,17 @@
plotData.push(statisticsSeries.min) plotData.push(statisticsSeries.min)
plotData.push(statisticsSeries.max) plotData.push(statisticsSeries.max)
plotData.push(statisticsSeries.mean) plotData.push(statisticsSeries.mean)
if (forNode === true) { // timestamp 0 with null value for reversed time axis if (forNode === true) { // timestamp 0 with null value for reversed time axis
if (plotData[1].length != 0) plotData[1].push(null) if (plotData[1].length != 0) plotData[1].push(null)
if (plotData[2].length != 0) plotData[2].push(null) if (plotData[2].length != 0) plotData[2].push(null)
if (plotData[3].length != 0) plotData[3].push(null) if (plotData[3].length != 0) plotData[3].push(null)
} }
plotSeries.push({ scale: 'y', width: lineWidth, stroke: 'red' })
plotSeries.push({ scale: 'y', width: lineWidth, stroke: 'green' }) plotSeries.push({ label: 'min', scale: 'y', width: lineWidth, stroke: 'red' })
plotSeries.push({ scale: 'y', width: lineWidth, stroke: 'black' }) plotSeries.push({ label: 'max', scale: 'y', width: lineWidth, stroke: 'green' })
plotSeries.push({ label: 'mean', scale: 'y', width: lineWidth, stroke: 'black' })
plotBands = [ plotBands = [
{ series: [2,3], fill: 'rgba(0,255,0,0.1)' }, { series: [2,3], fill: 'rgba(0,255,0,0.1)' },
{ series: [3,1], fill: 'rgba(255,0,0,0.1)' } { series: [3,1], fill: 'rgba(255,0,0,0.1)' }
@ -130,6 +198,9 @@
plotData.push(series[i].data) 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 if (forNode === true && plotData[1].length != 0) plotData[1].push(null) // timestamp 0 with null value for reversed time axis
plotSeries.push({ plotSeries.push({
label: scope === 'node' ? resources[i].hostname :
// scope === 'accelerator' ? resources[0].accelerators[i] :
scope + ' #' + (i+1),
scale: 'y', scale: 'y',
width: lineWidth, width: lineWidth,
stroke: lineColor(i, series.length) stroke: lineColor(i, series.length)
@ -140,6 +211,9 @@
const opts = { const opts = {
width, width,
height, height,
plugins: [
legendAsTooltipPlugin()
],
series: plotSeries, series: plotSeries,
axes: [ axes: [
{ {
@ -193,8 +267,11 @@
x: { time: false }, x: { time: false },
y: maxY ? { range: [0., maxY * 1.1] } : {} y: maxY ? { range: [0., maxY * 1.1] } : {}
}, },
cursor: { show: false }, legend : { // Display legend until max 12 Y-dataseries
legend: { show: false, live: false } 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) // console.log(opts)
@ -265,8 +342,11 @@
} }
</script> </script>
<script context="module"> <script context="module">
export function formatTime(t) {
export function formatTime(t, forNode = false) { if (t !== null) {
if (isNaN(t)) {
return t
} else {
let h = Math.floor(t / 3600) let h = Math.floor(t / 3600)
let m = Math.floor((t % 3600) / 60) let m = Math.floor((t % 3600) / 60)
if (h == 0) if (h == 0)
@ -276,6 +356,8 @@
else else
return `${h}:${m}h` return `${h}:${m}h`
} }
}
}
export function timeIncrs(timestep, maxX, forNode) { export function timeIncrs(timestep, maxX, forNode) {
if (forNode === true) { if (forNode === true) {
@ -343,8 +425,9 @@
{#if series[0].data.length > 0} {#if series[0].data.length > 0}
<div bind:this={plotWrapper} class="cc-plot"></div> <div bind:this={plotWrapper} class="cc-plot"></div>
{:else} {: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} {/if}
<style> <style>
.cc-plot { .cc-plot {
border-radius: 5px; border-radius: 5px;

View File

@ -6,12 +6,16 @@ const power = [1, 1e3, 1e6, 1e9, 1e12, 1e15, 1e18, 1e21]
const prefix = ['', 'K', 'M', 'G', 'T', 'P', 'E'] const prefix = ['', 'K', 'M', 'G', 'T', 'P', 'E']
export function formatNumber(x) { export function formatNumber(x) {
if ( isNaN(x) ) {
return x // Return if String , used in Histograms
} else {
for (let i = 0; i < prefix.length; i++) for (let i = 0; i < prefix.length; i++)
if (power[i] <= x && x < power[i+1]) if (power[i] <= x && x < power[i+1])
return `${Math.round((x / power[i]) * 100) / 100} ${prefix[i]}` 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 = '') { export function scaleNumbers(x, y , p = '') {
const oldPower = power[prefix.indexOf(p)] const oldPower = power[prefix.indexOf(p)]

View File

@ -313,3 +313,13 @@ export function checkMetricDisabled(m, c, s) { //[m]etric, [c]luster, [s]ubclust
} }
return false; 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
}