Adapt svelte to new schema, add removed metric box

- Moved 'scope' field to parent jobMetric
- Implemented unit { prefix, base } where necessary
- SubCluster Metric Config 'remove' option implemented in Joblists
This commit is contained in:
Christoph Kluge 2023-03-30 15:21:35 +02:00
parent 6b84e65d88
commit 0c1b66aad9
18 changed files with 313 additions and 105 deletions

View File

@ -269,6 +269,7 @@ func (r *queryResolver) NodeMetrics(ctx context.Context, cluster string, nodes [
for _, scopedMetric := range scopedMetrics { for _, scopedMetric := range scopedMetrics {
host.Metrics = append(host.Metrics, &model.JobMetricWithName{ host.Metrics = append(host.Metrics, &model.JobMetricWithName{
Name: metric, Name: metric,
Scope: schema.MetricScopeNode, // NodeMetrics allow fixed scope?
Metric: scopedMetric, Metric: scopedMetric,
}) })
} }
@ -282,7 +283,16 @@ func (r *queryResolver) NodeMetrics(ctx context.Context, cluster string, nodes [
// NumberOfNodes is the resolver for the numberOfNodes field. // NumberOfNodes is the resolver for the numberOfNodes field.
func (r *subClusterResolver) NumberOfNodes(ctx context.Context, obj *schema.SubCluster) (int, error) { func (r *subClusterResolver) NumberOfNodes(ctx context.Context, obj *schema.SubCluster) (int, error) {
panic(fmt.Errorf("not implemented: NumberOfNodes - numberOfNodes")) nodeList, err := archive.ParseNodeList(obj.Nodes)
if err != nil {
return 0, err
}
// log.Debugf(">>>> See raw list definition here: %v", nodeList)
stringList := nodeList.PrintList()
// log.Debugf(">>>> See parsed list here: %v", stringList)
numOfNodes := len(stringList)
// log.Debugf(">>>> See numOfNodes here: %v", len(stringList))
return numOfNodes, nil
} }
// Cluster returns generated.ClusterResolver implementation. // Cluster returns generated.ClusterResolver implementation.

View File

@ -324,10 +324,13 @@ func ArchiveJob(job *schema.Job, ctx context.Context) (*schema.JobMeta, error) {
} }
jobMeta.Statistics[metric] = schema.JobStatistics{ jobMeta.Statistics[metric] = schema.JobStatistics{
Unit: archive.GetMetricConfig(job.Cluster, metric).Unit, Unit: schema.Unit{
Avg: avg / float64(job.NumNodes), Prefix: archive.GetMetricConfig(job.Cluster, metric).Unit.Prefix,
Min: min, Base: archive.GetMetricConfig(job.Cluster, metric).Unit.Base,
Max: max, },
Avg: avg / float64(job.NumNodes),
Min: min,
Max: max,
} }
} }

View File

@ -14,6 +14,8 @@ import (
type NodeList [][]interface { type NodeList [][]interface {
consume(input string) (next string, ok bool) consume(input string) (next string, ok bool)
limits() []map[string]int64
prefix() string
} }
func (nl *NodeList) Contains(name string) bool { func (nl *NodeList) Contains(name string) bool {
@ -35,6 +37,29 @@ func (nl *NodeList) Contains(name string) bool {
return false return false
} }
func (nl *NodeList) PrintList() []string {
var out []string
for _, term := range *nl {
// log.Debugf("Term: %v", term)
prefix := term[0].prefix()
// log.Debugf("Prefix as String: %s", prefix)
limitArr := term[1].limits()
for _, inner := range limitArr {
for i := inner["start"]; i < inner["end"]+1; i++ {
node := fmt.Sprintf("%s%02d", prefix, i)
out = append(out, node)
}
// log.Debugf("Inner Map @ %d: %#v", indx, inner)
// log.Debugf("Start: %#v", inner["start"])
// log.Debugf("End: %#v", inner["end"])
}
}
// log.Debugf("Node List as Strings: %#v", out)
return out
}
type NLExprString string type NLExprString string
func (nle NLExprString) consume(input string) (next string, ok bool) { func (nle NLExprString) consume(input string) (next string, ok bool) {
@ -45,6 +70,16 @@ func (nle NLExprString) consume(input string) (next string, ok bool) {
return "", false return "", false
} }
func (nle NLExprString) limits() []map[string]int64 {
// Null implementation to fullfill interface requirement
l := make([]map[string]int64, 0)
return l
}
func (nle NLExprString) prefix() string {
return string(nle)
}
type NLExprIntRanges []NLExprIntRange type NLExprIntRanges []NLExprIntRange
func (nles NLExprIntRanges) consume(input string) (next string, ok bool) { func (nles NLExprIntRanges) consume(input string) (next string, ok bool) {
@ -56,6 +91,22 @@ func (nles NLExprIntRanges) consume(input string) (next string, ok bool) {
return "", false return "", false
} }
func (nles NLExprIntRanges) limits() []map[string]int64 {
l := make([]map[string]int64, 0)
for _, nle := range nles {
inner := nle.limits()
// log.Debugf("limits @ nles: %#v", inner)
l = append(l, inner[0])
}
return l
}
func (nles NLExprIntRanges) prefix() string {
// Null implementation to fullfill interface requirement
var s string
return s
}
type NLExprIntRange struct { type NLExprIntRange struct {
start, end int64 start, end int64
zeroPadded bool zeroPadded bool
@ -89,6 +140,22 @@ func (nle NLExprIntRange) consume(input string) (next string, ok bool) {
return "", false return "", false
} }
func (nle NLExprIntRange) limits() []map[string]int64 {
l := make([]map[string]int64, 0)
m := make(map[string]int64)
m["start"] = nle.start
m["end"] = nle.end
l = append(l, m)
// log.Debugf("limits @ nle: %#v", l)
return l
}
func (nles NLExprIntRange) prefix() string {
// Null implementation to fullfill interface requirement
var s string
return s
}
func ParseNodeList(raw string) (NodeList, error) { func ParseNodeList(raw string) (NodeList, error) {
isLetter := func(r byte) bool { return ('a' <= r && r <= 'z') || ('A' <= r && r <= 'Z') } isLetter := func(r byte) bool { return ('a' <= r && r <= 'z') || ('A' <= r && r <= 'Z') }
isDigit := func(r byte) bool { return '0' <= r && r <= '9' } isDigit := func(r byte) bool { return '0' <= r && r <= '9' }
@ -116,6 +183,8 @@ func ParseNodeList(raw string) (NodeList, error) {
for _, rawterm := range rawterms { for _, rawterm := range rawterms {
exprs := []interface { exprs := []interface {
consume(input string) (next string, ok bool) consume(input string) (next string, ok bool)
limits() []map[string]int64
prefix() string
}{} }{}
for i := 0; i < len(rawterm); i++ { for i := 0; i < len(rawterm); i++ {
c := rawterm[i] c := rawterm[i]

View File

@ -30,8 +30,8 @@
let rooflineMaxY let rooflineMaxY
let colWidth let colWidth
let numBins = 50 let numBins = 50
const ccconfig = getContext('cc-config'), const ccconfig = getContext('cc-config')
metricConfig = getContext('metrics') const metricConfig = getContext('metrics')
let metricsInHistograms = ccconfig.analysis_view_histogramMetrics, let metricsInHistograms = ccconfig.analysis_view_histogramMetrics,
metricsInScatterplots = ccconfig.analysis_view_scatterPlotMetrics metricsInScatterplots = ccconfig.analysis_view_scatterPlotMetrics
@ -161,24 +161,29 @@
<Histogram <Histogram
width={colWidth - 25} height={300 * 0.5} width={colWidth - 25} height={300 * 0.5}
data={$statsQuery.data.topUsers.sort((a, b) => b.count - a.count).map(({ count }, idx) => ({ count, value: idx }))} data={$statsQuery.data.topUsers.sort((a, b) => b.count - a.count).map(({ count }, idx) => ({ count, value: idx }))}
label={(x) => x < $statsQuery.data.topUsers.length ? $statsQuery.data.topUsers[Math.floor(x)].name : '0'} /> label={(x) => x < $statsQuery.data.topUsers.length ? $statsQuery.data.topUsers[Math.floor(x)].name : 'No Users'}
ylabel="Node Hours [h]"/>
{/key} {/key}
</div> </div>
</div> </div>
<div class="col-3"> <div class="col-3">
{#key $statsQuery.data.stats[0].histDuration} {#key $statsQuery.data.stats[0].histDuration}
<h4>Walltime Distribution</h4> <h4>Duration Distribution</h4>
<Histogram <Histogram
width={colWidth - 25} height={300} width={colWidth - 25}
data={$statsQuery.data.stats[0].histDuration} /> data={$statsQuery.data.stats[0].histDuration}
xlabel="Current Runtimes [h]"
ylabel="Number of 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> <h4>Number of Nodes Distribution</h4>
<Histogram <Histogram
width={colWidth - 25} height={300} width={colWidth - 25}
data={$statsQuery.data.stats[0].histNumNodes} /> data={$statsQuery.data.stats[0].histNumNodes}
xlabel="Allocated Nodes [#]"
ylabel="Number of Jobs" />
{/key} {/key}
</div> </div>
<div class="col-3"> <div class="col-3">
@ -189,7 +194,7 @@
{:else if $rooflineQuery.data && cluster} {:else if $rooflineQuery.data && cluster}
{#key $rooflineQuery.data} {#key $rooflineQuery.data}
<Roofline <Roofline
width={colWidth - 25} height={300} width={colWidth - 25}
tiles={$rooflineQuery.data.rooflineHeatmap} tiles={$rooflineQuery.data.rooflineHeatmap}
cluster={cluster.subClusters.length == 1 ? cluster.subClusters[0] : null} cluster={cluster.subClusters.length == 1 ? cluster.subClusters[0] : null}
maxY={rooflineMaxY} /> maxY={rooflineMaxY} />
@ -224,12 +229,16 @@
$footprintsQuery.data.footprints.nodehours, $footprintsQuery.data.footprints.nodehours,
$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>{item.metric} [{metricConfig(cluster.name, item.metric)?.unit}]</h4> <h4>Average Distribution of '{item.metric}'</h4>
<Histogram <Histogram
width={width} height={250} width={width} height={250}
min={item.min} max={item.max} min={item.min} max={item.max}
data={item.bins} label={item.label} /> data={item.bins}
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 : '')}]`}
ylabel="Node Hours [h]" />
</PlotTable> </PlotTable>
</Col> </Col>
</Row> </Row>
@ -254,12 +263,18 @@
<ScatterPlot <ScatterPlot
width={width} height={250} color={"rgba(0, 102, 204, 0.33)"} width={width} height={250} color={"rgba(0, 102, 204, 0.33)"}
xLabel={`${item.m1} [${metricConfig(cluster.name, item.m1)?.unit}]`} xLabel={`${item.m1} [${(metricConfig(cluster.name, item.m1)?.unit?.prefix ? metricConfig(cluster.name, item.m1)?.unit?.prefix : '') +
yLabel={`${item.m2} [${metricConfig(cluster.name, item.m2)?.unit}]`} (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 : '')}]`}
X={item.f1} Y={item.f2} S={$footprintsQuery.data.footprints.nodehours} /> X={item.f1} Y={item.f2} S={$footprintsQuery.data.footprints.nodehours} />
</PlotTable> </PlotTable>
</Col> </Col>
</Row> </Row>
{/if} {/if}
<style>
h4 {
text-align: center;
}
</style>

View File

@ -81,7 +81,7 @@
missingMetrics = metricNames.filter(metric => !metrics.some(jm => jm.name == metric)) missingMetrics = metricNames.filter(metric => !metrics.some(jm => jm.name == metric))
missingHosts = job.resources.map(({ hostname }) => ({ missingHosts = job.resources.map(({ hostname }) => ({
hostname: hostname, hostname: hostname,
metrics: metricNames.filter(metric => !metrics.some(jm => jm.metric.scope == 'node' && jm.metric.series.some(series => series.hostname == hostname))) metrics: metricNames.filter(metric => !metrics.some(jm => jm.scope == 'node' && jm.metric.series.some(series => series.hostname == hostname)))
})).filter(({ metrics }) => metrics.length > 0) })).filter(({ metrics }) => metrics.length > 0)
somethingMissing = missingMetrics.length > 0 || missingHosts.length > 0 somethingMissing = missingMetrics.length > 0 || missingHosts.length > 0
} }
@ -114,8 +114,8 @@
cluster={clusters cluster={clusters
.find(c => c.name == $initq.data.job.cluster).subClusters .find(c => c.name == $initq.data.job.cluster).subClusters
.find(sc => sc.name == $initq.data.job.subCluster)} .find(sc => sc.name == $initq.data.job.subCluster)}
flopsAny={$jobMetrics.data.jobMetrics.find(m => m.name == 'flops_any' && m.metric.scope == 'node').metric} flopsAny={$jobMetrics.data.jobMetrics.find(m => m.name == 'flops_any' && m.scope == 'node').metric}
memBw={$jobMetrics.data.jobMetrics.find(m => m.name == 'mem_bw' && m.metric.scope == 'node').metric} /> memBw={$jobMetrics.data.jobMetrics.find(m => m.name == 'mem_bw' && m.scope == 'node').metric} />
</Col> </Col>
{:else} {:else}
<Col></Col> <Col></Col>
@ -163,8 +163,9 @@
bind:this={plots[item.metric]} bind:this={plots[item.metric]}
on:more-loaded={({ detail }) => statsTable.moreLoaded(detail)} on:more-loaded={({ detail }) => statsTable.moreLoaded(detail)}
job={$initq.data.job} job={$initq.data.job}
metric={item.metric} metricName={item.metric}
scopes={item.data.map(x => x.metric)} rawData={item.data.map(x => x.metric)}
scopes={item.data.map(x => x.scope)}
width={width}/> width={width}/>
{:else} {:else}
<Card body color="warning">No data for <code>{item.metric}</code></Card> <Card body color="warning">No data for <code>{item.metric}</code></Card>

View File

@ -5,21 +5,36 @@
import { fetchMetrics, minScope } from './utils' import { fetchMetrics, minScope } from './utils'
export let job export let job
export let metric export let metricName
export let scopes export let scopes
export let width export let width
export let rawData
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const cluster = getContext('clusters').find(cluster => cluster.name == job.cluster) const cluster = getContext('clusters').find(cluster => cluster.name == job.cluster)
const subCluster = cluster.subClusters.find(subCluster => subCluster.name == job.subCluster) const subCluster = cluster.subClusters.find(subCluster => subCluster.name == job.subCluster)
const metricConfig = cluster.metricConfig.find(metricConfig => metricConfig.name == metric) const metricConfig = cluster.metricConfig.find(metricConfig => metricConfig.name == metricName)
let selectedHost = null, plot, fetching = false, error = null
let selectedScope = minScope(scopes)
let selectedScopeIndex = scopes.findIndex(s => s == selectedScope)
// console.log('- Inputs -')
// console.log(metricName)
// console.log(scopes)
// console.log(rawData)
// console.log('- Prep Scopes -')
// console.log(selectedScope)
// console.log(selectedScopeIndex)
let selectedScope = minScope(scopes.map(s => s.scope)), selectedHost = null, plot, fetching = false, error = null $: avaliableScopes = scopes
$: data = rawData[selectedScopeIndex]
$: avaliableScopes = scopes.map(metric => metric.scope)
$: data = scopes.find(metric => metric.scope == selectedScope)
$: series = data?.series.filter(series => selectedHost == null || series.hostname == selectedHost) $: series = data?.series.filter(series => selectedHost == null || series.hostname == selectedHost)
// console.log('- Prep Data -')
// console.log(rawData[selectedScopeIndex])
// console.log(rawData[selectedScopeIndex].series.filter(series => selectedHost == null || series.hostname == selectedHost))
let from = null, to = null let from = null, to = null
export function setTimeRange(f, t) { export function setTimeRange(f, t) {
from = f, to = t from = f, to = t
@ -29,7 +44,7 @@
export async function loadMore() { export async function loadMore() {
fetching = true fetching = true
let response = await fetchMetrics(job, [metric], ["core"]) let response = await fetchMetrics(job, [metricName], ["core"])
fetching = false fetching = false
if (response.error) { if (response.error) {
@ -38,9 +53,9 @@
} }
for (let jm of response.data.jobMetrics) { for (let jm of response.data.jobMetrics) {
if (jm.metric.scope != "node") { if (jm.scope != "node") {
scopes.push(jm.metric) scopes.push(jm.metric)
selectedScope = jm.metric.scope selectedScope = jm.scope
dispatch('more-loaded', jm) dispatch('more-loaded', jm)
if (!avaliableScopes.includes(selectedScope)) if (!avaliableScopes.includes(selectedScope))
avaliableScopes = [...avaliableScopes, selectedScope] avaliableScopes = [...avaliableScopes, selectedScope]
@ -52,7 +67,8 @@
</script> </script>
<InputGroup> <InputGroup>
<InputGroupText style="min-width: 150px;"> <InputGroupText style="min-width: 150px;">
{metric} ({metricConfig?.unit}) {metricName} ({(metricConfig?.unit?.prefix ? metricConfig.unit.prefix : '') +
(metricConfig?.unit?.base ? metricConfig.unit.base : '')})
</InputGroupText> </InputGroupText>
<select class="form-select" bind:value={selectedScope}> <select class="form-select" bind:value={selectedScope}>
{#each avaliableScopes as scope} {#each avaliableScopes as scope}
@ -82,7 +98,7 @@
width={width} height={300} width={width} height={300}
cluster={cluster} subCluster={subCluster} cluster={cluster} subCluster={subCluster}
timestep={data.timestep} timestep={data.timestep}
scope={selectedScope} metric={metric} scope={selectedScope} metric={metricName}
series={series} /> series={series} />
{/if} {/if}
{/key} {/key}

View File

@ -20,16 +20,19 @@
from.setMinutes(from.getMinutes() - 30) from.setMinutes(from.getMinutes() - 30)
} }
const ccconfig = getContext('cc-config'), clusters = getContext('clusters') const ccconfig = getContext('cc-config')
const clusters = getContext('clusters')
const nodesQuery = operationStore(`query($cluster: String!, $nodes: [String!], $from: Time!, $to: Time!) { const nodesQuery = operationStore(`query($cluster: String!, $nodes: [String!], $from: Time!, $to: Time!) {
nodeMetrics(cluster: $cluster, nodes: $nodes, from: $from, to: $to) { nodeMetrics(cluster: $cluster, nodes: $nodes, from: $from, to: $to) {
host, subCluster host
subCluster
metrics { metrics {
name, name
scope
metric { metric {
timestep timestep
scope unit { base, prefix }
series { series {
statistics { min, avg, max } statistics { min, avg, max }
data data
@ -46,6 +49,17 @@
$: $nodesQuery.variables = { cluster, nodes: [hostname], from: from.toISOString(), to: to.toISOString() } $: $nodesQuery.variables = { cluster, nodes: [hostname], from: from.toISOString(), to: to.toISOString() }
let metricUnits = {}
$: if ($nodesQuery.data) {
for (let metric of clusters.find(c => c.name == cluster).metricConfig) {
if (metric.unit.prefix || metric.unit.base) {
metricUnits[metric.name] = '(' + (metric.unit.prefix ? metric.unit.prefix : '') + (metric.unit.base ? metric.unit.base : '') + ')'
} else { // If no unit defined: Omit Unit Display
metricUnits[metric.name] = ''
}
}
}
query(nodesQuery) query(nodesQuery)
$: console.log($nodesQuery?.data?.nodeMetrics[0].metrics) $: console.log($nodesQuery?.data?.nodeMetrics[0].metrics)
@ -83,7 +97,7 @@
let:width let:width
itemsPerRow={ccconfig.plot_view_plotsPerRow} itemsPerRow={ccconfig.plot_view_plotsPerRow}
items={$nodesQuery.data.nodeMetrics[0].metrics.sort((a, b) => a.name.localeCompare(b.name))}> items={$nodesQuery.data.nodeMetrics[0].metrics.sort((a, b) => a.name.localeCompare(b.name))}>
<h4 style="text-align: center;">{item.name}</h4> <h4 style="text-align: center;">{item.name} {metricUnits[item.name]}</h4>
<MetricPlot <MetricPlot
width={width} height={300} metric={item.name} timestep={item.metric.timestep} width={width} height={300} metric={item.name} timestep={item.metric.timestep}
cluster={clusters.find(c => c.name == cluster)} subCluster={$nodesQuery.data.nodeMetrics[0].subCluster} cluster={clusters.find(c => c.name == cluster)} subCluster={$nodesQuery.data.nodeMetrics[0].subCluster}

View File

@ -11,7 +11,7 @@
const allMetrics = [...new Set(jobMetrics.map(m => m.name))].sort(), const allMetrics = [...new Set(jobMetrics.map(m => m.name))].sort(),
scopesForMetric = (metric) => jobMetrics scopesForMetric = (metric) => jobMetrics
.filter(jm => jm.name == metric) .filter(jm => jm.name == metric)
.map(jm => jm.metric.scope) .map(jm => jm.scope)
let hosts = job.resources.map(r => r.hostname).sort(), let hosts = job.resources.map(r => r.hostname).sort(),
selectedScopes = {}, selectedScopes = {},
@ -40,7 +40,7 @@
s.active = true s.active = true
} }
let series = jobMetrics.find(jm => jm.name == metric && jm.metric.scope == 'node')?.metric.series let series = jobMetrics.find(jm => jm.name == metric && jm.scope == 'node')?.metric.series
sorting = {...sorting} sorting = {...sorting}
hosts = hosts.sort((h1, h2) => { hosts = hosts.sort((h1, h2) => {
let s1 = series.find(s => s.hostname == h1)?.statistics let s1 = series.find(s => s.hostname == h1)?.statistics

View File

@ -5,7 +5,7 @@
export let jobMetrics export let jobMetrics
$: series = jobMetrics $: series = jobMetrics
.find(jm => jm.name == metric && jm.metric.scope == scope) .find(jm => jm.name == metric && jm.scope == scope)
?.metric.series.filter(s => s.hostname == host && s.statistics != null) ?.metric.series.filter(s => s.hostname == host && s.statistics != null)
</script> </script>

View File

@ -15,13 +15,14 @@
let from = new Date(Date.now() - 5 * 60 * 1000), to = new Date(Date.now()) let from = new Date(Date.now() - 5 * 60 * 1000), to = new Date(Date.now())
const mainQuery = operationStore(`query($cluster: String!, $filter: [JobFilter!]!, $metrics: [String!], $from: Time!, $to: Time!) { const mainQuery = operationStore(`query($cluster: String!, $filter: [JobFilter!]!, $metrics: [String!], $from: Time!, $to: Time!) {
nodeMetrics(cluster: $cluster, metrics: $metrics, from: $from, to: $to) { nodeMetrics(cluster: $cluster, metrics: $metrics, from: $from, to: $to) {
host, host
subCluster, subCluster
metrics { metrics {
name, name
scope
metric { metric {
scope timestep
timestep, unit { base, prefix }
series { data } series { data }
} }
} }
@ -47,13 +48,15 @@
? sum + (node.metrics.find(m => m.name == metric)?.metric.series.reduce((sum, series) => sum + series.data[series.data.length - 1], 0) || 0) ? sum + (node.metrics.find(m => m.name == metric)?.metric.series.reduce((sum, series) => sum + series.data[series.data.length - 1], 0) || 0)
: sum, 0) : sum, 0)
let allocatedNodes = {}, flopRate = {}, memBwRate = {} let allocatedNodes = {}, flopRate = {}, flopRateUnit = {}, memBwRate = {}, memBwRateUnit = {}
$: if ($initq.data && $mainQuery.data) { $: if ($initq.data && $mainQuery.data) {
let subClusters = $initq.data.clusters.find(c => c.name == cluster).subClusters let subClusters = $initq.data.clusters.find(c => c.name == cluster).subClusters
for (let subCluster of subClusters) { for (let subCluster of subClusters) {
allocatedNodes[subCluster.name] = $mainQuery.data.allocatedNodes.find(({ name }) => name == subCluster.name)?.count || 0 allocatedNodes[subCluster.name] = $mainQuery.data.allocatedNodes.find(({ name }) => name == subCluster.name)?.count || 0
flopRate[subCluster.name] = Math.floor(sumUp($mainQuery.data.nodeMetrics, subCluster.name, 'flops_any') * 100) / 100 flopRate[subCluster.name] = Math.floor(sumUp($mainQuery.data.nodeMetrics, subCluster.name, 'flops_any') * 100) / 100
flopRateUnit[subCluster.name] = subCluster.flopRateSimd.unit.prefix + subCluster.flopRateSimd.unit.base
memBwRate[subCluster.name] = Math.floor(sumUp($mainQuery.data.nodeMetrics, subCluster.name, 'mem_bw') * 100) / 100 memBwRate[subCluster.name] = Math.floor(sumUp($mainQuery.data.nodeMetrics, subCluster.name, 'mem_bw') * 100) / 100
memBwRateUnit[subCluster.name] = subCluster.memoryBandwidth.unit.prefix + subCluster.memoryBandwidth.unit.base
} }
} }
@ -116,13 +119,13 @@
</tr> </tr>
<tr> <tr>
<th scope="col">Flop Rate (Any) <Icon name="info-circle" class="p-1" style="cursor: help;" title="Flops[Any] = (Flops[Double] x 2) + Flops[Single]"/></th> <th scope="col">Flop Rate (Any) <Icon name="info-circle" class="p-1" style="cursor: help;" title="Flops[Any] = (Flops[Double] x 2) + Flops[Single]"/></th>
<td style="min-width: 100px;"><div class="col"><Progress value={flopRate[subCluster.name]} max={subCluster.flopRateSimd * subCluster.numberOfNodes}/></div></td> <td style="min-width: 100px;"><div class="col"><Progress value={flopRate[subCluster.name]} max={subCluster.flopRateSimd.value * subCluster.numberOfNodes}/></div></td>
<td>({formatNumber(flopRate[subCluster.name])}Flops/s / {formatNumber((subCluster.flopRateSimd * subCluster.numberOfNodes))}Flops/s [Max])</td> <td>({flopRate[subCluster.name]} {flopRateUnit[subCluster.name]} / {(subCluster.flopRateSimd.value * subCluster.numberOfNodes)} {flopRateUnit[subCluster.name]} [Max])</td>
</tr> </tr>
<tr> <tr>
<th scope="col">MemBw Rate</th> <th scope="col">MemBw Rate</th>
<td style="min-width: 100px;"><div class="col"><Progress value={memBwRate[subCluster.name]} max={subCluster.memoryBandwidth * subCluster.numberOfNodes}/></div></td> <td style="min-width: 100px;"><div class="col"><Progress value={memBwRate[subCluster.name]} max={subCluster.memoryBandwidth.value * subCluster.numberOfNodes}/></div></td>
<td>({formatNumber(memBwRate[subCluster.name])}Byte/s / {formatNumber((subCluster.memoryBandwidth * subCluster.numberOfNodes))}Byte/s [Max])</td> <td>({memBwRate[subCluster.name]} {memBwRateUnit[subCluster.name]} / {(subCluster.memoryBandwidth.value * subCluster.numberOfNodes)} {memBwRateUnit[subCluster.name]} [Max])</td>
</tr> </tr>
</Table> </Table>
</CardBody> </CardBody>
@ -150,7 +153,7 @@
<h4 class="mb-3 text-center">Top Users</h4> <h4 class="mb-3 text-center">Top Users</h4>
{#key $mainQuery.data} {#key $mainQuery.data}
<Histogram <Histogram
width={colWidth1 - 25} height={300} width={colWidth1 - 25}
data={$mainQuery.data.topUsers.sort((a, b) => b.count - a.count).map(({ count }, idx) => ({ count, value: idx }))} data={$mainQuery.data.topUsers.sort((a, b) => b.count - a.count).map(({ count }, idx) => ({ count, value: idx }))}
label={(x) => x < $mainQuery.data.topUsers.length ? $mainQuery.data.topUsers[Math.floor(x)].name : '0'} label={(x) => x < $mainQuery.data.topUsers.length ? $mainQuery.data.topUsers[Math.floor(x)].name : '0'}
xlabel="User Name" ylabel="Number of Jobs" /> xlabel="User Name" ylabel="Number of Jobs" />
@ -172,7 +175,7 @@
<h4 class="mb-3 text-center">Top Projects</h4> <h4 class="mb-3 text-center">Top Projects</h4>
{#key $mainQuery.data} {#key $mainQuery.data}
<Histogram <Histogram
width={colWidth1 - 25} height={300} width={colWidth1 - 25}
data={$mainQuery.data.topProjects.sort((a, b) => b.count - a.count).map(({ count }, idx) => ({ count, value: idx }))} data={$mainQuery.data.topProjects.sort((a, b) => b.count - a.count).map(({ count }, idx) => ({ count, value: idx }))}
label={(x) => x < $mainQuery.data.topProjects.length ? $mainQuery.data.topProjects[Math.floor(x)].name : '0'} label={(x) => x < $mainQuery.data.topProjects.length ? $mainQuery.data.topProjects[Math.floor(x)].name : '0'}
xlabel="Project Code" ylabel="Number of Jobs" /> xlabel="Project Code" ylabel="Number of Jobs" />
@ -193,9 +196,10 @@
<h4 class="mb-3 text-center">Duration Distribution</h4> <h4 class="mb-3 text-center">Duration Distribution</h4>
{#key $mainQuery.data.stats} {#key $mainQuery.data.stats}
<Histogram <Histogram
width={colWidth2 - 25} height={300} width={colWidth2 - 25}
data={$mainQuery.data.stats[0].histDuration} data={$mainQuery.data.stats[0].histDuration}
xlabel="Current Runtime in Hours [h]" ylabel="Number of Jobs" /> xlabel="Current Runtimes [h]"
ylabel="Number of Jobs" />
{/key} {/key}
</div> </div>
</Col> </Col>
@ -203,9 +207,10 @@
<h4 class="mb-3 text-center">Number of Nodes Distribution</h4> <h4 class="mb-3 text-center">Number of Nodes Distribution</h4>
{#key $mainQuery.data.stats} {#key $mainQuery.data.stats}
<Histogram <Histogram
width={colWidth2 - 25} height={300} width={colWidth2 - 25}
data={$mainQuery.data.stats[0].histNumNodes} data={$mainQuery.data.stats[0].histNumNodes}
xlabel="Allocated Nodes" ylabel="Number of Jobs" /> xlabel="Allocated Nodes [#]"
ylabel="Number of Jobs" />
{/key} {/key}
</Col> </Col>
</Row> </Row>

View File

@ -28,13 +28,14 @@
const nodesQuery = operationStore(`query($cluster: String!, $metrics: [String!], $from: Time!, $to: Time!) { const nodesQuery = operationStore(`query($cluster: String!, $metrics: [String!], $from: Time!, $to: Time!) {
nodeMetrics(cluster: $cluster, metrics: $metrics, from: $from, to: $to) { nodeMetrics(cluster: $cluster, metrics: $metrics, from: $from, to: $to) {
host, host
subCluster subCluster
metrics { metrics {
name, name
scope
metric { metric {
scope timestep
timestep, unit { base, prefix }
series { series {
statistics { min, avg, max } statistics { min, avg, max }
data data
@ -49,6 +50,18 @@
to: to.toISOString() to: to.toISOString()
}) })
let metricUnits = {}
$: if ($nodesQuery.data) {
let thisCluster = clusters.find(c => c.name == cluster)
for (let metric of thisCluster.metricConfig) {
if (metric.unit.prefix || metric.unit.base) {
metricUnits[metric.name] = '(' + (metric.unit.prefix ? metric.unit.prefix : '') + (metric.unit.base ? metric.unit.base : '') + ')'
} else { // If no unit defined: Omit Unit Display
metricUnits[metric.name] = ''
}
}
}
$: $nodesQuery.variables = { cluster, metrics: [selectedMetric], from: from.toISOString(), to: to.toISOString() } $: $nodesQuery.variables = { cluster, metrics: [selectedMetric], from: from.toISOString(), to: to.toISOString() }
query(nodesQuery) query(nodesQuery)
@ -71,7 +84,7 @@
<InputGroupText>Metric</InputGroupText> <InputGroupText>Metric</InputGroupText>
<select class="form-select" bind:value={selectedMetric}> <select class="form-select" bind:value={selectedMetric}>
{#each clusters.find(c => c.name == cluster).metricConfig as metric} {#each clusters.find(c => c.name == cluster).metricConfig as metric}
<option value={metric.name}>{metric.name} ({metric.unit})</option> <option value={metric.name}>{metric.name} {metricUnits[metric.name]}</option>
{/each} {/each}
</select> </select>
</InputGroup> </InputGroup>
@ -98,8 +111,8 @@
let:width let:width
itemsPerRow={ccconfig.plot_view_plotsPerRow} itemsPerRow={ccconfig.plot_view_plotsPerRow}
items={$nodesQuery.data.nodeMetrics items={$nodesQuery.data.nodeMetrics
.filter(h => h.host.includes(hostnameFilter) && h.metrics.some(m => m.name == selectedMetric && m.metric.scope == 'node')) .filter(h => h.host.includes(hostnameFilter) && h.metrics.some(m => m.name == selectedMetric && m.scope == 'node'))
.map(h => ({ host: h.host, subCluster: h.subCluster, data: h.metrics.find(m => m.name == selectedMetric && m.metric.scope == 'node') })) .map(h => ({ host: h.host, subCluster: h.subCluster, data: h.metrics.find(m => m.name == selectedMetric && m.scope == 'node') }))
.sort((a, b) => a.host.localeCompare(b.host))}> .sort((a, b) => a.host.localeCompare(b.host))}>
<h4 style="width: 100%; text-align: center;"><a href="/monitoring/node/{cluster}/{item.host}">{item.host} ({item.subCluster})</a></h4> <h4 style="width: 100%; text-align: center;"><a href="/monitoring/node/{cluster}/{item.host}">{item.host} ({item.subCluster})</a></h4>

View File

@ -136,19 +136,23 @@
</Table> </Table>
</Col> </Col>
<div class="col-4" style="text-align: center;" bind:clientWidth={w1}> <div class="col-4" style="text-align: center;" bind:clientWidth={w1}>
<b>Walltime</b> <b>Duration Distribution</b>
{#key $stats.data.jobsStatistics[0].histDuration} {#key $stats.data.jobsStatistics[0].histDuration}
<Histogram <Histogram
data={$stats.data.jobsStatistics[0].histDuration} data={$stats.data.jobsStatistics[0].histDuration}
width={w1 - 25} height={histogramHeight} /> width={w1 - 25} height={histogramHeight}
xlabel="Current Runtimes [h]"
ylabel="Number of Jobs"/>
{/key} {/key}
</div> </div>
<div class="col-4" style="text-align: center;" bind:clientWidth={w2}> <div class="col-4" style="text-align: center;" bind:clientWidth={w2}>
<b>Number of Nodes</b> <b>Number of Nodes Distribution</b>
{#key $stats.data.jobsStatistics[0].histNumNodes} {#key $stats.data.jobsStatistics[0].histNumNodes}
<Histogram <Histogram
data={$stats.data.jobsStatistics[0].histNumNodes} data={$stats.data.jobsStatistics[0].histNumNodes}
width={w2 - 25} height={histogramHeight} /> width={w2 - 25} height={histogramHeight}
xlabel="Allocated Nodes [#]"
ylabel="Number of Jobs" />
{/key} {/key}
</div> </div>
{/if} {/if}

View File

@ -101,9 +101,11 @@
{#if $initialized} {#if $initialized}
({clusters ({clusters
.map(cluster => cluster.metricConfig.find(m => m.name == metric)) .map(cluster => cluster.metricConfig.find(m => m.name == metric))
.filter(m => m != null).map(m => m.unit) .filter(m => m != null)
.reduce((arr, unit) => arr.includes(unit) ? arr : [...arr, unit], []) .map(m => (m.unit?.prefix?m.unit?.prefix:'') + (m.unit?.base?m.unit?.base:'')) // Build unitStr
.join(', ')}) .reduce((arr, unitStr) => arr.includes(unitStr) ? arr : [...arr, unitStr], []) // w/o this, output would be [unitStr, unitStr]
.join(', ')
})
{/if} {/if}
</th> </th>
{/each} {/each}

View File

@ -24,12 +24,14 @@
let scopes = [job.numNodes == 1 ? 'core' : 'node'] let scopes = [job.numNodes == 1 ? 'core' : 'node']
const cluster = getContext('clusters').find(c => c.name == job.cluster) const cluster = getContext('clusters').find(c => c.name == job.cluster)
// Get all MetricConfs which include subCluster-specific settings for this job
const metricConfig = getContext('metrics')
const metricsQuery = operationStore(`query($id: ID!, $metrics: [String!]!, $scopes: [MetricScope!]!) { const metricsQuery = operationStore(`query($id: ID!, $metrics: [String!]!, $scopes: [MetricScope!]!) {
jobMetrics(id: $id, metrics: $metrics, scopes: $scopes) { jobMetrics(id: $id, metrics: $metrics, scopes: $scopes) {
name name
scope
metric { metric {
unit, scope, timestep unit { prefix, base }, timestep
statisticsSeries { min, mean, max } statisticsSeries { min, mean, max }
series { series {
hostname, id, data hostname, id, data
@ -44,13 +46,64 @@
}) })
const selectScope = (jobMetrics) => jobMetrics.reduce( const selectScope = (jobMetrics) => jobMetrics.reduce(
(a, b) => maxScope([a.metric.scope, b.metric.scope]) == a.metric.scope (a, b) => maxScope([a.scope, b.scope]) == a.scope
? (job.numNodes > 1 ? a : b) ? (job.numNodes > 1 ? a : b)
: (job.numNodes > 1 ? b : a), jobMetrics[0]) : (job.numNodes > 1 ? b : a), jobMetrics[0])
const sortAndSelectScope = (jobMetrics) => metrics const sortAndSelectScope = (jobMetrics) => metrics
.map(name => jobMetrics.filter(jobMetric => jobMetric.name == name)) .map(function(name) {
.map(jobMetrics => jobMetrics.length > 0 ? selectScope(jobMetrics) : null) // Get MetricConf for this selected/requested metric
let thisConfig = metricConfig(cluster, name)
let thisSCIndex = thisConfig.subClusters.findIndex(sc => sc.name == job.subCluster)
// Check if Subcluster has MetricConf: If not found (index == -1), no further remove flag check required
if (thisSCIndex >= 0) {
// SubCluster Config present: Check if remove flag is set
if (thisConfig.subClusters[thisSCIndex].remove == true) {
// Return null data and informational flag
// console.log('Case 1.1 -> Returned')
// console.log({removed: true, data: null})
return {removed: true, data: null}
} else {
// load and return metric, if data available
let thisMetric = jobMetrics.filter(jobMetric => jobMetric.name == name) // Returns Array
if (thisMetric.length > 0) {
// console.log('Case 1.2.1 -> Returned')
// console.log({removed: false, data: thisMetric})
return {removed: false, data: thisMetric}
} else {
// console.log('Case 1.2.2 -> Returned:')
// console.log({removed: false, data: null})
return {removed: false, data: null}
}
}
} else {
// No specific subCluster config: 'remove' flag not set, deemed false -> load and return metric, if data available
let thisMetric = jobMetrics.filter(jobMetric => jobMetric.name == name) // Returns Array
if (thisMetric.length > 0) {
// console.log('Case 2.1 -> Returned')
// console.log({removed: false, data: thisMetric})
return {removed: false, data: thisMetric}
} else {
// console.log('Case 2.2 -> Returned')
// console.log({removed: false, data: null})
return {removed: false, data: null}
}
}
})
.map(function(jobMetrics) {
if (jobMetrics.data != null && jobMetrics.data.length > 0) {
// console.log('Before')
// console.log(jobMetrics.data)
// console.log('After')
// console.log(selectScope(jobMetrics.data))
let res = {removed: jobMetrics.removed, data: selectScope(jobMetrics.data)}
// console.log('Packed')
// console.log(res)
return res
} else {
return jobMetrics
}
})
$: metricsQuery.variables = { id: job.id, metrics, scopes } $: metricsQuery.variables = { id: job.id, metrics, scopes }
@ -81,17 +134,20 @@
{:else} {:else}
{#each sortAndSelectScope($metricsQuery.data.jobMetrics) as metric, i (metric || i)} {#each sortAndSelectScope($metricsQuery.data.jobMetrics) as metric, i (metric || i)}
<td> <td>
{#if metric != null} <!-- Subluster Metricconfig remove keyword for jobtables (joblist main, user joblist, project joblist) to be used here as toplevel case-->
{#if metric.removed == false && metric.data != null}
<MetricPlot <MetricPlot
width={plotWidth} width={plotWidth}
height={plotHeight} height={plotHeight}
timestep={metric.metric.timestep} timestep={metric.data.metric.timestep}
scope={metric.metric.scope} scope={metric.data.scope}
series={metric.metric.series} series={metric.data.metric.series}
statisticsSeries={metric.metric.statisticsSeries} statisticsSeries={metric.data.metric.statisticsSeries}
metric={metric.name} metric={metric.data.name}
cluster={cluster} cluster={cluster}
subCluster={job.subCluster} /> subCluster={job.subCluster} />
{:else if metric.removed == true && metric.data == null}
<Card body color="info">Metric disabled for subcluster '{ job.subCluster }'</Card>
{:else} {:else}
<Card body color="warning">Missing Data</Card> <Card body color="warning">Missing Data</Card>
{/if} {/if}

View File

@ -18,10 +18,10 @@
import { onMount } from 'svelte' import { onMount } from 'svelte'
export let data export let data
export let width export let width = 500
export let height export let height = 300
export let xlabel export let xlabel = ''
export let ylabel export let ylabel = ''
export let min = null export let min = null
export let max = null export let max = null
export let label = formatNumber export let label = formatNumber

View File

@ -18,7 +18,7 @@
let ctx, canvasElement let ctx, canvasElement
const labels = metrics.filter(name => { const labels = metrics.filter(name => {
if (!jobMetrics.find(m => m.name == name && m.metric.scope == "node")) { if (!jobMetrics.find(m => m.name == name && m.scope == "node")) {
console.warn(`PolarPlot: No metric data for '${name}'`) console.warn(`PolarPlot: No metric data for '${name}'`)
return false return false
} }
@ -27,7 +27,7 @@
const getValuesForStat = (getStat) => labels.map(name => { const getValuesForStat = (getStat) => labels.map(name => {
const peak = metricConfig(cluster, name).peak const peak = metricConfig(cluster, name).peak
const metric = jobMetrics.find(m => m.name == name && m.metric.scope == "node") const metric = jobMetrics.find(m => m.name == name && m.scope == "node")
const value = getStat(metric.metric) / peak const value = getStat(metric.metric) / peak
return value <= 1. ? value : 1. return value <= 1. ? value : 1.
}) })

View File

@ -71,7 +71,7 @@
if (width <= 0) if (width <= 0)
return return
const [minX, maxX, minY, maxY] = [0.01, 1000, 1., cluster?.flopRateSimd || defaultMaxY] const [minX, maxX, minY, maxY] = [0.01, 1000, 1., cluster?.flopRateSimd?.value || defaultMaxY]
const w = width - paddingLeft - paddingRight const w = width - paddingLeft - paddingRight
const h = height - paddingTop - paddingBottom const h = height - paddingTop - paddingBottom
@ -185,13 +185,13 @@
ctx.lineWidth = 2 ctx.lineWidth = 2
ctx.beginPath() ctx.beginPath()
if (cluster != null) { if (cluster != null) {
const ycut = 0.01 * cluster.memoryBandwidth const ycut = 0.01 * cluster.memoryBandwidth.value
const scalarKnee = (cluster.flopRateScalar - ycut) / cluster.memoryBandwidth const scalarKnee = (cluster.flopRateScalar.value - ycut) / cluster.memoryBandwidth.value
const simdKnee = (cluster.flopRateSimd - ycut) / cluster.memoryBandwidth const simdKnee = (cluster.flopRateSimd.value - ycut) / cluster.memoryBandwidth.value
const scalarKneeX = getCanvasX(scalarKnee), const scalarKneeX = getCanvasX(scalarKnee),
simdKneeX = getCanvasX(simdKnee), simdKneeX = getCanvasX(simdKnee),
flopRateScalarY = getCanvasY(cluster.flopRateScalar), flopRateScalarY = getCanvasY(cluster.flopRateScalar.value),
flopRateSimdY = getCanvasY(cluster.flopRateSimd) flopRateSimdY = getCanvasY(cluster.flopRateSimd.value)
if (scalarKneeX < width - paddingRight) { if (scalarKneeX < width - paddingRight) {
ctx.moveTo(scalarKneeX, flopRateScalarY) ctx.moveTo(scalarKneeX, flopRateScalarY)
@ -270,8 +270,8 @@
export function transformPerNodeData(nodes) { export function transformPerNodeData(nodes) {
const x = [], y = [], c = [] const x = [], y = [], c = []
for (let node of nodes) { for (let node of nodes) {
let flopsAny = node.metrics.find(m => m.name == 'flops_any' && m.metric.scope == 'node')?.metric let flopsAny = node.metrics.find(m => m.name == 'flops_any' && m.scope == 'node')?.metric
let memBw = node.metrics.find(m => m.name == 'mem_bw' && m.metric.scope == 'node')?.metric let memBw = node.metrics.find(m => m.name == 'mem_bw' && m.scope == 'node')?.metric
if (!flopsAny || !memBw) if (!flopsAny || !memBw)
continue continue
@ -301,8 +301,8 @@
export let memBw = null export let memBw = null
export let cluster = null export let cluster = null
export let maxY = null export let maxY = null
export let width export let width = 500
export let height export let height = 300
export let tiles = null export let tiles = null
export let colorDots = true export let colorDots = true
export let showTime = true export let showTime = true

View File

@ -37,11 +37,11 @@ export function init(extraInitQuery = '') {
clusters { clusters {
name, name,
metricConfig { metricConfig {
name, unit {base, prefix}, peak, name, unit { base, prefix }, peak,
normal, caution, alert, normal, caution, alert,
timestep, scope, timestep, scope,
aggregation, aggregation,
subClusters { name, peak, normal, caution, alert } subClusters { name, peak, normal, caution, alert, remove }
} }
partitions partitions
subClusters { subClusters {
@ -49,9 +49,9 @@ export function init(extraInitQuery = '') {
socketsPerNode socketsPerNode
coresPerSocket coresPerSocket
threadsPerCore threadsPerCore
flopRateScalar flopRateScalar { unit { base, prefix }, value }
flopRateSimd flopRateSimd { unit { base, prefix }, value }
memoryBandwidth memoryBandwidth { unit { base, prefix }, value }
numberOfNodes numberOfNodes
topology { topology {
node, socket, core node, socket, core