mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2024-12-27 05:49:04 +01:00
Merge branch 'master' into 71_improve_systemsview
This commit is contained in:
commit
a2cc1bd226
@ -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)
|
||||||
|
@ -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>
|
||||||
|
@ -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"
|
||||||
|
@ -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}
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
|
216
web/frontend/src/plots/Histogramuplot.svelte
Normal file
216
web/frontend/src/plots/Histogramuplot.svelte
Normal 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}
|
||||||
|
|
||||||
|
|
@ -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;
|
||||||
|
@ -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)]
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user