Files
cc-backend/web/frontend/src/status/dashdetails/UsageDash.svelte

581 lines
17 KiB
Svelte

<!--
@component Main cluster status view component; renders current system-usage information
Properties:
- `presetCluster String`: The cluster to show status information for
- `presetSubCluster String?`: The subCluster to show status information for [Default: null]
- `useCbColors Bool?`: Use colorblind friendly colors [Default: false]
- `useAltColors Bool?`: Use alternative color set [Default: false]
-->
<script>
import {
Row,
Col,
Spinner,
Card,
Table,
Icon,
Tooltip,
Input,
InputGroup,
InputGroupText,
} from "@sveltestrap/sveltestrap";
import { queryStore, gql, getContextClient } from "@urql/svelte";
import {
scramble,
scrambleNames,
convert2uplot,
} from "../../generic/utils.js";
import Pie, { colors } from "../../generic/plots/Pie.svelte";
import Histogram from "../../generic/plots/Histogram.svelte";
import Refresher from "../../generic/helper/Refresher.svelte";
/* Svelte 5 Props */
let {
presetCluster,
presetSubCluster = null,
useCbColors = false,
useAltColors = false,
loadMe = false,
} = $props();
/* Const Init */
const client = getContextClient();
const durationBinOptions = ["1m", "10m", "1h", "6h", "12h"];
/* State Init */
let pagingState = $state({ page: 1, itemsPerPage: 10 }); // Top 10
let selectedHistograms = $state([]); // Dummy For Refresh
let colWidthJobs = $state(0);
let colWidthNodes = $state(0);
let colWidthAccs = $state(0);
let numDurationBins = $state("1h");
/* Derived */
const canvasPrefix = $derived(
`${presetCluster}-${presetSubCluster ? presetSubCluster : ""}`,
);
const statusFilter = $derived(
presetSubCluster
? [
{ cluster: { eq: presetCluster } },
{ subCluster: { eq: presetSubCluster } },
{ state: ["running"] },
]
: [{ cluster: { eq: presetCluster } }, { state: ["running"] }],
);
const topStatsQuery = $derived(
loadMe
? queryStore({
client: client,
query: gql`
query ($filter: [JobFilter!]!) {
allUsers: jobsStatistics(
filter: $filter
groupBy: USER
) {
id
name
totalJobs
totalNodes
totalAccs
}
allProjects: jobsStatistics(
filter: $filter
groupBy: PROJECT
) {
id
totalJobs
totalNodes
totalAccs
}
}
`,
variables: {
filter: statusFilter,
},
requestPolicy: "network-only",
})
: null,
);
// Sort + slice top-10 from the full results in the frontend
const topUserJobs = $derived(
$topStatsQuery?.data?.allUsers
?.toSorted((a, b) => b.totalJobs - a.totalJobs)
.slice(0, 10) ?? [],
);
const topProjectJobs = $derived(
$topStatsQuery?.data?.allProjects
?.toSorted((a, b) => b.totalJobs - a.totalJobs)
.slice(0, 10) ?? [],
);
const topUserNodes = $derived(
$topStatsQuery?.data?.allUsers
?.toSorted((a, b) => b.totalNodes - a.totalNodes)
.slice(0, 10) ?? [],
);
const topProjectNodes = $derived(
$topStatsQuery?.data?.allProjects
?.toSorted((a, b) => b.totalNodes - a.totalNodes)
.slice(0, 10) ?? [],
);
const topUserAccs = $derived(
$topStatsQuery?.data?.allUsers
?.toSorted((a, b) => b.totalAccs - a.totalAccs)
.slice(0, 10) ?? [],
);
const topProjectAccs = $derived(
$topStatsQuery?.data?.allProjects
?.toSorted((a, b) => b.totalAccs - a.totalAccs)
.slice(0, 10) ?? [],
);
// Note: nodeMetrics are requested on configured $timestep resolution
const nodeStatusQuery = $derived(
loadMe
? queryStore({
client: client,
query: gql`
query (
$filter: [JobFilter!]!
$selectedHistograms: [String!]
$numDurationBins: String
) {
jobsStatistics(
filter: $filter
metrics: $selectedHistograms
numDurationBins: $numDurationBins
) {
histDuration {
count
value
}
histNumNodes {
count
value
}
histNumAccs {
count
value
}
}
}
`,
variables: {
filter: statusFilter,
selectedHistograms: selectedHistograms, // No Metrics requested for node hardware stats
numDurationBins: numDurationBins,
},
requestPolicy: "network-only",
})
: null,
);
/* Functions */
function legendColors(targetIdx) {
// Reuses first color if targetIdx overflows
let c;
if (useCbColors) {
c = [...colors["colorblind"]];
} else if (useAltColors) {
c = [...colors["alternative"]];
} else {
c = [...colors["default"]];
}
return c[(c.length + targetIdx) % c.length];
}
</script>
<!-- Refresher and space for other options -->
<Row class="justify-content-between">
<Col class="mb-2 mb-md-0" xs="12" md="5" lg="4" xl="3">
<InputGroup>
<InputGroupText>
<Icon name="bar-chart-line-fill" />
</InputGroupText>
<InputGroupText>Duration Bin Size</InputGroupText>
<Input type="select" bind:value={numDurationBins}>
{#each durationBinOptions as dbin}
<option value={dbin}>{dbin}</option>
{/each}
</Input>
</InputGroup>
</Col>
<Col xs="12" md="5" lg="4" xl="3">
<Refresher
initially={120}
onRefresh={() => {
pagingState = { page: 1, itemsPerPage: 10 };
selectedHistograms = [...$state.snapshot(selectedHistograms)];
}}
/>
</Col>
</Row>
<hr />
<!-- Job Duration, Top Users and Projects-->
{#if $topStatsQuery?.fetching || $nodeStatusQuery?.fetching}
<Spinner />
{:else if $topStatsQuery?.data && $nodeStatusQuery?.data}
<Row>
<Col xs="12" lg="4" class="p-2">
{#key $nodeStatusQuery?.data?.jobsStatistics[0]?.histDuration}
<Histogram
data={convert2uplot(
$nodeStatusQuery?.data?.jobsStatistics[0]?.histDuration,
)}
title="Duration Distribution"
xlabel="Current Job Runtimes"
xunit="Runtime"
ylabel="Number of Jobs"
yunit="Jobs"
height="275"
usesBins
xtime
enableFlip
/>
{/key}
</Col>
<Col xs="6" md="3" lg="2" class="p-2">
<div bind:clientWidth={colWidthJobs}>
<h4 class="text-center">Top Users: Jobs</h4>
<Pie
{useAltColors}
canvasId="{canvasPrefix}-hpcpie-jobs-users"
size={colWidthJobs * 0.75}
sliceLabel="Jobs"
quantities={topUserJobs.map(
(tu) => tu["totalJobs"],
)}
entities={topUserJobs.map((tu) =>
scrambleNames ? scramble(tu.id) : tu.id,
)}
/>
</div>
</Col>
<Col xs="6" md="3" lg="2" class="p-2">
<Table>
<tr class="mb-2">
<th></th>
<th style="padding-left: 0.5rem;">User</th>
<th>Jobs</th>
</tr>
{#each topUserJobs as tu, i}
<tr>
<td
><Icon name="circle-fill" style="color: {legendColors(i)};" /></td
>
<td id="{canvasPrefix}-topName-jobs-{tu.id}">
<a
target="_blank"
href="/monitoring/user/{tu.id}?cluster={presetCluster}{presetSubCluster
? '&partition=' + presetSubCluster
: ''}&state=running"
>{scrambleNames ? scramble(tu.id) : tu.id}
</a>
</td>
{#if tu?.name}
<Tooltip
target={`${canvasPrefix}-topName-jobs-${tu.id}`}
placement="left"
>{scrambleNames ? scramble(tu.name) : tu.name}</Tooltip
>
{/if}
<td>{tu["totalJobs"]}</td>
</tr>
{/each}
</Table>
</Col>
<Col xs="6" md="3" lg="2" class="p-2">
<h4 class="text-center">Top Projects: Jobs</h4>
<Pie
{useAltColors}
canvasId="{canvasPrefix}-hpcpie-jobs-projects"
size={colWidthJobs * 0.75}
sliceLabel={"Jobs"}
quantities={topProjectJobs.map(
(tp) => tp["totalJobs"],
)}
entities={topProjectJobs.map((tp) =>
scrambleNames ? scramble(tp.id) : tp.id,
)}
/>
</Col>
<Col xs="6" md="3" lg="2" class="p-2">
<Table>
<tr class="mb-2">
<th></th>
<th style="padding-left: 0.5rem;">Project</th>
<th>Jobs</th>
</tr>
{#each topProjectJobs as tp, i}
<tr>
<td
><Icon name="circle-fill" style="color: {legendColors(i)};" /></td
>
<td>
<a
target="_blank"
href="/monitoring/jobs/?cluster={presetCluster}{presetSubCluster
? '&partition=' + presetSubCluster
: ''}&state=running&project={tp.id}&projectMatch=eq"
>{scrambleNames ? scramble(tp.id) : tp.id}
</a>
</td>
<td>{tp["totalJobs"]}</td>
</tr>
{/each}
</Table>
</Col>
</Row>
{:else}
<Card class="mx-4" body color="warning"
>Cannot render job status charts: No data!</Card
>
{/if}
<hr />
<!-- Node Distribution, Top Users and Projects-->
{#if $topStatsQuery?.fetching || $nodeStatusQuery?.fetching}
<Spinner />
{:else if $topStatsQuery?.data && $nodeStatusQuery?.data}
<Row>
<Col xs="12" lg="4" class="p-2">
<Histogram
data={convert2uplot(
$nodeStatusQuery?.data?.jobsStatistics[0]?.histNumNodes,
)}
title="Number of Nodes Distribution"
xlabel="Allocated Nodes"
xunit="Nodes"
ylabel="Number of Jobs"
yunit="Jobs"
height="275"
enableFlip
/>
</Col>
<Col xs="6" md="3" lg="2" class="p-2">
<div bind:clientWidth={colWidthNodes}>
<h4 class="text-center">Top Users: Nodes</h4>
<Pie
{useAltColors}
canvasId="{canvasPrefix}-hpcpie-nodes-users"
size={colWidthNodes * 0.75}
sliceLabel="Nodes"
quantities={topUserNodes.map(
(tu) => tu["totalNodes"],
)}
entities={topUserNodes.map((tu) =>
scrambleNames ? scramble(tu.id) : tu.id,
)}
/>
</div>
</Col>
<Col xs="6" md="3" lg="2" class="p-2">
<Table>
<tr class="mb-2">
<th></th>
<th style="padding-left: 0.5rem;">User</th>
<th>Nodes</th>
</tr>
{#each topUserNodes as tu, i}
<tr>
<td
><Icon name="circle-fill" style="color: {legendColors(i)};" /></td
>
<td id="{canvasPrefix}-topName-nodes-{tu.id}">
<a
target="_blank"
href="/monitoring/user/{tu.id}?cluster={presetCluster}{presetSubCluster
? '&partition=' + presetSubCluster
: ''}&state=running"
>{scrambleNames ? scramble(tu.id) : tu.id}
</a>
</td>
{#if tu?.name}
<Tooltip
target={`${canvasPrefix}-topName-nodes-${tu.id}`}
placement="left"
>{scrambleNames ? scramble(tu.name) : tu.name}</Tooltip
>
{/if}
<td>{tu["totalNodes"]}</td>
</tr>
{/each}
</Table>
</Col>
<Col xs="6" md="3" lg="2" class="p-2">
<h4 class="text-center">Top Projects: Nodes</h4>
<Pie
{useAltColors}
canvasId="{canvasPrefix}-hpcpie-nodes-projects"
size={colWidthNodes * 0.75}
sliceLabel={"Nodes"}
quantities={topProjectNodes.map(
(tp) => tp["totalNodes"],
)}
entities={topProjectNodes.map((tp) =>
scrambleNames ? scramble(tp.id) : tp.id,
)}
/>
</Col>
<Col xs="6" md="3" lg="2" class="p-2">
<Table>
<tr class="mb-2">
<th></th>
<th style="padding-left: 0.5rem;">Project</th>
<th>Nodes</th>
</tr>
{#each topProjectNodes as tp, i}
<tr>
<td
><Icon name="circle-fill" style="color: {legendColors(i)};" /></td
>
<td>
<a
target="_blank"
href="/monitoring/jobs/?cluster={presetCluster}{presetSubCluster
? '&partition=' + presetSubCluster
: ''}&state=running&project={tp.id}&projectMatch=eq"
>{scrambleNames ? scramble(tp.id) : tp.id}
</a>
</td>
<td>{tp["totalNodes"]}</td>
</tr>
{/each}
</Table>
</Col>
</Row>
{:else}
<Card class="mx-4" body color="warning"
>Cannot render node status charts: No data!</Card
>
{/if}
<hr />
<!-- Acc Distribution, Top Users and Projects-->
{#if $topStatsQuery?.fetching || $nodeStatusQuery?.fetching}
<Spinner />
{:else if $topStatsQuery?.data && $nodeStatusQuery?.data}
<Row>
<Col xs="12" lg="4" class="p-2">
<Histogram
data={convert2uplot(
$nodeStatusQuery?.data?.jobsStatistics[0]?.histNumAccs,
)}
title="Number of Accelerators Distribution"
xlabel="Allocated Accs"
xunit="Accs"
ylabel="Number of Jobs"
yunit="Jobs"
height="275"
enableFlip
/>
</Col>
<Col xs="6" md="3" lg="2" class="p-2">
<div bind:clientWidth={colWidthAccs}>
<h4 class="text-center">Top Users: GPUs</h4>
<Pie
{useAltColors}
canvasId="{canvasPrefix}-hpcpie-accs-users"
size={colWidthAccs * 0.75}
sliceLabel="GPUs"
quantities={topUserAccs.map(
(tu) => tu["totalAccs"],
)}
entities={topUserAccs.map((tu) =>
scrambleNames ? scramble(tu.id) : tu.id,
)}
/>
</div>
</Col>
<Col xs="6" md="3" lg="2" class="p-2">
<Table>
<tr class="mb-2">
<th></th>
<th style="padding-left: 0.5rem;">User</th>
<th>GPUs</th>
</tr>
{#each topUserAccs as tu, i}
<tr>
<td
><Icon name="circle-fill" style="color: {legendColors(i)};" /></td
>
<td id="{canvasPrefix}-topName-accs-{tu.id}">
<a
target="_blank"
href="/monitoring/user/{tu.id}?cluster={presetCluster}{presetSubCluster
? '&partition=' + presetSubCluster
: ''}&state=running"
>{scrambleNames ? scramble(tu.id) : tu.id}
</a>
</td>
{#if tu?.name}
<Tooltip
target={`${canvasPrefix}-topName-accs-${tu.id}`}
placement="left"
>{scrambleNames ? scramble(tu.name) : tu.name}</Tooltip
>
{/if}
<td>{tu["totalAccs"]}</td>
</tr>
{/each}
</Table>
</Col>
<Col xs="6" md="3" lg="2" class="p-2">
<h4 class="text-center">Top Projects: GPUs</h4>
<Pie
{useAltColors}
canvasId="{canvasPrefix}-hpcpie-accs-projects"
size={colWidthAccs * 0.75}
sliceLabel={"GPUs"}
quantities={topProjectAccs.map(
(tp) => tp["totalAccs"],
)}
entities={topProjectAccs.map((tp) =>
scrambleNames ? scramble(tp.id) : tp.id,
)}
/>
</Col>
<Col xs="6" md="3" lg="2" class="p-2">
<Table>
<tr class="mb-2">
<th></th>
<th style="padding-left: 0.5rem;">Project</th>
<th>GPUs</th>
</tr>
{#each topProjectAccs as tp, i}
<tr>
<td
><Icon name="circle-fill" style="color: {legendColors(i)};" /></td
>
<td>
<a
target="_blank"
href="/monitoring/jobs/?cluster={presetCluster}{presetSubCluster
? '&partition=' + presetSubCluster
: ''}&state=running&project={tp.id}&projectMatch=eq"
>{scrambleNames ? scramble(tp.id) : tp.id}
</a>
</td>
<td>{tp["totalAccs"]}</td>
</tr>
{/each}
</Table>
</Col>
</Row>
{:else}
<Card class="mx-4" body color="warning"
>Cannot render accelerator status charts: No data!</Card
>
{/if}