cc-backend/web/frontend/src/Analysis.root.svelte

313 lines
12 KiB
Svelte
Raw Normal View History

2022-06-22 11:20:57 +02:00
<script>
import { init, convert2uplot } from './utils.js'
2022-06-22 11:20:57 +02:00
import { getContext, onMount } from 'svelte'
import { queryStore, gql, getContextClient } from '@urql/svelte'
2022-06-22 11:20:57 +02:00
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 Pie from './plots/Pie.svelte'
import { binsFromFootprint } from './plots/Histogram.svelte'
2022-06-22 11:20:57 +02:00
import ScatterPlot from './plots/Scatter.svelte'
import PlotTable from './PlotTable.svelte'
import Roofline from './plots/Roofline.svelte'
const { query: initq } = init()
export let filterPresets
// By default, look at the jobs of the last 6 hours:
if (filterPresets?.startTime == null) {
if (filterPresets == null)
filterPresets = {}
let now = new Date(Date.now())
let hourAgo = new Date(now)
hourAgo.setHours(hourAgo.getHours() - 6)
filterPresets.startTime = { from: hourAgo.toISOString(), to: now.toISOString() }
}
let cluster;
let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
let jobFilters = [];
let rooflineMaxY;
let colWidth1, colWidth2, colWidth3, colWidth4;
let numBins = 50;
let maxY = -1;
const ccconfig = getContext('cc-config')
const metricConfig = getContext('metrics')
2022-06-22 11:20:57 +02:00
let metricsInHistograms = ccconfig.analysis_view_histogramMetrics,
metricsInScatterplots = ccconfig.analysis_view_scatterPlotMetrics
$: metrics = [...new Set([...metricsInHistograms, ...metricsInScatterplots.flat()])]
getContext('on-init')(({ data }) => {
if (data != null) {
cluster = data.clusters.find(c => c.name == filterPresets.cluster)
console.assert(cluster != null, `This cluster could not be found: ${filterPresets.cluster}`)
rooflineMaxY = cluster.subClusters.reduce((max, part) => Math.max(max, part.flopRateSimd.value), 0)
maxY = rooflineMaxY
2022-06-22 11:20:57 +02:00
}
})
const client = getContextClient();
$: statsQuery = queryStore({
client: client,
query: gql`
query($jobFilters: [JobFilter!]!) {
stats: jobsStatistics(filter: $jobFilters) {
2023-05-12 11:19:37 +02:00
totalJobs
shortJobs
totalWalltime
totalCoreHours
histDuration { count, value }
histNumNodes { count, value }
}
2023-06-09 12:42:01 +02:00
topUsers: jobsCount(filter: $jobFilters, groupBy: USER, weight: NODE_HOURS, limit: 5) { name, count }
2022-06-22 11:20:57 +02:00
}
2023-05-12 11:19:37 +02:00
`,
variables: { jobFilters }
})
2022-06-22 11:20:57 +02:00
$: footprintsQuery = queryStore({
client: client,
query: gql`
query($jobFilters: [JobFilter!]!, $metrics: [String!]!) {
footprints: jobsFootprints(filter: $jobFilters, metrics: $metrics) {
2023-05-12 11:19:37 +02:00
nodehours,
metrics { metric, data }
}
}`,
variables: { jobFilters, metrics }
})
2022-06-22 11:20:57 +02:00
$: rooflineQuery = queryStore({
client: client,
query: gql`
query($jobFilters: [JobFilter!]!, $rows: Int!, $cols: Int!,
2023-05-12 11:19:37 +02:00
$minX: Float!, $minY: Float!, $maxX: Float!, $maxY: Float!) {
rooflineHeatmap(filter: $jobFilters, rows: $rows, cols: $cols,
2023-05-12 11:19:37 +02:00
minX: $minX, minY: $minY, maxX: $maxX, maxY: $maxY)
}
`,
variables: { jobFilters, rows: 50, cols: 50, minX: 0.01, minY: 1., maxX: 1000., maxY }
})
2022-06-22 11:20:57 +02:00
onMount(() => filterComponent.update())
2022-06-22 11:20:57 +02:00
</script>
<Row>
{#if $initq.fetching || $statsQuery.fetching || $footprintsQuery.fetching}
<Col xs="auto">
<Spinner />
</Col>
{/if}
<Col xs="auto">
{#if $initq.error}
<Card body color="danger">{$initq.error.message}</Card>
{:else if cluster}
<PlotSelection
availableMetrics={cluster.metricConfig.map(mc => mc.name)}
bind:metricsInHistograms={metricsInHistograms}
bind:metricsInScatterplots={metricsInScatterplots} />
{/if}
</Col>
<Col xs="auto">
<Filters
bind:this={filterComponent}
2022-06-22 11:20:57 +02:00
filterPresets={filterPresets}
disableClusterSelection={true}
startTimeQuickSelect={true}
on:update={({ detail }) => {
jobFilters = detail.filters;
2022-06-22 11:20:57 +02:00
}} />
</Col>
</Row>
<br/>
{#if $statsQuery.error}
<Row>
<Col>
<Card body color="danger">{$statsQuery.error.message}</Card>
</Col>
</Row>
{:else if $statsQuery.data}
<Row cols={3} class="mb-4">
<Col>
<Table>
<tr>
<th scope="col">Total Jobs</th>
<td>{$statsQuery.data.stats[0].totalJobs}</td>
</tr>
<tr>
<th scope="col">Short Jobs</th>
<td>{$statsQuery.data.stats[0].shortJobs}</td>
</tr>
<tr>
<th scope="col">Total Walltime</th>
<td>{$statsQuery.data.stats[0].totalWalltime}</td>
</tr>
<tr>
<th scope="col">Total Core Hours</th>
<td>{$statsQuery.data.stats[0].totalCoreHours}</td>
</tr>
</Table>
</Col>
<Col>
<div bind:clientWidth={colWidth1}>
<h5>Top Users</h5>
{#key $statsQuery.data.topUsers}
<Pie
size={colWidth1}
sliceLabel='Hours'
quantities={$statsQuery.data.topUsers.sort((a, b) => b.count - a.count).map((tu) => tu.count)}
entities={$statsQuery.data.topUsers.sort((a, b) => b.count - a.count).map((tu) => tu.name)}
/>
{/key}
</div>
</Col>
<Col>
<Table>
<tr class="mb-2"><th>User Name</th><th>Node Hours</th></tr>
{#each $statsQuery.data.topUsers.sort((a, b) => b.count - a.count) as { name, count }}
2022-06-22 11:20:57 +02:00
<tr>
<th scope="col"><a href="/monitoring/user/{name}?cluster={cluster.name}">{name}</a></th>
<td>{count}</td>
2022-06-22 11:20:57 +02:00
</tr>
{/each}
</Table>
</Col>
</Row>
<Row cols={3} class="mb-2">
<Col>
{#if $rooflineQuery.fetching}
<Spinner />
{:else if $rooflineQuery.error}
<Card body color="danger">{$rooflineQuery.error.message}</Card>
{:else if $rooflineQuery.data && cluster}
<div bind:clientWidth={colWidth2}>
{#key $rooflineQuery.data}
<Roofline
width={colWidth2} height={300}
tiles={$rooflineQuery.data.rooflineHeatmap}
cluster={cluster.subClusters.length == 1 ? cluster.subClusters[0] : null}
maxY={rooflineMaxY} />
2022-06-22 11:20:57 +02:00
{/key}
</div>
{/if}
</Col>
<Col>
<div bind:clientWidth={colWidth3}>
2022-06-22 11:20:57 +02:00
{#key $statsQuery.data.stats[0].histDuration}
<Histogramuplot
width={colWidth3} height={300}
data={convert2uplot($statsQuery.data.stats[0].histDuration)}
title="Duration Distribution"
xlabel="Current Runtimes"
xunit="Hours"
ylabel="Number of Jobs"
yunit="Jobs"/>
2022-06-22 11:20:57 +02:00
{/key}
</div>
</Col>
<Col>
<div bind:clientWidth={colWidth4}>
2022-06-22 11:20:57 +02:00
{#key $statsQuery.data.stats[0].histNumNodes}
<Histogramuplot
width={colWidth4} height={300}
data={convert2uplot($statsQuery.data.stats[0].histNumNodes)}
title="Number of Nodes Distribution"
xlabel="Allocated Nodes"
xunit="Nodes"
ylabel="Number of Jobs"
yunit="Jobs"/>
2022-06-22 11:20:57 +02:00
{/key}
</div>
</Col>
2022-06-22 11:20:57 +02:00
</Row>
{/if}
<hr class="my-6"/>
2022-06-22 11:20:57 +02:00
{#if $footprintsQuery.error}
<Row>
<Col>
<Card body color="danger">{$footprintsQuery.error.message}</Card>
</Col>
</Row>
{:else if $footprintsQuery.data && $initq.data}
<Row>
<Col>
<Card body>
These histograms show the distribution of the averages of all jobs matching the filters. Each job/average is weighted by its node hours.
Note that some metrics could be disabled for specific subclusters as per metriConfig and thus could affect shown average values.
2022-06-22 11:20:57 +02:00
</Card>
<br/>
</Col>
</Row>
<Row>
<Col>
<PlotTable
let:item
let:width
renderFor="analysis"
2022-06-22 11:20:57 +02:00
items={metricsInHistograms.map(metric => ({ metric, ...binsFromFootprint(
$footprintsQuery.data.footprints.nodehours,
$footprintsQuery.data.footprints.metrics.find(f => f.metric == metric).data, numBins) }))}
itemsPerRow={ccconfig.plot_view_plotsPerRow}>
<Histogramuplot
data={convert2uplot(item.bins)}
2022-06-22 11:20:57 +02:00
width={width} height={250}
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 : '')}]`}
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"/>
2022-06-22 11:20:57 +02:00
</PlotTable>
</Col>
</Row>
<br/>
<Row>
<Col>
<Card body>
Each circle represents one job. The size of a circle is proportional to its node hours. Darker circles mean multiple jobs have the same averages for the respective metrics.
Note that some metrics could be disabled for specific subclusters as per metriConfig and thus could affect shown average values.
2022-06-22 11:20:57 +02:00
</Card>
<br/>
</Col>
</Row>
<Row>
<Col>
<PlotTable
let:item
let:width
items={metricsInScatterplots.map(([m1, m2]) => ({
m1, f1: $footprintsQuery.data.footprints.metrics.find(f => f.metric == m1).data,
m2, f2: $footprintsQuery.data.footprints.metrics.find(f => f.metric == m2).data }))}
itemsPerRow={ccconfig.plot_view_plotsPerRow}>
<ScatterPlot
width={width} height={250} color={"rgba(0, 102, 204, 0.33)"}
xLabel={`${item.m1} [${(metricConfig(cluster.name, item.m1)?.unit?.prefix ? metricConfig(cluster.name, item.m1)?.unit?.prefix : '') +
(metricConfig(cluster.name, item.m1)?.unit?.base ? metricConfig(cluster.name, item.m1)?.unit?.base : '')}]`}
yLabel={`${item.m2} [${(metricConfig(cluster.name, item.m2)?.unit?.prefix ? metricConfig(cluster.name, item.m2)?.unit?.prefix : '') +
(metricConfig(cluster.name, item.m2)?.unit?.base ? metricConfig(cluster.name, item.m2)?.unit?.base : '')}]`}
2022-06-22 11:20:57 +02:00
X={item.f1} Y={item.f2} S={$footprintsQuery.data.footprints.nodehours} />
</PlotTable>
</Col>
</Row>
{/if}
<style>
h5 {
text-align: center;
}
</style>