mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-07-22 20:41:40 +02:00
fix: decouple polarPlot data query, add new dedicated gql endpoint
- includes go package upgrades - includes gqlgen error workaround
This commit is contained in:
@@ -31,17 +31,16 @@
|
||||
init,
|
||||
groupByScope,
|
||||
checkMetricDisabled,
|
||||
transformDataForRoofline,
|
||||
} from "./generic/utils.js";
|
||||
import Metric from "./job/Metric.svelte";
|
||||
import StatsTable from "./job/StatsTable.svelte";
|
||||
import JobSummary from "./job/JobSummary.svelte";
|
||||
import EnergySummary from "./job/EnergySummary.svelte";
|
||||
import ConcurrentJobs from "./generic/helper/ConcurrentJobs.svelte";
|
||||
import PlotGrid from "./generic/PlotGrid.svelte";
|
||||
import Roofline from "./generic/plots/Roofline.svelte";
|
||||
import JobInfo from "./generic/joblist/JobInfo.svelte";
|
||||
import MetricSelection from "./generic/select/MetricSelection.svelte";
|
||||
import JobInfo from "./generic/joblist/JobInfo.svelte";
|
||||
import ConcurrentJobs from "./generic/helper/ConcurrentJobs.svelte";
|
||||
import JobSummary from "./job/JobSummary.svelte";
|
||||
import JobRoofline from "./job/JobRoofline.svelte";
|
||||
import EnergySummary from "./job/EnergySummary.svelte";
|
||||
import PlotGrid from "./generic/PlotGrid.svelte";
|
||||
import StatsTable from "./job/StatsTable.svelte";
|
||||
|
||||
export let dbid;
|
||||
export let username;
|
||||
@@ -57,7 +56,6 @@
|
||||
selectedScopes = [];
|
||||
|
||||
let plots = {},
|
||||
roofWidth,
|
||||
statsTable
|
||||
|
||||
let missingMetrics = [],
|
||||
@@ -117,33 +115,12 @@
|
||||
}
|
||||
`;
|
||||
|
||||
const roofQuery = gql`
|
||||
query ($dbid: ID!, $selectedMetrics: [String!]!, $selectedScopes: [MetricScope!]!, $selectedResolution: Int) {
|
||||
jobMetrics(id: $dbid, metrics: $selectedMetrics, scopes: $selectedScopes, resolution: $selectedResolution) {
|
||||
name
|
||||
scope
|
||||
metric {
|
||||
series {
|
||||
data
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
$: jobMetrics = queryStore({
|
||||
client: client,
|
||||
query: query,
|
||||
variables: { dbid, selectedMetrics, selectedScopes },
|
||||
});
|
||||
|
||||
// Roofline: Always load roofMetrics with configured timestep (Resolution: 0)
|
||||
$: roofMetrics = queryStore({
|
||||
client: client,
|
||||
query: roofQuery,
|
||||
variables: { dbid, selectedMetrics: ["flops_any", "mem_bw"], selectedScopes: ["node"], selectedResolution: 0 },
|
||||
});
|
||||
|
||||
// Handle Job Query on Init -> is not executed anymore
|
||||
getContext("on-init")(() => {
|
||||
let job = $initq.data.job;
|
||||
@@ -235,7 +212,7 @@ const roofQuery = gql`
|
||||
</script>
|
||||
|
||||
<Row class="mb-3">
|
||||
<!-- Column 1: Job Info, Job Tags, Concurrent Jobs, Admin Message if found-->
|
||||
<!-- Row 1, Column 1: Job Info, Job Tags, Concurrent Jobs, Admin Message if found-->
|
||||
<Col xs={12} md={6} xl={3} class="mb-3 mb-xxl-0">
|
||||
{#if $initq.error}
|
||||
<Card body color="danger">{$initq.error.message}</Card>
|
||||
@@ -277,51 +254,30 @@ const roofQuery = gql`
|
||||
{/if}
|
||||
</Col>
|
||||
|
||||
<!-- Column 2: Job Footprint, Polar Representation, Heuristic Summary -->
|
||||
<!-- Row 1, Column 2: Job Footprint, Polar Representation -->
|
||||
<Col xs={12} md={6} xl={4} xxl={3} class="mb-3 mb-xxl-0">
|
||||
{#if $initq.error}
|
||||
<Card body color="danger">{$initq.error.message}</Card>
|
||||
{:else if $initq?.data && $jobMetrics?.data}
|
||||
<JobSummary job={$initq.data.job} jobMetrics={$jobMetrics.data.jobMetrics}/>
|
||||
{:else if $initq?.data}
|
||||
<JobSummary job={$initq.data.job}/>
|
||||
{:else}
|
||||
<Spinner secondary />
|
||||
{/if}
|
||||
</Col>
|
||||
|
||||
<!-- Column 3: Job Roofline; If footprint Enabled: full width, else half width -->
|
||||
<!-- Row 1, Column 3: Job Roofline; If footprint Enabled: full width, else half width -->
|
||||
<Col xs={12} md={12} xl={5} xxl={6}>
|
||||
{#if $initq.error || $roofMetrics.error}
|
||||
<Card body color="danger">
|
||||
<p>Initq Error: {$initq.error?.message}</p>
|
||||
<p>roofMetrics (jobMetrics) Error: {$roofMetrics.error?.message}</p>
|
||||
</Card>
|
||||
{:else if $initq?.data && $roofMetrics?.data}
|
||||
<Card style="height: 400px;">
|
||||
<div bind:clientWidth={roofWidth}>
|
||||
<Roofline
|
||||
allowSizeChange={true}
|
||||
width={roofWidth}
|
||||
renderTime={true}
|
||||
subCluster={$initq.data.clusters
|
||||
.find((c) => c.name == $initq.data.job.cluster)
|
||||
.subClusters.find((sc) => sc.name == $initq.data.job.subCluster)}
|
||||
data={transformDataForRoofline(
|
||||
$roofMetrics.data?.jobMetrics?.find(
|
||||
(m) => m.name == "flops_any" && m.scope == "node",
|
||||
)?.metric,
|
||||
$roofMetrics.data?.jobMetrics?.find(
|
||||
(m) => m.name == "mem_bw" && m.scope == "node",
|
||||
)?.metric,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
{#if $initq.error}
|
||||
<Card body color="danger">{$initq.error.message}</Card>
|
||||
{:else if $initq?.data}
|
||||
<JobRoofline job={$initq.data.job} clusters={$initq.data.clusters}/>
|
||||
{:else}
|
||||
<Spinner secondary />
|
||||
<Spinner secondary />
|
||||
{/if}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<!-- Row 2: Energy Information if available -->
|
||||
{#if $initq?.data && $initq.data.job.energyFootprint.length != 0}
|
||||
<Row class="mb-3">
|
||||
<Col>
|
||||
@@ -330,6 +286,7 @@ const roofQuery = gql`
|
||||
</Row>
|
||||
{/if}
|
||||
|
||||
<!-- Metric Plot Grid -->
|
||||
<Card class="mb-3">
|
||||
<CardBody>
|
||||
<Row class="mb-2">
|
||||
@@ -390,6 +347,7 @@ const roofQuery = gql`
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<!-- Statistcics Table -->
|
||||
<Row class="mb-3">
|
||||
<Col>
|
||||
{#if $initq.data}
|
||||
|
@@ -3,7 +3,7 @@
|
||||
|
||||
Properties:
|
||||
- `polarMetrics [Object]?`: Metric names and scaled peak values for rendering polar plot [Default: [] ]
|
||||
- `jobMetrics [GraphQL.JobMetricWithName]?`: Metric data [Default: null]
|
||||
- `polarData [GraphQL.JobMetricStatWithName]?`: Metric data [Default: null]
|
||||
- `height Number?`: Plot height [Default: 365]
|
||||
-->
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
);
|
||||
|
||||
export let polarMetrics = [];
|
||||
export let jobMetrics = null;
|
||||
export let polarData = [];
|
||||
export let height = 350;
|
||||
|
||||
const labels = polarMetrics
|
||||
@@ -40,53 +40,26 @@
|
||||
.sort(function (a, b) {return ((a > b) ? 1 : ((b > a) ? -1 : 0))});
|
||||
|
||||
function loadData(type) {
|
||||
if (!labels) {
|
||||
if (labels && (type == 'avg' || type == 'min' ||type == 'max')) {
|
||||
return getValues(type)
|
||||
} else if (!labels) {
|
||||
console.warn("Empty 'polarMetrics' array prop! Cannot render Polar representation.")
|
||||
return []
|
||||
} else {
|
||||
if (type === 'avg') {
|
||||
return getValues(getAvg)
|
||||
} else if (type === 'max') {
|
||||
return getValues(getMax)
|
||||
} else if (type === 'min') {
|
||||
return getValues(getMin)
|
||||
}
|
||||
console.log('Unknown Type For Polar Data (must be one of [min, max, avg])')
|
||||
return []
|
||||
console.warn('Unknown Type For Polar Data (must be one of [min, max, avg])')
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
// Helpers
|
||||
// Helper
|
||||
|
||||
const getValues = (getStat) => labels.map(name => {
|
||||
const getValues = (type) => labels.map(name => {
|
||||
// Peak is adapted and scaled for job shared state
|
||||
const peak = polarMetrics.find(m => m.name == name).peak
|
||||
const metric = jobMetrics.find(m => m.name == name && m.scope == "node")
|
||||
const value = getStat(metric.metric) / peak
|
||||
const peak = polarMetrics.find(m => m?.name == name)?.peak
|
||||
const metric = polarData.find(m => m?.name == name)?.stats
|
||||
const value = (peak && metric) ? (metric[type] / peak) : 0
|
||||
return value <= 1. ? value : 1.
|
||||
})
|
||||
|
||||
function getMax(metric) {
|
||||
let max = metric.series[0].statistics.max;
|
||||
for (let series of metric.series)
|
||||
max = Math.max(max, series.statistics.max)
|
||||
return max
|
||||
}
|
||||
|
||||
function getMin(metric) {
|
||||
let min = metric.series[0].statistics.min;
|
||||
for (let series of metric.series)
|
||||
min = Math.min(min, series.statistics.min)
|
||||
return min
|
||||
}
|
||||
|
||||
function getAvg(metric) {
|
||||
let avg = 0;
|
||||
for (let series of metric.series)
|
||||
avg += series.statistics.avg
|
||||
return avg / metric.series.length
|
||||
}
|
||||
|
||||
// Chart JS Objects
|
||||
|
||||
const data = {
|
||||
|
79
web/frontend/src/job/JobRoofline.svelte
Normal file
79
web/frontend/src/job/JobRoofline.svelte
Normal file
@@ -0,0 +1,79 @@
|
||||
<!--
|
||||
@component Job View Roofline component; Queries data for and renders roofline plot.
|
||||
|
||||
Properties:
|
||||
- `job Object`: The GQL job object
|
||||
- `clusters Array`: The GQL clusters array
|
||||
-->
|
||||
|
||||
<script>
|
||||
import {
|
||||
queryStore,
|
||||
gql,
|
||||
getContextClient
|
||||
} from "@urql/svelte";
|
||||
import {
|
||||
Card,
|
||||
Spinner
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import {
|
||||
transformDataForRoofline,
|
||||
} from "../generic/utils.js";
|
||||
import Roofline from "../generic/plots/Roofline.svelte";
|
||||
|
||||
export let job;
|
||||
export let clusters;
|
||||
|
||||
let roofWidth;
|
||||
|
||||
const client = getContextClient();
|
||||
const roofQuery = gql`
|
||||
query ($dbid: ID!, $selectedMetrics: [String!]!, $selectedScopes: [MetricScope!]!, $selectedResolution: Int) {
|
||||
jobMetrics(id: $dbid, metrics: $selectedMetrics, scopes: $selectedScopes, resolution: $selectedResolution) {
|
||||
name
|
||||
scope
|
||||
metric {
|
||||
series {
|
||||
data
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Roofline: Always load roofMetrics with configured timestep (Resolution: 0)
|
||||
$: roofMetrics = queryStore({
|
||||
client: client,
|
||||
query: roofQuery,
|
||||
variables: { dbid: job.id, selectedMetrics: ["flops_any", "mem_bw"], selectedScopes: ["node"], selectedResolution: 0 },
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
{#if $roofMetrics.error}
|
||||
<Card body color="danger">{$roofMetrics.error.message}</Card>
|
||||
{:else if $roofMetrics?.data}
|
||||
<Card style="height: 400px;">
|
||||
<div bind:clientWidth={roofWidth}>
|
||||
<Roofline
|
||||
width={roofWidth}
|
||||
subCluster={clusters
|
||||
.find((c) => c.name == job.cluster)
|
||||
.subClusters.find((sc) => sc.name == job.subCluster)}
|
||||
data={transformDataForRoofline(
|
||||
$roofMetrics.data?.jobMetrics?.find(
|
||||
(m) => m.name == "flops_any" && m.scope == "node",
|
||||
)?.metric,
|
||||
$roofMetrics.data?.jobMetrics?.find(
|
||||
(m) => m.name == "mem_bw" && m.scope == "node",
|
||||
)?.metric,
|
||||
)}
|
||||
allowSizeChange
|
||||
renderTime
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
{:else}
|
||||
<Spinner secondary />
|
||||
{/if}
|
||||
|
@@ -1,9 +1,8 @@
|
||||
<!--
|
||||
@component Job Summary component; Displays job.footprint data as bars in relation to thresholds, as polar plot, and summariziong comment
|
||||
@component Job Summary component; Displays aggregated job footprint statistics and performance indicators
|
||||
|
||||
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']
|
||||
-->
|
||||
@@ -12,317 +11,31 @@
|
||||
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 { findJobFootprintThresholds } from "../generic/utils.js";
|
||||
import JobFootprintBars from "./jobsummary/JobFootprintBars.svelte";
|
||||
import JobFootprintPolar from "./jobsummary/JobFootprintPolar.svelte";
|
||||
|
||||
|
||||
export let job;
|
||||
export let jobMetrics;
|
||||
export let width = "auto";
|
||||
export let height = "400px";
|
||||
|
||||
const ccconfig = getContext("cc-config")
|
||||
const globalMetrics = getContext("globalMetrics")
|
||||
const showFootprintTab = !!ccconfig[`job_view_showFootprint`];
|
||||
|
||||
// Metrics Configured To Be Footprints For (sub)Cluster
|
||||
const clusterFootprintMetrics = getContext("clusters")
|
||||
.find((c) => c.name == job.cluster)?.subClusters
|
||||
.find((sc) => sc.name == job.subCluster)?.footprint || []
|
||||
|
||||
// Data For Polarplot Will Be Calculated Based On JobMetrics And Thresholds
|
||||
const polarMetrics = globalMetrics.reduce((pms, gm) => {
|
||||
if (clusterFootprintMetrics.includes(gm.name)) {
|
||||
const fmt = findJobFootprintThresholds(job, gm.footprint, getContext("getMetricConfig")(job.cluster, job.subCluster, gm.name));
|
||||
pms.push({ name: gm.name, peak: fmt ? fmt.peak : null });
|
||||
}
|
||||
return pms;
|
||||
}, [])
|
||||
|
||||
// Prepare Job Footprint Data Based On Values Saved In Database
|
||||
const jobFootprintData = !showFootprintTab ? null : 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 = findJobFootprintThresholds(job, jf.stat, fmc);
|
||||
|
||||
// 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: `Footprint value way ${fmc.lowerIsBetter ? "above" : "below"} expected normal threshold.`,
|
||||
impact: 3
|
||||
};
|
||||
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "caution")) {
|
||||
return {
|
||||
...fmBase,
|
||||
color: "warning",
|
||||
message: `Footprint value ${fmc.lowerIsBetter ? "above" : "below"} expected normal threshold.`,
|
||||
impact: 2,
|
||||
};
|
||||
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "normal")) {
|
||||
return {
|
||||
...fmBase,
|
||||
color: "success",
|
||||
message: "Footprint value within expected thresholds.",
|
||||
impact: 1,
|
||||
};
|
||||
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "peak")) {
|
||||
return {
|
||||
...fmBase,
|
||||
color: "info",
|
||||
message:
|
||||
"Footprint value above expected normal threshold: Check for artifacts recommended.",
|
||||
impact: 0,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...fmBase,
|
||||
color: "secondary",
|
||||
message:
|
||||
"Footprint value 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(value, 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 value <= thresholds.peak && value > thresholds.normal;
|
||||
case "alert":
|
||||
if (lowerIsBetter)
|
||||
return value <= thresholds.peak && value >= thresholds.alert;
|
||||
else return value <= thresholds.alert && value >= 0;
|
||||
case "caution":
|
||||
if (lowerIsBetter)
|
||||
return value < thresholds.alert && value >= thresholds.caution;
|
||||
else return value <= thresholds.caution && value > thresholds.alert;
|
||||
case "normal":
|
||||
if (lowerIsBetter)
|
||||
return value < thresholds.caution && value >= 0;
|
||||
else return value <= thresholds.normal && value > 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(jobFootprintData)
|
||||
*/
|
||||
const showFootprintTab = !!getContext("cc-config")[`job_view_showFootprint`];
|
||||
</script>
|
||||
|
||||
<Card class="overflow-auto" style="width: {width}; height: {height}">
|
||||
<TabContent> <!-- on:tab={(e) => (status = e.detail)} -->
|
||||
<TabContent>
|
||||
{#if showFootprintTab}
|
||||
<TabPane tabId="foot" tab="Footprint" active>
|
||||
<CardBody>
|
||||
{#each jobFootprintData 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}
|
||||
<Icon name="exclamation-triangle-fill" class="text-danger" />
|
||||
{:else if fpd.impact === 2}
|
||||
<Icon name="exclamation-triangle" class="text-warning" />
|
||||
{:else if fpd.impact === 0}
|
||||
<Icon name="info-circle" class="text-info" />
|
||||
{:else if fpd.impact === -1}
|
||||
<Icon name="info-circle-fill" class="text-danger" />
|
||||
{/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-smile" 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"
|
||||
>{fpd.message}</Tooltip
|
||||
>
|
||||
</div>
|
||||
<Row cols={12} class="{(jobFootprintData.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"
|
||||
>{fpd.message}</Tooltip
|
||||
>
|
||||
{/if}
|
||||
{/each}
|
||||
</CardBody>
|
||||
<!-- Bars CardBody Here-->
|
||||
<JobFootprintBars {job} />
|
||||
</TabPane>
|
||||
{/if}
|
||||
<TabPane tabId="polar" tab="Polar" active={!showFootprintTab}>
|
||||
<CardBody>
|
||||
<Polar
|
||||
{polarMetrics}
|
||||
{jobMetrics}
|
||||
/>
|
||||
</CardBody>
|
||||
<!-- Polar Plot CardBody Here -->
|
||||
<JobFootprintPolar {job} />
|
||||
</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>
|
||||
|
@@ -35,9 +35,8 @@
|
||||
sorting = {},
|
||||
isMetricSelectionOpen = false,
|
||||
selectedMetrics =
|
||||
getContext("cc-config")[
|
||||
`job_view_nodestats_selectedMetrics:${job.cluster}`
|
||||
] || getContext("cc-config")["job_view_nodestats_selectedMetrics"];
|
||||
getContext("cc-config")[`job_view_nodestats_selectedMetrics:${job.cluster}`] ||
|
||||
getContext("cc-config")["job_view_nodestats_selectedMetrics"];
|
||||
|
||||
for (let metric of allMetrics) {
|
||||
// Not Exclusive or Multi-Node: get maxScope directly (mostly: node)
|
||||
|
210
web/frontend/src/job/jobsummary/JobFootprintBars.svelte
Normal file
210
web/frontend/src/job/jobsummary/JobFootprintBars.svelte
Normal file
@@ -0,0 +1,210 @@
|
||||
<!--
|
||||
@component Job Footprint Bar component; Displays job footprint db data as bars relative to thresholds. Displays quality indicators and tooltips.
|
||||
|
||||
Properties:
|
||||
- `job Object`: The GQL job object
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { getContext } from "svelte";
|
||||
import {
|
||||
CardBody,
|
||||
Progress,
|
||||
Icon,
|
||||
Tooltip,
|
||||
Row,
|
||||
Col
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { findJobFootprintThresholds } from "../../generic/utils.js";
|
||||
|
||||
export let job;
|
||||
|
||||
// Prepare Job Footprint Data Based On Values Saved In Database
|
||||
const jobFootprintData = 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 = findJobFootprintThresholds(job, jf.stat, fmc);
|
||||
|
||||
// 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: `Footprint value way ${fmc.lowerIsBetter ? "above" : "below"} expected normal threshold.`,
|
||||
impact: 3
|
||||
};
|
||||
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "caution")) {
|
||||
return {
|
||||
...fmBase,
|
||||
color: "warning",
|
||||
message: `Footprint value ${fmc.lowerIsBetter ? "above" : "below"} expected normal threshold.`,
|
||||
impact: 2,
|
||||
};
|
||||
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "normal")) {
|
||||
return {
|
||||
...fmBase,
|
||||
color: "success",
|
||||
message: "Footprint value within expected thresholds.",
|
||||
impact: 1,
|
||||
};
|
||||
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "peak")) {
|
||||
return {
|
||||
...fmBase,
|
||||
color: "info",
|
||||
message:
|
||||
"Footprint value above expected normal threshold: Check for artifacts recommended.",
|
||||
impact: 0,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...fmBase,
|
||||
color: "secondary",
|
||||
message:
|
||||
"Footprint value 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(value, 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 value <= thresholds.peak && value > thresholds.normal;
|
||||
case "alert":
|
||||
if (lowerIsBetter)
|
||||
return value <= thresholds.peak && value >= thresholds.alert;
|
||||
else return value <= thresholds.alert && value >= 0;
|
||||
case "caution":
|
||||
if (lowerIsBetter)
|
||||
return value < thresholds.alert && value >= thresholds.caution;
|
||||
else return value <= thresholds.caution && value > thresholds.alert;
|
||||
case "normal":
|
||||
if (lowerIsBetter)
|
||||
return value < thresholds.caution && value >= 0;
|
||||
else return value <= thresholds.normal && value > thresholds.caution;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<CardBody>
|
||||
{#if jobFootprintData.length === 0}
|
||||
<div class="text-center">No footprint data for job available.</div>
|
||||
{:else}
|
||||
{#each jobFootprintData 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}
|
||||
<Icon name="exclamation-triangle-fill" class="text-danger" />
|
||||
{:else if fpd.impact === 2}
|
||||
<Icon name="exclamation-triangle" class="text-warning" />
|
||||
{:else if fpd.impact === 0}
|
||||
<Icon name="info-circle" class="text-info" />
|
||||
{:else if fpd.impact === -1}
|
||||
<Icon name="info-circle-fill" class="text-danger" />
|
||||
{/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-smile" 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"
|
||||
>{fpd.message}</Tooltip
|
||||
>
|
||||
</div>
|
||||
<Row cols={12} class="{(jobFootprintData.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"
|
||||
>{fpd.message}</Tooltip
|
||||
>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</CardBody>
|
||||
|
||||
<style>
|
||||
.cursor-help {
|
||||
cursor: help;
|
||||
}
|
||||
</style>
|
72
web/frontend/src/job/jobsummary/JobFootprintPolar.svelte
Normal file
72
web/frontend/src/job/jobsummary/JobFootprintPolar.svelte
Normal file
@@ -0,0 +1,72 @@
|
||||
<!--
|
||||
@component Job Footprint Polar Plot component; Displays queried job metric statistics polar plot.
|
||||
|
||||
Properties:
|
||||
- `job Object`: The GQL job object
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { getContext } from "svelte";
|
||||
import {
|
||||
queryStore,
|
||||
gql,
|
||||
getContextClient
|
||||
} from "@urql/svelte";
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
Spinner
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import Polar from "../../generic/plots/Polar.svelte";
|
||||
import { findJobFootprintThresholds } from "../../generic/utils.js";
|
||||
|
||||
export let job;
|
||||
|
||||
// Metric Names Configured To Be Footprints For (sub)Cluster
|
||||
const clusterFootprintMetrics = getContext("clusters")
|
||||
.find((c) => c.name == job.cluster)?.subClusters
|
||||
.find((sc) => sc.name == job.subCluster)?.footprint || []
|
||||
|
||||
// Get Scaled Peak Threshold Based on Footprint Type ([min, max, avg]) and Job Exclusivity
|
||||
const polarMetrics = getContext("globalMetrics").reduce((pms, gm) => {
|
||||
if (clusterFootprintMetrics.includes(gm.name)) {
|
||||
const fmt = findJobFootprintThresholds(job, gm.footprint, getContext("getMetricConfig")(job.cluster, job.subCluster, gm.name));
|
||||
pms.push({ name: gm.name, peak: fmt ? fmt.peak : null });
|
||||
}
|
||||
return pms;
|
||||
}, [])
|
||||
|
||||
// Pull All Series For Footprint Metrics Statistics Only On Node Scope
|
||||
const client = getContextClient();
|
||||
const polarQuery = gql`
|
||||
query ($dbid: ID!, $selectedMetrics: [String!]!) {
|
||||
jobMetricStats(id: $dbid, metrics: $selectedMetrics) {
|
||||
name
|
||||
stats {
|
||||
min
|
||||
avg
|
||||
max
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
$: polarData = queryStore({
|
||||
client: client,
|
||||
query: polarQuery,
|
||||
variables:{ dbid: job.id, selectedMetrics: clusterFootprintMetrics },
|
||||
});
|
||||
</script>
|
||||
|
||||
<CardBody>
|
||||
{#if $polarData.fetching}
|
||||
<Spinner />
|
||||
{:else if $polarData.error}
|
||||
<Card body color="danger">{$polarData.error.message}</Card>
|
||||
{:else}
|
||||
<Polar
|
||||
{polarMetrics}
|
||||
polarData={$polarData.data.jobMetricStats}
|
||||
/>
|
||||
{/if}
|
||||
</CardBody>
|
Reference in New Issue
Block a user