Merge pull request #323 from ClusterCockpit/add_histogram_bin_select

Add histogram bin select
This commit is contained in:
Jan Eitzinger
2025-01-28 13:38:15 +01:00
committed by GitHub
9 changed files with 1391 additions and 375 deletions

View File

@@ -174,6 +174,7 @@
},
});
// Note: Different footprints than those saved in DB per Job -> Caused by Legacy Naming
$: footprintsQuery = queryStore({
client: client,
query: gql`
@@ -470,10 +471,12 @@
height={300}
data={convert2uplot($statsQuery.data.stats[0].histDuration)}
title="Duration Distribution"
xlabel="Current Runtimes"
xunit="Hours"
xlabel="Current Job Runtimes"
xunit="Runtime"
ylabel="Number of Jobs"
yunit="Jobs"
usesBins
xtime
/>
{/key}
</Col>

View File

@@ -463,7 +463,7 @@
<hr />
<!-- Usage Stats as Histograms -->
<!-- User and Project Stats as Pie-Charts -->
<Row cols={{ lg: 4, md: 2, sm: 1 }}>
<Col class="p-2">
@@ -587,17 +587,23 @@
{/key}
</Col>
</Row>
<hr class="my-2" />
<!-- Static Stats as Histograms : Running Duration && Allocated Hardware Counts-->
<Row cols={{ lg: 2, md: 1 }}>
<Col class="p-2">
{#key $mainQuery.data.stats}
<Histogram
data={convert2uplot($mainQuery.data.stats[0].histDuration)}
title="Duration Distribution"
xlabel="Current Runtimes"
xunit="Hours"
xlabel="Current Job Runtimes"
xunit="Runtime"
ylabel="Number of Jobs"
yunit="Jobs"
usesBins
xtime
/>
{/key}
</Col>
@@ -640,7 +646,11 @@
{/key}
</Col>
</Row>
<hr class="my-2" />
<!-- Selectable Stats as Histograms : Average Values of Running Jobs -->
{#if metricsInHistograms}
{#key $mainQuery.data.stats[0].histMetrics}
<PlotGrid

View File

@@ -17,6 +17,9 @@
Icon,
Card,
Spinner,
Input,
InputGroup,
InputGroupText
} from "@sveltestrap/sveltestrap";
import {
queryStore,
@@ -59,6 +62,11 @@
let showFootprint = filterPresets.cluster
? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
: !!ccconfig.plot_list_showFootprint;
let numDurationBins = "1h";
let numMetricBins = 10;
let durationBinOptions = ["1m","10m","1h","6h","12h"];
let metricBinOptions = [10, 20, 50, 100];
$: metricsInHistograms = selectedCluster
? ccconfig[`user_view_histogramMetrics:${selectedCluster}`] || []
@@ -68,8 +76,8 @@
$: stats = queryStore({
client: client,
query: gql`
query ($jobFilters: [JobFilter!]!, $metricsInHistograms: [String!]) {
jobsStatistics(filter: $jobFilters, metrics: $metricsInHistograms) {
query ($jobFilters: [JobFilter!]!, $metricsInHistograms: [String!], $numDurationBins: String, $numMetricBins: Int) {
jobsStatistics(filter: $jobFilters, metrics: $metricsInHistograms, numDurationBins: $numDurationBins , numMetricBins: $numMetricBins ) {
totalJobs
shortJobs
totalWalltime
@@ -96,7 +104,7 @@
}
}
`,
variables: { jobFilters, metricsInHistograms },
variables: { jobFilters, metricsInHistograms, numDurationBins, numMetricBins },
});
onMount(() => filterComponent.updateFilters());
@@ -118,8 +126,8 @@
{/if}
<!-- ROW2: Tools-->
<Row cols={{ xs: 1, md: 2, lg: 4}} class="mb-3">
<Col lg="2" class="mb-2 mb-lg-0">
<Row cols={{ xs: 1, md: 2, lg: 6}} class="mb-3">
<Col class="mb-2 mb-lg-0">
<ButtonGroup class="w-100">
<Button outline color="primary" on:click={() => (isSortingOpen = true)}>
<Icon name="sort-up" /> Sorting
@@ -133,7 +141,7 @@
</Button>
</ButtonGroup>
</Col>
<Col lg="4" xl="6" class="mb-1 mb-lg-0">
<Col lg="4" class="mb-1 mb-lg-0">
<Filters
{filterPresets}
{matchedJobs}
@@ -148,12 +156,27 @@
}}
/>
</Col>
<Col lg="3" xl="2" class="mb-2 mb-lg-0">
<Col class="mb-2 mb-lg-0">
<InputGroup>
<InputGroupText>
<Icon name="bar-chart-line-fill" />
</InputGroupText>
<InputGroupText>
Duration Bin Size
</InputGroupText>
<Input type="select" bind:value={numDurationBins} style="max-width: 120px;">
{#each durationBinOptions as dbin}
<option value={dbin}>{dbin}</option>
{/each}
</Input>
</InputGroup>
</Col>
<Col class="mb-2 mb-lg-0">
<TextFilter
on:set-filter={({ detail }) => filterComponent.updateFilters(detail)}
/>
</Col>
<Col lg="3" xl="2" class="mb-1 mb-lg-0">
<Col class="mb-1 mb-lg-0">
<Refresher on:refresh={() => {
jobList.refreshJobs()
jobList.refreshAllMetrics()
@@ -215,10 +238,12 @@
<Histogram
data={convert2uplot($stats.data.jobsStatistics[0].histDuration)}
title="Duration Distribution"
xlabel="Current Runtimes"
xunit="Hours"
xlabel="Job Runtimes"
xunit="Runtime"
ylabel="Number of Jobs"
yunit="Jobs"
usesBins
xtime
/>
{/key}
</Col>
@@ -238,16 +263,32 @@
</Row>
<!-- ROW4+5: Selectable Histograms -->
<Row cols={{ xs: 1, md: 5}}>
<Col>
<Row>
<Col xs="12" md="3" lg="2" class="mb-2 mb-md-0">
<Button
outline
color="secondary"
class="w-100"
on:click={() => (isHistogramSelectionOpen = true)}
>
<Icon name="bar-chart-line" /> Select Histograms
</Button>
</Col>
<Col xs="12" md="9" lg="10" class="mb-2 mb-md-0">
<InputGroup>
<InputGroupText>
<Icon name="bar-chart-line-fill" />
</InputGroupText>
<InputGroupText>
Metric Bins
</InputGroupText>
<Input type="select" bind:value={numMetricBins} style="max-width: 120px;">
{#each metricBinOptions as mbin}
<option value={mbin}>{mbin}</option>
{/each}
</Input>
</InputGroup>
</Col>
</Row>
{#if metricsInHistograms?.length > 0}
{#if $stats.error}
@@ -273,12 +314,12 @@
>
<Histogram
data={convert2uplot(item.data)}
usesBins={true}
title="Distribution of '{item.metric} ({item.stat})' footprints"
xlabel={`${item.metric} bin maximum ${item?.unit ? `[${item.unit}]` : ``}`}
xunit={item.unit}
ylabel="Number of Jobs"
yunit="Jobs"
usesBins
/>
</PlotGrid>
{/key}

View File

@@ -15,8 +15,8 @@
<script>
import uPlot from "uplot";
import { formatNumber } from "../units.js";
import { onMount, onDestroy } from "svelte";
import { formatNumber } from "../units.js";
import { Card } from "@sveltestrap/sveltestrap";
export let data;
@@ -26,16 +26,31 @@
export let title = "";
export let xlabel = "";
export let xunit = "";
export let xtime = false;
export let ylabel = "";
export let yunit = "";
const { bars } = uPlot.paths;
const drawStyles = {
bars: 1,
points: 2,
};
function formatTime(t) {
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);
if (h == 0) return `${m}m`;
else if (m == 0) return `${h}h`;
else return `${h}:${m}h`;
}
}
}
function paths(u, seriesIdx, idx0, idx1, extendGap, buildClip) {
let s = u.series[seriesIdx];
let style = s.drawStyle;
@@ -139,7 +154,7 @@
label: xlabel,
labelGap: 10,
size: 25,
incrs: [1, 2, 5, 6, 10, 12, 50, 100, 500, 1000, 5000, 10000],
incrs: xtime ? [60, 120, 300, 600, 1800, 3600, 7200, 14400, 18000, 21600, 43200, 86400] : [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000],
border: {
show: true,
stroke: "#000000",
@@ -149,7 +164,13 @@
size: 5 / devicePixelRatio,
stroke: "#000000",
},
values: (_, t) => t.map((v) => formatNumber(v)),
values: (_, t) => t.map((v) => {
if (xtime) {
return formatTime(v);
} else {
return formatNumber(v)
}
}),
},
{
stroke: "#000000",
@@ -166,17 +187,25 @@
size: 5 / devicePixelRatio,
stroke: "#000000",
},
values: (_, t) => t.map((v) => formatNumber(v)),
values: (_, t) => t.map((v) => {
return formatNumber(v)
}),
},
],
series: [
{
label: xunit !== "" ? xunit : null,
value: (u, ts, sidx, didx) => {
if (usesBins) {
if (usesBins && xtime) {
const min = u.data[sidx][didx - 1] ? formatTime(u.data[sidx][didx - 1]) : 0;
const max = formatTime(u.data[sidx][didx]);
ts = min + "-" + max; // narrow spaces
} else 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
} else if (xtime) {
ts = formatTime(ts);
}
return ts;
},
@@ -191,6 +220,7 @@
},
{
drawStyle: drawStyles.bars,
width: 1, // 1 / lastBinCount,
lineInterpolation: null,
stroke: "#85abce",
fill: "#85abce", // + "1A", // Transparent Fill

View File

@@ -438,7 +438,7 @@ function getMetricConfigDeep(metric, cluster, subCluster) {
}
}
export function convert2uplot(canvasData) {
export function convert2uplot(canvasData, secondsToMinutes = false, secondsToHours = false) {
// Prep: Uplot Data Structure
let uplotData = [[],[]] // [X, Y1, Y2, ...]
// Iterate if exists
@@ -446,11 +446,21 @@ export function convert2uplot(canvasData) {
canvasData.forEach( cd => {
if (Object.keys(cd).length == 4) { // MetricHisto Datafromat
uplotData[0].push(cd?.max ? cd.max : 0)
uplotData[1].push(cd?.count ? cd.count : 0)
} else { // Default -> Fill Histodata with zero values on unused value placing -> maybe allows zoom trigger as known
if (secondsToHours) {
let hours = cd.value / 3600
console.log("x seconds to y hours", cd.value, hours)
uplotData[0].push(hours)
} else if (secondsToMinutes) {
let minutes = cd.value / 60
console.log("x seconds to y minutes", cd.value, minutes)
uplotData[0].push(minutes)
} else {
uplotData[0].push(cd.value)
}
uplotData[1].push(cd.count)
} else { // Default
uplotData[0].push(cd.value)
uplotData[1].push(cd.count)
}
}
})
}
return uplotData