mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2024-12-26 13:29:05 +01:00
Merge branch 'rework_jobview_header' into 275_tag_scope_jobview_rework
This commit is contained in:
commit
8feb805167
@ -37,9 +37,9 @@
|
|||||||
import Metric from "./job/Metric.svelte";
|
import Metric from "./job/Metric.svelte";
|
||||||
import TagManagement from "./job/TagManagement.svelte";
|
import TagManagement from "./job/TagManagement.svelte";
|
||||||
import StatsTable from "./job/StatsTable.svelte";
|
import StatsTable from "./job/StatsTable.svelte";
|
||||||
import JobFootprint from "./generic/helper/JobFootprint.svelte";
|
import JobSummary from "./job/JobSummary.svelte";
|
||||||
|
import ConcurrentJobs from "./generic/helper/ConcurrentJobs.svelte";
|
||||||
import PlotTable from "./generic/PlotTable.svelte";
|
import PlotTable from "./generic/PlotTable.svelte";
|
||||||
import Polar from "./generic/plots/Polar.svelte";
|
|
||||||
import Roofline from "./generic/plots/Roofline.svelte";
|
import Roofline from "./generic/plots/Roofline.svelte";
|
||||||
import JobInfo from "./generic/joblist/JobInfo.svelte";
|
import JobInfo from "./generic/joblist/JobInfo.svelte";
|
||||||
import MetricSelection from "./generic/select/MetricSelection.svelte";
|
import MetricSelection from "./generic/select/MetricSelection.svelte";
|
||||||
@ -59,7 +59,9 @@
|
|||||||
selectedScopes = [];
|
selectedScopes = [];
|
||||||
|
|
||||||
let plots = {},
|
let plots = {},
|
||||||
jobTags
|
jobTags,
|
||||||
|
statsTable,
|
||||||
|
roofWidth
|
||||||
|
|
||||||
let missingMetrics = [],
|
let missingMetrics = [],
|
||||||
missingHosts = [],
|
missingHosts = [],
|
||||||
@ -231,74 +233,71 @@
|
|||||||
}));
|
}));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Row>
|
<Row class="mb-0 mb-xxl-2">
|
||||||
<Col>
|
<!-- Column 1: Job Info, Concurrent Jobs, Admin Message if found-->
|
||||||
|
<Col xs={12} md={6} xl={3} class="mb-3 mb-xxl-0">
|
||||||
{#if $initq.error}
|
{#if $initq.error}
|
||||||
<Card body color="danger">{$initq.error.message}</Card>
|
<Card body color="danger">{$initq.error.message}</Card>
|
||||||
{:else if $initq.data}
|
{:else if $initq.data}
|
||||||
|
<Card class="overflow-auto" style="height: 400px;">
|
||||||
|
<TabContent> <!-- on:tab={(e) => (status = e.detail)} -->
|
||||||
|
<TabPane tabId="meta-info" tab="Job Info" active>
|
||||||
|
<CardBody class="pb-2">
|
||||||
<JobInfo job={$initq.data.job} {jobTags} />
|
<JobInfo job={$initq.data.job} {jobTags} />
|
||||||
|
</CardBody>
|
||||||
|
</TabPane>
|
||||||
|
{#if $initq.data.job.concurrentJobs != null && $initq.data.job.concurrentJobs.items.length != 0}
|
||||||
|
<TabPane tabId="shared-jobs">
|
||||||
|
<span slot="tab">
|
||||||
|
{$initq.data.job.concurrentJobs.items.length} Concurrent Jobs
|
||||||
|
</span>
|
||||||
|
<CardBody>
|
||||||
|
<ConcurrentJobs cJobs={$initq.data.job.concurrentJobs} showLinks={(authlevel > roles.manager)}/>
|
||||||
|
</CardBody>
|
||||||
|
</TabPane>
|
||||||
|
{/if}
|
||||||
|
{#if $initq.data?.job?.metaData?.message}
|
||||||
|
<TabPane tabId="admin-msg" tab="Admin Note">
|
||||||
|
<CardBody>
|
||||||
|
<p>This note was added by administrators:</p>
|
||||||
|
<hr/>
|
||||||
|
<p>{@html $initq.data.job.metaData.message}</p>
|
||||||
|
</CardBody>
|
||||||
|
</TabPane>
|
||||||
|
{/if}
|
||||||
|
</TabContent>
|
||||||
|
</Card>
|
||||||
{:else}
|
{:else}
|
||||||
<Spinner secondary />
|
<Spinner secondary />
|
||||||
{/if}
|
{/if}
|
||||||
</Col>
|
</Col>
|
||||||
{#if $initq.data && showFootprint}
|
|
||||||
<Col>
|
<!-- If enabled: Column 2: Job Footprint, Polar Representation, Heuristic Summary -->
|
||||||
<JobFootprint
|
{#if showFootprint}
|
||||||
job={$initq.data.job}
|
<Col xs={12} md={6} xl={4} xxl={3} class="mb-3 mb-xxl-0">
|
||||||
/>
|
{#if $initq.error}
|
||||||
</Col>
|
<Card body color="danger">{$initq.error.message}</Card>
|
||||||
{/if}
|
{:else if $initq?.data && $jobMetrics?.data}
|
||||||
{#if $initq?.data && $jobMetrics?.data?.jobMetrics}
|
<JobSummary job={$initq.data.job} jobMetrics={$jobMetrics.data.jobMetrics}/>
|
||||||
{#if $initq.data.job.concurrentJobs != null && $initq.data.job.concurrentJobs.items.length != 0}
|
|
||||||
{#if authlevel > roles.manager}
|
|
||||||
<Col>
|
|
||||||
<h5>
|
|
||||||
Concurrent Jobs <Icon
|
|
||||||
name="info-circle"
|
|
||||||
style="cursor:help;"
|
|
||||||
title="Shared jobs running on the same node with overlapping runtimes"
|
|
||||||
/>
|
|
||||||
</h5>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="/monitoring/jobs/?{$initq.data.job.concurrentJobs
|
|
||||||
.listQuery}"
|
|
||||||
target="_blank">See All</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
{#each $initq.data.job.concurrentJobs.items as pjob, index}
|
|
||||||
<li>
|
|
||||||
<a href="/monitoring/job/{pjob.id}" target="_blank"
|
|
||||||
>{pjob.jobId}</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
</Col>
|
|
||||||
{:else}
|
{:else}
|
||||||
<Col>
|
<Spinner secondary />
|
||||||
<h5>
|
{/if}
|
||||||
{$initq.data.job.concurrentJobs.items.length} Concurrent Jobs
|
|
||||||
</h5>
|
|
||||||
<p>
|
|
||||||
Number of shared jobs on the same node with overlapping runtimes.
|
|
||||||
</p>
|
|
||||||
</Col>
|
</Col>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
|
||||||
<Col>
|
<!-- Column 3: Job Roofline; If footprint Enabled: full width, else half width -->
|
||||||
<Polar
|
<Col xs={12} md={showFootprint ? 12 : 6} xl={showFootprint ? 5 : 6} xxl={6}>
|
||||||
metrics={ccconfig[
|
{#if $initq.error || $jobMetrics.error}
|
||||||
`job_view_polarPlotMetrics:${$initq.data.job.cluster}`
|
<Card body color="danger">
|
||||||
] || ccconfig[`job_view_polarPlotMetrics`]}
|
<p>Initq Error: {$initq.error?.message}</p>
|
||||||
cluster={$initq.data.job.cluster}
|
<p>jobMetrics Error: {$jobMetrics.error?.message}</p>
|
||||||
subCluster={$initq.data.job.subCluster}
|
</Card>
|
||||||
jobMetrics={$jobMetrics.data.jobMetrics}
|
{:else if $initq?.data && $jobMetrics?.data}
|
||||||
/>
|
<Card style="height: 400px;">
|
||||||
</Col>
|
<div bind:clientWidth={roofWidth}>
|
||||||
<Col>
|
|
||||||
<Roofline
|
<Roofline
|
||||||
|
allowSizeChange={true}
|
||||||
|
width={roofWidth}
|
||||||
renderTime={true}
|
renderTime={true}
|
||||||
subCluster={$initq.data.clusters
|
subCluster={$initq.data.clusters
|
||||||
.find((c) => c.name == $initq.data.job.cluster)
|
.find((c) => c.name == $initq.data.job.cluster)
|
||||||
@ -312,14 +311,17 @@
|
|||||||
)?.metric,
|
)?.metric,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</div>
|
||||||
|
</Card>
|
||||||
{:else}
|
{:else}
|
||||||
<Col />
|
|
||||||
<Spinner secondary />
|
<Spinner secondary />
|
||||||
<Col />
|
|
||||||
{/if}
|
{/if}
|
||||||
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row class="mb-3">
|
|
||||||
|
<hr/>
|
||||||
|
|
||||||
|
<Row class="mb-2">
|
||||||
<Col xs="auto">
|
<Col xs="auto">
|
||||||
{#if $initq.data}
|
{#if $initq.data}
|
||||||
<TagManagement job={$initq.data.job} {username} {authlevel} {roles} bind:jobTags />
|
<TagManagement job={$initq.data.job} {username} {authlevel} {roles} bind:jobTags />
|
||||||
@ -376,9 +378,13 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row class="mt-2">
|
|
||||||
|
<hr/>
|
||||||
|
|
||||||
|
<Row>
|
||||||
<Col>
|
<Col>
|
||||||
{#if $initq.data}
|
{#if $initq.data}
|
||||||
|
<Card>
|
||||||
<TabContent>
|
<TabContent>
|
||||||
{#if somethingMissing}
|
{#if somethingMissing}
|
||||||
<TabPane tabId="resources" tab="Resources" active={somethingMissing}>
|
<TabPane tabId="resources" tab="Resources" active={somethingMissing}>
|
||||||
@ -418,6 +424,7 @@
|
|||||||
{#if $jobMetrics?.data?.jobMetrics}
|
{#if $jobMetrics?.data?.jobMetrics}
|
||||||
{#key $jobMetrics.data.jobMetrics}
|
{#key $jobMetrics.data.jobMetrics}
|
||||||
<StatsTable
|
<StatsTable
|
||||||
|
bind:this={statsTable}
|
||||||
job={$initq.data.job}
|
job={$initq.data.job}
|
||||||
jobMetrics={$jobMetrics.data.jobMetrics}
|
jobMetrics={$jobMetrics.data.jobMetrics}
|
||||||
/>
|
/>
|
||||||
@ -445,6 +452,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
</TabContent>
|
</TabContent>
|
||||||
|
</Card>
|
||||||
{/if}
|
{/if}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
101
web/frontend/src/generic/helper/ConcurrentJobs.svelte
Normal file
101
web/frontend/src/generic/helper/ConcurrentJobs.svelte
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<!--
|
||||||
|
@component Concurrent Jobs Component; Lists all concurrent jobs in one scrollable card.
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
- `cJobs JobLinkResultList`: List of concurrent Jobs
|
||||||
|
- `showLinks Bool?`: Show list as clickable links [Default: false]
|
||||||
|
- `renderCard Bool?`: If to render component as content only or with card wrapping [Default: true]
|
||||||
|
- `width String?`: Width of the card [Default: 'auto']
|
||||||
|
- `height String?`: Height of the card [Default: '310px']
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardBody,
|
||||||
|
Icon
|
||||||
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
|
export let cJobs;
|
||||||
|
export let showLinks = false;
|
||||||
|
export let renderCard = false;
|
||||||
|
export let width = "auto";
|
||||||
|
export let height = "400px";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if renderCard}
|
||||||
|
<Card class="overflow-auto" style="width: {width}; height: {height}">
|
||||||
|
<CardHeader class="mb-0 d-flex justify-content-center">
|
||||||
|
{cJobs.items.length} Concurrent Jobs
|
||||||
|
<Icon
|
||||||
|
style="cursor:help; margin-left:0.5rem;"
|
||||||
|
name="info-circle"
|
||||||
|
title="Jobs running on the same node with overlapping runtimes using shared resources"
|
||||||
|
/>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody>
|
||||||
|
{#if showLinks}
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="/monitoring/jobs/?{cJobs.listQuery}"
|
||||||
|
target="_blank">See All</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
{#each cJobs.items as cJob}
|
||||||
|
<li>
|
||||||
|
<a href="/monitoring/job/{cJob.id}" target="_blank"
|
||||||
|
>{cJob.jobId}</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{:else}
|
||||||
|
<ul>
|
||||||
|
{#each cJobs.items as cJob}
|
||||||
|
<li>
|
||||||
|
{cJob.jobId}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
{:else}
|
||||||
|
<p>
|
||||||
|
{cJobs.items.length} Jobs running on the same node with overlapping runtimes using shared resources.
|
||||||
|
( <a
|
||||||
|
href="/monitoring/jobs/?{cJobs.listQuery}"
|
||||||
|
target="_blank">See All</a
|
||||||
|
> )
|
||||||
|
</p>
|
||||||
|
<hr/>
|
||||||
|
{#if showLinks}
|
||||||
|
<ul>
|
||||||
|
{#each cJobs.items as cJob}
|
||||||
|
<li>
|
||||||
|
<a href="/monitoring/job/{cJob.id}" target="_blank"
|
||||||
|
>{cJob.jobId}</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{:else}
|
||||||
|
<ul>
|
||||||
|
{#each cJobs.items as cJob}
|
||||||
|
<li>
|
||||||
|
{cJob.jobId}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
ul {
|
||||||
|
columns: 3;
|
||||||
|
-webkit-columns: 3;
|
||||||
|
-moz-columns: 3;
|
||||||
|
}
|
||||||
|
</style>
|
@ -117,7 +117,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p class="mb-2">
|
||||||
{#each jobTags as tag}
|
{#each jobTags as tag}
|
||||||
<Tag {tag} />
|
<Tag {tag} />
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -2,10 +2,11 @@
|
|||||||
@component Polar Plot based on chartJS Radar
|
@component Polar Plot based on chartJS Radar
|
||||||
|
|
||||||
Properties:
|
Properties:
|
||||||
- `metrics [String]`: Metric names to display as polar plot
|
- `footprintData [Object]?`: job.footprint content, evaluated in regards to peak config in jobSummary.svelte [Default: null]
|
||||||
- `cluster GraphQL.Cluster`: Cluster Object of the parent job
|
- `metrics [String]?`: Metric names to display as polar plot [Default: null]
|
||||||
- `subCluster GraphQL.SubCluster`: SubCluster Object of the parent job
|
- `cluster GraphQL.Cluster?`: Cluster Object of the parent job [Default: null]
|
||||||
- `jobMetrics [GraphQL.JobMetricWithName]`: Metric data
|
- `subCluster GraphQL.SubCluster?`: SubCluster Object of the parent job [Default: null]
|
||||||
|
- `jobMetrics [GraphQL.JobMetricWithName]?`: Metric data [Default: null]
|
||||||
- `height Number?`: Plot height [Default: 365]
|
- `height Number?`: Plot height [Default: 365]
|
||||||
-->
|
-->
|
||||||
|
|
||||||
@ -33,29 +34,57 @@
|
|||||||
LineElement
|
LineElement
|
||||||
);
|
);
|
||||||
|
|
||||||
export let metrics
|
export let footprintData = null;
|
||||||
export let cluster
|
export let metrics = null;
|
||||||
export let subCluster
|
export let cluster = null;
|
||||||
export let jobMetrics
|
export let subCluster = null;
|
||||||
export let height = 365
|
export let jobMetrics = null;
|
||||||
|
export let height = 350;
|
||||||
|
|
||||||
const getMetricConfig = getContext("getMetricConfig")
|
function getLabels() {
|
||||||
|
if (footprintData) {
|
||||||
const labels = metrics.filter(name => {
|
return footprintData.filter(fpd => {
|
||||||
|
if (!jobMetrics.find(m => m.name == fpd.name && m.scope == "node" || fpd.impact == 4)) {
|
||||||
|
console.warn(`PolarPlot: No metric data (or config) for '${fpd.name}'`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
.map(filtered => filtered.name)
|
||||||
|
.sort(function (a, b) {
|
||||||
|
return ((a > b) ? 1 : ((b > a) ? -1 : 0));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return metrics.filter(name => {
|
||||||
if (!jobMetrics.find(m => m.name == name && m.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
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
.sort(function (a, b) {
|
||||||
|
return ((a > b) ? 1 : ((b > a) ? -1 : 0));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getValuesForStat = (getStat) => labels.map(name => {
|
const labels = getLabels();
|
||||||
|
const getMetricConfig = getContext("getMetricConfig");
|
||||||
|
|
||||||
|
const getValuesForStatGeneric = (getStat) => labels.map(name => {
|
||||||
const peak = getMetricConfig(cluster, subCluster, name).peak
|
const peak = getMetricConfig(cluster, subCluster, name).peak
|
||||||
const metric = jobMetrics.find(m => m.name == name && m.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.
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const getValuesForStatFootprint = (getStat) => labels.map(name => {
|
||||||
|
const peak = footprintData.find(fpd => fpd.name === name).peak
|
||||||
|
const metric = jobMetrics.find(m => m.name == name && m.scope == "node")
|
||||||
|
const value = getStat(metric.metric) / peak
|
||||||
|
return value <= 1. ? value : 1.
|
||||||
|
})
|
||||||
|
|
||||||
function getMax(metric) {
|
function getMax(metric) {
|
||||||
let max = 0
|
let max = 0
|
||||||
for (let series of metric.series)
|
for (let series of metric.series)
|
||||||
@ -70,12 +99,32 @@
|
|||||||
return avg / metric.series.length
|
return avg / metric.series.length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadDataGeneric(type) {
|
||||||
|
if (type === 'avg') {
|
||||||
|
return getValuesForStatGeneric(getAvg)
|
||||||
|
} else if (type === 'max') {
|
||||||
|
return getValuesForStatGeneric(getMax)
|
||||||
|
}
|
||||||
|
console.log('Unknown Type For Polar Data')
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadDataForFootprint(type) {
|
||||||
|
if (type === 'avg') {
|
||||||
|
return getValuesForStatFootprint(getAvg)
|
||||||
|
} else if (type === 'max') {
|
||||||
|
return getValuesForStatFootprint(getMax)
|
||||||
|
}
|
||||||
|
console.log('Unknown Type For Polar Data')
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
labels: labels,
|
labels: labels,
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: 'Max',
|
label: 'Max',
|
||||||
data: getValuesForStat(getMax),
|
data: footprintData ? loadDataForFootprint('max') : loadDataGeneric('max'), //
|
||||||
fill: 1,
|
fill: 1,
|
||||||
backgroundColor: 'rgba(0, 102, 255, 0.25)',
|
backgroundColor: 'rgba(0, 102, 255, 0.25)',
|
||||||
borderColor: 'rgb(0, 102, 255)',
|
borderColor: 'rgb(0, 102, 255)',
|
||||||
@ -86,7 +135,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Avg',
|
label: 'Avg',
|
||||||
data: getValuesForStat(getAvg),
|
data: footprintData ? loadDataForFootprint('avg') : loadDataGeneric('avg'), // getValuesForStat(getAvg)
|
||||||
fill: true,
|
fill: true,
|
||||||
backgroundColor: 'rgba(255, 153, 0, 0.25)',
|
backgroundColor: 'rgba(255, 153, 0, 0.25)',
|
||||||
borderColor: 'rgb(255, 153, 0)',
|
borderColor: 'rgb(255, 153, 0)',
|
||||||
@ -100,7 +149,7 @@
|
|||||||
|
|
||||||
// No custom defined options but keep for clarity
|
// No custom defined options but keep for clarity
|
||||||
const options = {
|
const options = {
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: true,
|
||||||
animation: false,
|
animation: false,
|
||||||
scales: { // fix scale
|
scales: { // fix scale
|
||||||
r: {
|
r: {
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
- `allowSizeChange Bool?`: If dimensions of rendered plot can change [Default: false]
|
- `allowSizeChange Bool?`: If dimensions of rendered plot can change [Default: false]
|
||||||
- `subCluster GraphQL.SubCluster?`: SubCluster Object; contains required topology information [Default: null]
|
- `subCluster GraphQL.SubCluster?`: SubCluster Object; contains required topology information [Default: null]
|
||||||
- `width Number?`: Plot width (reactively adaptive) [Default: 600]
|
- `width Number?`: Plot width (reactively adaptive) [Default: 600]
|
||||||
- `height Number?`: Plot height (reactively adaptive) [Default: 350]
|
- `height Number?`: Plot height (reactively adaptive) [Default: 380]
|
||||||
|
|
||||||
Data Format:
|
Data Format:
|
||||||
- `data = [null, [], []]`
|
- `data = [null, [], []]`
|
||||||
@ -33,7 +33,7 @@
|
|||||||
export let allowSizeChange = false;
|
export let allowSizeChange = false;
|
||||||
export let subCluster = null;
|
export let subCluster = null;
|
||||||
export let width = 600;
|
export let width = 600;
|
||||||
export let height = 350;
|
export let height = 380;
|
||||||
|
|
||||||
let plotWrapper = null;
|
let plotWrapper = null;
|
||||||
let uplot = null;
|
let uplot = null;
|
||||||
@ -41,8 +41,6 @@
|
|||||||
|
|
||||||
const lineWidth = clusterCockpitConfig.plot_general_lineWidth;
|
const lineWidth = clusterCockpitConfig.plot_general_lineWidth;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
function getGradientR(x) {
|
function getGradientR(x) {
|
||||||
if (x < 0.5) return 0;
|
if (x < 0.5) return 0;
|
||||||
@ -317,7 +315,7 @@
|
|||||||
// The Color Scale For Time Information
|
// The Color Scale For Time Information
|
||||||
const posX = u.valToPos(0.1, "x", true)
|
const posX = u.valToPos(0.1, "x", true)
|
||||||
const posXLimit = u.valToPos(100, "x", true)
|
const posXLimit = u.valToPos(100, "x", true)
|
||||||
const posY = u.valToPos(15000.0, "y", true)
|
const posY = u.valToPos(14000.0, "y", true)
|
||||||
u.ctx.fillStyle = 'black'
|
u.ctx.fillStyle = 'black'
|
||||||
u.ctx.fillText('Start', posX, posY)
|
u.ctx.fillText('Start', posX, posY)
|
||||||
const start = posX + 10
|
const start = posX + 10
|
||||||
@ -364,7 +362,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if data != null}
|
{#if data != null}
|
||||||
<div bind:this={plotWrapper} />
|
<div bind:this={plotWrapper} class="p-2"/>
|
||||||
{:else}
|
{:else}
|
||||||
<Card class="mx-4" body color="warning">Cannot render roofline: No data!</Card
|
<Card class="mx-4" body color="warning">Cannot render roofline: No data!</Card
|
||||||
>
|
>
|
||||||
|
340
web/frontend/src/job/JobSummary.svelte
Normal file
340
web/frontend/src/job/JobSummary.svelte
Normal file
@ -0,0 +1,340 @@
|
|||||||
|
<!--
|
||||||
|
@component Job Summary component; Displays job.footprint data as bars in relation to thresholds, as polar plot, and summariziong comment
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
- `job Object`: The GQL job object
|
||||||
|
- `displayTitle Bool?`: If to display cardHeader with title [Default: true]
|
||||||
|
- `width String?`: Width of the card [Default: 'auto']
|
||||||
|
- `height String?`: Height of the card [Default: '310px']
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script context="module">
|
||||||
|
function findJobThresholds(job, metricConfig) {
|
||||||
|
if (!job || !metricConfig) {
|
||||||
|
console.warn("Argument missing for findJobThresholds!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// metricConfig is on subCluster-Level
|
||||||
|
const defaultThresholds = {
|
||||||
|
peak: metricConfig.peak,
|
||||||
|
normal: metricConfig.normal,
|
||||||
|
caution: metricConfig.caution,
|
||||||
|
alert: metricConfig.alert
|
||||||
|
};
|
||||||
|
|
||||||
|
// Job_Exclusivity does not matter, only aggregation
|
||||||
|
if (metricConfig.aggregation === "avg") {
|
||||||
|
return defaultThresholds;
|
||||||
|
} else if (metricConfig.aggregation === "sum") {
|
||||||
|
const topol = getContext("getHardwareTopology")(job.cluster, job.subCluster)
|
||||||
|
const jobFraction = job.numHWThreads / topol.node.length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
peak: round(defaultThresholds.peak * jobFraction, 0),
|
||||||
|
normal: round(defaultThresholds.normal * jobFraction, 0),
|
||||||
|
caution: round(defaultThresholds.caution * jobFraction, 0),
|
||||||
|
alert: round(defaultThresholds.alert * jobFraction, 0),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
"Missing or unkown aggregation mode (sum/avg) for metric:",
|
||||||
|
metricConfig,
|
||||||
|
);
|
||||||
|
return defaultThresholds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
Progress,
|
||||||
|
Icon,
|
||||||
|
Tooltip,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
TabContent,
|
||||||
|
TabPane
|
||||||
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
import Polar from "../generic/plots/Polar.svelte";
|
||||||
|
import { round } from "mathjs";
|
||||||
|
|
||||||
|
export let job;
|
||||||
|
export let jobMetrics;
|
||||||
|
export let width = "auto";
|
||||||
|
export let height = "400px";
|
||||||
|
|
||||||
|
const ccconfig = getContext("cc-config")
|
||||||
|
|
||||||
|
const footprintData = job?.footprint?.map((jf) => {
|
||||||
|
const fmc = getContext("getMetricConfig")(job.cluster, job.subCluster, jf.name);
|
||||||
|
if (fmc) {
|
||||||
|
// Unit
|
||||||
|
const unit = (fmc?.unit?.prefix ? fmc.unit.prefix : "") + (fmc?.unit?.base ? fmc.unit.base : "")
|
||||||
|
|
||||||
|
// Threshold / -Differences
|
||||||
|
const fmt = findJobThresholds(job, fmc);
|
||||||
|
if (jf.name === "flops_any") fmt.peak = round(fmt.peak * 0.85, 0);
|
||||||
|
|
||||||
|
// Define basic data -> Value: Use as Provided
|
||||||
|
const fmBase = {
|
||||||
|
name: jf.name,
|
||||||
|
stat: jf.stat,
|
||||||
|
value: jf.value,
|
||||||
|
unit: unit,
|
||||||
|
peak: fmt.peak,
|
||||||
|
dir: fmc.lowerIsBetter
|
||||||
|
};
|
||||||
|
|
||||||
|
if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "alert")) {
|
||||||
|
return {
|
||||||
|
...fmBase,
|
||||||
|
color: "danger",
|
||||||
|
message: `Metric average way ${fmc.lowerIsBetter ? "above" : "below"} expected normal thresholds.`,
|
||||||
|
impact: 3
|
||||||
|
};
|
||||||
|
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "caution")) {
|
||||||
|
return {
|
||||||
|
...fmBase,
|
||||||
|
color: "warning",
|
||||||
|
message: `Metric average ${fmc.lowerIsBetter ? "above" : "below"} expected normal thresholds.`,
|
||||||
|
impact: 2,
|
||||||
|
};
|
||||||
|
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "normal")) {
|
||||||
|
return {
|
||||||
|
...fmBase,
|
||||||
|
color: "success",
|
||||||
|
message: "Metric average within expected thresholds.",
|
||||||
|
impact: 1,
|
||||||
|
};
|
||||||
|
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "peak")) {
|
||||||
|
return {
|
||||||
|
...fmBase,
|
||||||
|
color: "info",
|
||||||
|
message:
|
||||||
|
"Metric average above expected normal thresholds: Check for artifacts recommended.",
|
||||||
|
impact: 0,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
...fmBase,
|
||||||
|
color: "secondary",
|
||||||
|
message:
|
||||||
|
"Metric average above expected peak threshold: Check for artifacts!",
|
||||||
|
impact: -1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else { // No matching metric config: display as single value
|
||||||
|
return {
|
||||||
|
name: jf.name,
|
||||||
|
stat: jf.stat,
|
||||||
|
value: jf.value,
|
||||||
|
message:
|
||||||
|
`No config for metric ${jf.name} found.`,
|
||||||
|
impact: 4,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}).sort(function (a, b) { // Sort by impact value primarily, within impact sort name alphabetically
|
||||||
|
return a.impact - b.impact || ((a.name > b.name) ? 1 : ((b.name > a.name) ? -1 : 0));
|
||||||
|
});;
|
||||||
|
|
||||||
|
function evalFootprint(mean, thresholds, lowerIsBetter, level) {
|
||||||
|
// Handle Metrics in which less value is better
|
||||||
|
switch (level) {
|
||||||
|
case "peak":
|
||||||
|
if (lowerIsBetter)
|
||||||
|
return false; // metric over peak -> return false to trigger impact -1
|
||||||
|
else return mean <= thresholds.peak && mean > thresholds.normal;
|
||||||
|
case "alert":
|
||||||
|
if (lowerIsBetter)
|
||||||
|
return mean <= thresholds.peak && mean >= thresholds.alert;
|
||||||
|
else return mean <= thresholds.alert && mean >= 0;
|
||||||
|
case "caution":
|
||||||
|
if (lowerIsBetter)
|
||||||
|
return mean < thresholds.alert && mean >= thresholds.caution;
|
||||||
|
else return mean <= thresholds.caution && mean > thresholds.alert;
|
||||||
|
case "normal":
|
||||||
|
if (lowerIsBetter)
|
||||||
|
return mean < thresholds.caution && mean >= 0;
|
||||||
|
else return mean <= thresholds.normal && mean > thresholds.caution;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeSummary(fpd) {
|
||||||
|
// Hardcoded! Needs to be retrieved from globalMetrics
|
||||||
|
const performanceMetrics = ['flops_any', 'mem_bw'];
|
||||||
|
const utilizationMetrics = ['cpu_load', 'acc_utilization'];
|
||||||
|
const energyMetrics = ['cpu_power'];
|
||||||
|
|
||||||
|
let performanceScore = 0;
|
||||||
|
let utilizationScore = 0;
|
||||||
|
let energyScore = 0;
|
||||||
|
|
||||||
|
let performanceMetricsCounted = 0;
|
||||||
|
let utilizationMetricsCounted = 0;
|
||||||
|
let energyMetricsCounted = 0;
|
||||||
|
|
||||||
|
fpd.forEach(metric => {
|
||||||
|
console.log('Metric, Impact', metric.name, metric.impact)
|
||||||
|
if (performanceMetrics.includes(metric.name)) {
|
||||||
|
performanceScore += metric.impact
|
||||||
|
performanceMetricsCounted += 1
|
||||||
|
} else if (utilizationMetrics.includes(metric.name)) {
|
||||||
|
utilizationScore += metric.impact
|
||||||
|
utilizationMetricsCounted += 1
|
||||||
|
} else if (energyMetrics.includes(metric.name)) {
|
||||||
|
energyScore += metric.impact
|
||||||
|
energyMetricsCounted += 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
performanceScore = (performanceMetricsCounted == 0) ? performanceScore : (performanceScore / performanceMetricsCounted);
|
||||||
|
utilizationScore = (utilizationMetricsCounted == 0) ? utilizationScore : (utilizationScore / utilizationMetricsCounted);
|
||||||
|
energyScore = (energyMetricsCounted == 0) ? energyScore : (energyScore / energyMetricsCounted);
|
||||||
|
|
||||||
|
let res = [];
|
||||||
|
|
||||||
|
console.log('Perf', performanceScore, performanceMetricsCounted)
|
||||||
|
console.log('Util', utilizationScore, utilizationMetricsCounted)
|
||||||
|
console.log('Energy', energyScore, energyMetricsCounted)
|
||||||
|
|
||||||
|
if (performanceScore == 1) {
|
||||||
|
res.push('<b>Performance:</b> Your job performs well.')
|
||||||
|
} else if (performanceScore != 0) {
|
||||||
|
res.push('<b>Performance:</b> Your job performs suboptimal.')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (utilizationScore == 1) {
|
||||||
|
res.push('<b>Utilization:</b> Your job utilizes resources well.')
|
||||||
|
} else if (utilizationScore != 0) {
|
||||||
|
res.push('<b>Utilization:</b> Your job utilizes resources suboptimal.')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (energyScore == 1) {
|
||||||
|
res.push('<b>Energy:</b> Your job has good energy values.')
|
||||||
|
} else if (energyScore != 0) {
|
||||||
|
res.push('<b>Energy:</b> Your job consumes more energy than necessary.')
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
$: summaryMessages = writeSummary(footprintData)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Card class="overflow-auto" style="width: {width}; height: {height}">
|
||||||
|
<TabContent> <!-- on:tab={(e) => (status = e.detail)} -->
|
||||||
|
<TabPane tabId="foot" tab="Footprint" active>
|
||||||
|
<CardBody>
|
||||||
|
{#each footprintData as fpd, index}
|
||||||
|
{#if fpd.impact !== 4}
|
||||||
|
<div class="mb-1 d-flex justify-content-between">
|
||||||
|
<div> <b>{fpd.name} ({fpd.stat})</b></div>
|
||||||
|
<div
|
||||||
|
class="cursor-help d-inline-flex"
|
||||||
|
id={`footprint-${job.jobId}-${index}`}
|
||||||
|
>
|
||||||
|
<div class="mx-1">
|
||||||
|
{#if fpd.impact === 3 || fpd.impact === -1}
|
||||||
|
<Icon name="exclamation-triangle-fill" class="text-danger" />
|
||||||
|
{:else if fpd.impact === 2}
|
||||||
|
<Icon name="exclamation-triangle" class="text-warning" />
|
||||||
|
{/if}
|
||||||
|
{#if fpd.impact === 3}
|
||||||
|
<Icon name="emoji-frown" class="text-danger" />
|
||||||
|
{:else if fpd.impact === 2}
|
||||||
|
<Icon name="emoji-neutral" class="text-warning" />
|
||||||
|
{:else if fpd.impact === 1}
|
||||||
|
<Icon name="emoji-smile" class="text-success" />
|
||||||
|
{:else if fpd.impact === 0}
|
||||||
|
<Icon name="emoji-laughing" class="text-info" />
|
||||||
|
{:else if fpd.impact === -1}
|
||||||
|
<Icon name="emoji-dizzy" class="text-danger" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{fpd.value} / {fpd.peak}
|
||||||
|
{fpd.unit}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Tooltip
|
||||||
|
target={`footprint-${job.jobId}-${index}`}
|
||||||
|
placement="right"
|
||||||
|
offset={[0, 20]}>{fpd.message}</Tooltip
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<Row cols={12} class="{(footprintData.length == (index + 1)) ? 'mb-0' : 'mb-2'}">
|
||||||
|
{#if fpd.dir}
|
||||||
|
<Col xs="1">
|
||||||
|
<Icon name="caret-left-fill" />
|
||||||
|
</Col>
|
||||||
|
{/if}
|
||||||
|
<Col xs="11" class="align-content-center">
|
||||||
|
<Progress value={fpd.value} max={fpd.peak} color={fpd.color} />
|
||||||
|
</Col>
|
||||||
|
{#if !fpd.dir}
|
||||||
|
<Col xs="1">
|
||||||
|
<Icon name="caret-right-fill" />
|
||||||
|
</Col>
|
||||||
|
{/if}
|
||||||
|
</Row>
|
||||||
|
{:else}
|
||||||
|
<div class="mb-1 d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<b>{fpd.name} ({fpd.stat})</b>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="cursor-help d-inline-flex"
|
||||||
|
id={`footprint-${job.jobId}-${index}`}
|
||||||
|
>
|
||||||
|
<div class="mx-1">
|
||||||
|
<Icon name="info-circle"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{fpd.value}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Tooltip
|
||||||
|
target={`footprint-${job.jobId}-${index}`}
|
||||||
|
placement="right"
|
||||||
|
offset={[0, 20]}>{fpd.message}</Tooltip
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</CardBody>
|
||||||
|
</TabPane>
|
||||||
|
<TabPane tabId="polar" tab="Polar">
|
||||||
|
<CardBody>
|
||||||
|
<Polar
|
||||||
|
{footprintData}
|
||||||
|
{jobMetrics}
|
||||||
|
/>
|
||||||
|
</CardBody>
|
||||||
|
</TabPane>
|
||||||
|
<TabPane tabId="summary" tab="Summary">
|
||||||
|
<CardBody>
|
||||||
|
<p>Based on footprint data, this job performs as follows:</p>
|
||||||
|
<hr/>
|
||||||
|
<ul>
|
||||||
|
{#each summaryMessages as sm}
|
||||||
|
<li>
|
||||||
|
{@html sm}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</CardBody>
|
||||||
|
</TabPane>
|
||||||
|
</TabContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.cursor-help {
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
</style>
|
@ -84,7 +84,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Table>
|
<Table class="mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
@ -146,8 +146,6 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
<br />
|
|
||||||
|
|
||||||
<MetricSelection
|
<MetricSelection
|
||||||
cluster={job.cluster}
|
cluster={job.cluster}
|
||||||
configName="job_view_nodestats_selectedMetrics"
|
configName="job_view_nodestats_selectedMetrics"
|
||||||
|
Loading…
Reference in New Issue
Block a user