mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-10-26 14:25:06 +01:00
Merge branch 'hotfix' into add_detailed_nodelist
This commit is contained in:
@@ -422,14 +422,14 @@
|
||||
<td><Icon name="circle-fill" style="color: {colors[i]};" /></td>
|
||||
{#if groupSelection.key == "user"}
|
||||
<th scope="col"
|
||||
><a href="/monitoring/user/{te.id}?cluster={cluster.name}"
|
||||
><a href="/monitoring/user/{te.id}?cluster={cluster}"
|
||||
>{te.id}</a
|
||||
></th
|
||||
>
|
||||
{:else}
|
||||
<th scope="col"
|
||||
><a
|
||||
href="/monitoring/jobs/?cluster={cluster.name}&project={te.id}&projectMatch=eq"
|
||||
href="/monitoring/jobs/?cluster={cluster}&project={te.id}&projectMatch=eq"
|
||||
>{te.id}</a
|
||||
></th
|
||||
>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
export let isAdmin;
|
||||
export let isApi;
|
||||
export let username;
|
||||
export let ncontent;
|
||||
</script>
|
||||
|
||||
{#if isAdmin == true}
|
||||
@@ -22,7 +23,7 @@
|
||||
<CardHeader>
|
||||
<CardTitle class="mb-1">Admin Options</CardTitle>
|
||||
</CardHeader>
|
||||
<AdminSettings />
|
||||
<AdminSettings {ncontent}/>
|
||||
</Card>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -139,9 +139,6 @@
|
||||
return names;
|
||||
}, [])
|
||||
),
|
||||
...(ccconfig[`job_view_polarPlotMetrics:${job.cluster}`] ||
|
||||
ccconfig[`job_view_polarPlotMetrics`]
|
||||
),
|
||||
...(ccconfig[`job_view_nodestats_selectedMetrics:${job.cluster}`] ||
|
||||
ccconfig[`job_view_nodestats_selectedMetrics`]
|
||||
),
|
||||
|
||||
@@ -6,7 +6,8 @@ new Config({
|
||||
props: {
|
||||
isAdmin: isAdmin,
|
||||
isApi: isApi,
|
||||
username: username
|
||||
username: username,
|
||||
ncontent: ncontent,
|
||||
},
|
||||
context: new Map([
|
||||
['cc-config', clusterCockpitConfig],
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
import AddUser from "./admin/AddUser.svelte";
|
||||
import ShowUsers from "./admin/ShowUsers.svelte";
|
||||
import Options from "./admin/Options.svelte";
|
||||
import NoticeEdit from "./admin/NoticeEdit.svelte";
|
||||
|
||||
export let ncontent;
|
||||
|
||||
let users = [];
|
||||
let roles = [];
|
||||
@@ -52,4 +55,5 @@
|
||||
<EditProject on:reload={getUserList} />
|
||||
</Col>
|
||||
<Options />
|
||||
<NoticeEdit {ncontent}/>
|
||||
</Row>
|
||||
|
||||
78
web/frontend/src/config/admin/NoticeEdit.svelte
Normal file
78
web/frontend/src/config/admin/NoticeEdit.svelte
Normal file
@@ -0,0 +1,78 @@
|
||||
<!--
|
||||
@component Admin edit notice.txt content card
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { Col, Card, CardTitle, CardBody } from "@sveltestrap/sveltestrap";
|
||||
import { fade } from "svelte/transition";
|
||||
|
||||
export let ncontent;
|
||||
|
||||
let message = { msg: "", color: "#d63384" };
|
||||
let displayMessage = false;
|
||||
|
||||
async function handleEditNotice() {
|
||||
const content = document.querySelector("#notice-content").value;
|
||||
|
||||
let formData = new FormData();
|
||||
formData.append("new-content", content);
|
||||
|
||||
try {
|
||||
const res = await fetch(`/config/notice/`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
if (res.ok) {
|
||||
let text = await res.text();
|
||||
popMessage(text, "#048109");
|
||||
} else {
|
||||
let text = await res.text();
|
||||
throw new Error("Response Code " + res.status + "-> " + text);
|
||||
}
|
||||
} catch (err) {
|
||||
popMessage(err, "#d63384");
|
||||
}
|
||||
}
|
||||
|
||||
function popMessage(response, rescolor) {
|
||||
message = { msg: response, color: rescolor };
|
||||
displayMessage = true;
|
||||
setTimeout(function () {
|
||||
displayMessage = false;
|
||||
}, 3500);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Col>
|
||||
<Card class="h-100">
|
||||
<CardBody>
|
||||
<CardTitle class="mb-3">Edit Notice Shown On Homepage</CardTitle>
|
||||
<p>Empty content ("No Content.") hides notice card on homepage.</p>
|
||||
<div class="input-group mb-3">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="No Content."
|
||||
value={ncontent}
|
||||
id="notice-content"
|
||||
/>
|
||||
|
||||
<!-- PreventDefault on Sveltestrap-Button more complex to achieve than just use good ol' html button -->
|
||||
<!-- see: https://stackoverflow.com/questions/69630422/svelte-how-to-use-event-modifiers-in-my-own-components -->
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
type="button"
|
||||
id="edit-notice-button"
|
||||
on:click|preventDefault={handleEditNotice}>Edit Notice</button
|
||||
>
|
||||
</div>
|
||||
<p>
|
||||
{#if displayMessage}<b
|
||||
><code style="color: {message.color};" out:fade
|
||||
>Update: {message.msg}</code
|
||||
></b
|
||||
>{/if}
|
||||
</p>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Col>
|
||||
@@ -76,7 +76,7 @@
|
||||
numHWThreads: filterPresets.numHWThreads || { from: null, to: null },
|
||||
numAccelerators: filterPresets.numAccelerators || { from: null, to: null },
|
||||
|
||||
stats: [],
|
||||
stats: filterPresets.stats || [],
|
||||
};
|
||||
|
||||
let isClusterOpen = false,
|
||||
@@ -127,27 +127,30 @@
|
||||
items.push({ jobId: { [filters.jobIdMatch]: filters.jobId } });
|
||||
if (filters.arrayJobId != null)
|
||||
items.push({ arrayJobId: filters.arrayJobId });
|
||||
if (filters.numNodes.from != null || filters.numNodes.to != null)
|
||||
if (filters.numNodes.from != null || filters.numNodes.to != null) {
|
||||
items.push({
|
||||
numNodes: { from: filters.numNodes.from, to: filters.numNodes.to },
|
||||
});
|
||||
if (filters.numHWThreads.from != null || filters.numHWThreads.to != null)
|
||||
isNodesModified = true;
|
||||
}
|
||||
if (filters.numHWThreads.from != null || filters.numHWThreads.to != null) {
|
||||
items.push({
|
||||
numHWThreads: {
|
||||
from: filters.numHWThreads.from,
|
||||
to: filters.numHWThreads.to,
|
||||
},
|
||||
});
|
||||
if (
|
||||
filters.numAccelerators.from != null ||
|
||||
filters.numAccelerators.to != null
|
||||
)
|
||||
isHwthreadsModified = true;
|
||||
}
|
||||
if (filters.numAccelerators.from != null || filters.numAccelerators.to != null) {
|
||||
items.push({
|
||||
numAccelerators: {
|
||||
from: filters.numAccelerators.from,
|
||||
to: filters.numAccelerators.to,
|
||||
},
|
||||
});
|
||||
isAccsModified = true;
|
||||
}
|
||||
if (filters.user)
|
||||
items.push({ user: { [filters.userMatch]: filters.user } });
|
||||
if (filters.project)
|
||||
@@ -197,10 +200,10 @@
|
||||
opts.push(`energy=${filters.energy.from}-${filters.energy.to}`);
|
||||
if (filters.numNodes.from && filters.numNodes.to)
|
||||
opts.push(`numNodes=${filters.numNodes.from}-${filters.numNodes.to}`);
|
||||
if (filters.numHWThreads.from && filters.numHWThreads.to)
|
||||
opts.push(`numHWThreads=${filters.numHWThreads.from}-${filters.numHWThreads.to}`);
|
||||
if (filters.numAccelerators.from && filters.numAccelerators.to)
|
||||
opts.push(
|
||||
`numAccelerators=${filters.numAccelerators.from}-${filters.numAccelerators.to}`,
|
||||
);
|
||||
opts.push(`numAccelerators=${filters.numAccelerators.from}-${filters.numAccelerators.to}`);
|
||||
if (filters.user.length != 0)
|
||||
if (filters.userMatch != "in") {
|
||||
opts.push(`user=${filters.user}`);
|
||||
@@ -214,7 +217,10 @@
|
||||
if (filters.arrayJobId) opts.push(`arrayJobId=${filters.arrayJobId}`);
|
||||
if (filters.project && filters.projectMatch != "contains")
|
||||
opts.push(`projectMatch=${filters.projectMatch}`);
|
||||
|
||||
if (filters.stats.length != 0)
|
||||
for (let stat of filters.stats) {
|
||||
opts.push(`stat=${stat.field}-${stat.from}-${stat.to}`);
|
||||
}
|
||||
if (opts.length == 0 && window.location.search.length <= 1) return;
|
||||
|
||||
let newurl = `${window.location.pathname}?${opts.join("&")}`;
|
||||
@@ -364,8 +370,7 @@
|
||||
{#if (isNodesModified || isHwthreadsModified) && isAccsModified},
|
||||
{/if}
|
||||
{#if isAccsModified}
|
||||
Accelerators: {filters.numAccelerators.from} - {filters
|
||||
.numAccelerators.to}
|
||||
Accelerators: {filters.numAccelerators.from} - {filters.numAccelerators.to}
|
||||
{/if}
|
||||
</Info>
|
||||
{/if}
|
||||
@@ -385,7 +390,7 @@
|
||||
{#if filters.stats.length > 0}
|
||||
<Info icon="bar-chart" on:click={() => (isStatsOpen = true)}>
|
||||
{filters.stats
|
||||
.map((stat) => `${stat.text}: ${stat.from} - ${stat.to}`)
|
||||
.map((stat) => `${stat.field}: ${stat.from} - ${stat.to}`)
|
||||
.join(", ")}
|
||||
</Info>
|
||||
{/if}
|
||||
|
||||
@@ -30,6 +30,10 @@
|
||||
initialized = getContext("initialized"),
|
||||
globalMetrics = getContext("globalMetrics");
|
||||
|
||||
const equalsCheck = (a, b) => {
|
||||
return JSON.stringify(a) === JSON.stringify(b);
|
||||
}
|
||||
|
||||
export let sorting = { field: "startTime", type: "col", order: "DESC" };
|
||||
export let matchedJobs = 0;
|
||||
export let metrics = ccconfig.plot_list_selectedMetrics;
|
||||
@@ -40,6 +44,8 @@
|
||||
let page = 1;
|
||||
let paging = { itemsPerPage, page };
|
||||
let filter = [];
|
||||
let lastFilter = [];
|
||||
let lastSorting = null;
|
||||
let triggerMetricRefresh = false;
|
||||
|
||||
function getUnit(m) {
|
||||
@@ -105,9 +111,34 @@
|
||||
variables: { paging, sorting, filter },
|
||||
});
|
||||
|
||||
let jobs = []
|
||||
$: if (!usePaging && sorting) {
|
||||
// console.log('Reset Paging ...')
|
||||
paging = { itemsPerPage: 10, page: 1 }
|
||||
};
|
||||
|
||||
let jobs = [];
|
||||
$: if ($initialized && $jobsStore.data) {
|
||||
jobs = [...$jobsStore.data.jobs.items]
|
||||
if (usePaging) {
|
||||
jobs = [...$jobsStore.data.jobs.items]
|
||||
} else { // Prevents jump to table head in continiuous mode, only if no change in sort or filter
|
||||
if (equalsCheck(filter, lastFilter) && equalsCheck(sorting, lastSorting)) {
|
||||
// console.log('Both Equal: Continuous Addition ... Set None')
|
||||
jobs = jobs.concat([...$jobsStore.data.jobs.items])
|
||||
} else if (equalsCheck(filter, lastFilter)) {
|
||||
// console.log('Filter Equal: Continuous Reset ... Set lastSorting')
|
||||
lastSorting = { ...sorting }
|
||||
jobs = [...$jobsStore.data.jobs.items]
|
||||
} else if (equalsCheck(sorting, lastSorting)) {
|
||||
// console.log('Sorting Equal: Continuous Reset ... Set lastFilter')
|
||||
lastFilter = [ ...filter ]
|
||||
jobs = [...$jobsStore.data.jobs.items]
|
||||
} else {
|
||||
// console.log('None Equal: Continuous Reset ... Set lastBoth')
|
||||
lastSorting = { ...sorting }
|
||||
lastFilter = [ ...filter ]
|
||||
jobs = [...$jobsStore.data.jobs.items]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: matchedJobs = $jobsStore.data != null ? $jobsStore.data.jobs.count : -1;
|
||||
@@ -170,7 +201,6 @@
|
||||
}
|
||||
|
||||
if (!usePaging) {
|
||||
let scrollMultiplier = 1
|
||||
window.addEventListener('scroll', () => {
|
||||
let {
|
||||
scrollTop,
|
||||
@@ -181,8 +211,7 @@
|
||||
// Add 100 px offset to trigger load earlier
|
||||
if (scrollTop + clientHeight >= scrollHeight - 100 && $jobsStore.data != null && $jobsStore.data.jobs.hasNextPage) {
|
||||
let pendingPaging = { ...paging }
|
||||
scrollMultiplier += 1
|
||||
pendingPaging.itemsPerPage = itemsPerPage * scrollMultiplier
|
||||
pendingPaging.page += 1
|
||||
paging = pendingPaging
|
||||
};
|
||||
});
|
||||
|
||||
@@ -77,6 +77,13 @@
|
||||
dispatch("set-filter", { states });
|
||||
}}>Close & Apply</Button
|
||||
>
|
||||
<Button
|
||||
color="warning"
|
||||
on:click={() => {
|
||||
states = [...allJobStates];
|
||||
pendingStates = [];
|
||||
}}>Deselect All</Button
|
||||
>
|
||||
<Button
|
||||
color="danger"
|
||||
on:click={() => {
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
- `cluster Object?`: The currently selected cluster config [Default: null]
|
||||
- `isOpen Bool?`: Is this filter component opened [Default: false]
|
||||
- `numNodes Object?`: The currently selected numNodes filter [Default: {from:null, to:null}]
|
||||
- `numHWThreads Object?`: The currently selected numHWTreads filter [Default: {from:null, to:null}]
|
||||
- `numHWThreads Object?`: The currently selected numHWThreads filter [Default: {from:null, to:null}]
|
||||
- `numAccelerators Object?`: The currently selected numAccelerators filter [Default: {from:null, to:null}]
|
||||
- `isNodesModified Bool?`: Is the node filter modified [Default: false]
|
||||
- `isHwtreadsModified Bool?`: Is the Hwthreads filter modified [Default: false]
|
||||
- `isHwthreadsModified Bool?`: Is the Hwthreads filter modified [Default: false]
|
||||
- `isAccsModified Bool?`: Is the Accelerator filter modified [Default: false]
|
||||
- `namedNode String?`: The currently selected single named node (= hostname) [Default: null]
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
);
|
||||
|
||||
// Limited to Single-Node Thread Count
|
||||
const findMaxNumHWTreadsPerNode = (clusters) =>
|
||||
const findMaxNumHWThreadsPerNode = (clusters) =>
|
||||
clusters.reduce(
|
||||
(max, cluster) =>
|
||||
Math.max(
|
||||
@@ -91,13 +91,13 @@
|
||||
minNumNodes = filterRanges.numNodes.from;
|
||||
maxNumNodes = filterRanges.numNodes.to;
|
||||
maxNumAccelerators = findMaxNumAccels([{ subClusters }]);
|
||||
maxNumHWThreads = findMaxNumHWTreadsPerNode([{ subClusters }]);
|
||||
maxNumHWThreads = findMaxNumHWThreadsPerNode([{ subClusters }]);
|
||||
} else if (clusters.length > 0) {
|
||||
const { filterRanges } = header.clusters[0];
|
||||
minNumNodes = filterRanges.numNodes.from;
|
||||
maxNumNodes = filterRanges.numNodes.to;
|
||||
maxNumAccelerators = findMaxNumAccels(clusters);
|
||||
maxNumHWThreads = findMaxNumHWTreadsPerNode(clusters);
|
||||
maxNumHWThreads = findMaxNumHWThreadsPerNode(clusters);
|
||||
for (let cluster of header.clusters) {
|
||||
const { filterRanges } = cluster;
|
||||
minNumNodes = Math.min(minNumNodes, filterRanges.numNodes.from);
|
||||
|
||||
@@ -29,10 +29,11 @@
|
||||
export let isOpen = false;
|
||||
export let stats = [];
|
||||
|
||||
let statistics = []
|
||||
let statistics = [];
|
||||
|
||||
function loadRanges(isInitialized) {
|
||||
if (!isInitialized) return;
|
||||
statistics = getStatsItems();
|
||||
statistics = getStatsItems(stats);
|
||||
}
|
||||
|
||||
function resetRanges() {
|
||||
|
||||
@@ -8,44 +8,6 @@
|
||||
- `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 {
|
||||
@@ -59,7 +21,7 @@
|
||||
Row,
|
||||
Col
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { round } from "mathjs";
|
||||
import { findJobFootprintThresholds } from "../utils.js";
|
||||
|
||||
export let job;
|
||||
export let displayTitle = true;
|
||||
@@ -73,8 +35,7 @@
|
||||
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);
|
||||
const fmt = findJobFootprintThresholds(job, jf.stat, fmc);
|
||||
|
||||
// Define basic data -> Value: Use as Provided
|
||||
const fmBase = {
|
||||
@@ -89,21 +50,21 @@
|
||||
return {
|
||||
...fmBase,
|
||||
color: "danger",
|
||||
message: `Metric average way ${fmc.lowerIsBetter ? "above" : "below"} expected normal thresholds.`,
|
||||
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: `Metric average ${fmc.lowerIsBetter ? "above" : "below"} expected normal thresholds.`,
|
||||
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: "Metric average within expected thresholds.",
|
||||
message: "Footprint value within expected thresholds.",
|
||||
impact: 1,
|
||||
};
|
||||
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "peak")) {
|
||||
@@ -111,7 +72,7 @@
|
||||
...fmBase,
|
||||
color: "info",
|
||||
message:
|
||||
"Metric average above expected normal thresholds: Check for artifacts recommended.",
|
||||
"Footprint value above expected normal threshold: Check for artifacts recommended.",
|
||||
impact: 0,
|
||||
};
|
||||
} else {
|
||||
@@ -119,7 +80,7 @@
|
||||
...fmBase,
|
||||
color: "secondary",
|
||||
message:
|
||||
"Metric average above expected peak threshold: Check for artifacts!",
|
||||
"Footprint value above expected peak threshold: Check for artifacts!",
|
||||
impact: -1,
|
||||
};
|
||||
}
|
||||
@@ -136,25 +97,25 @@
|
||||
return a.impact - b.impact || ((a.name > b.name) ? 1 : ((b.name > a.name) ? -1 : 0));
|
||||
});;
|
||||
|
||||
function evalFootprint(mean, thresholds, lowerIsBetter, level) {
|
||||
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 mean <= thresholds.peak && mean > thresholds.normal;
|
||||
else return value <= thresholds.peak && value > thresholds.normal;
|
||||
case "alert":
|
||||
if (lowerIsBetter)
|
||||
return mean <= thresholds.peak && mean >= thresholds.alert;
|
||||
else return mean <= thresholds.alert && mean >= 0;
|
||||
return value <= thresholds.peak && value >= thresholds.alert;
|
||||
else return value <= thresholds.alert && value >= 0;
|
||||
case "caution":
|
||||
if (lowerIsBetter)
|
||||
return mean < thresholds.alert && mean >= thresholds.caution;
|
||||
else return mean <= thresholds.caution && mean > thresholds.alert;
|
||||
return value < thresholds.alert && value >= thresholds.caution;
|
||||
else return value <= thresholds.caution && value > thresholds.alert;
|
||||
case "normal":
|
||||
if (lowerIsBetter)
|
||||
return mean < thresholds.caution && mean >= 0;
|
||||
else return mean <= thresholds.normal && mean > thresholds.caution;
|
||||
return value < thresholds.caution && value >= 0;
|
||||
else return value <= thresholds.normal && value > thresholds.caution;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -181,10 +142,14 @@
|
||||
>
|
||||
<div class="mx-1">
|
||||
<!-- Alerts Only -->
|
||||
{#if fpd.impact === 3 || fpd.impact === -1}
|
||||
<Icon name="exclamation-triangle-fill" class="text-danger" />
|
||||
{#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}
|
||||
<!-- Emoji for all states-->
|
||||
{#if fpd.impact === 3}
|
||||
@@ -194,7 +159,7 @@
|
||||
{:else if fpd.impact === 1}
|
||||
<Icon name="emoji-smile" class="text-success" />
|
||||
{:else if fpd.impact === 0}
|
||||
<Icon name="emoji-laughing" class="text-info" />
|
||||
<Icon name="emoji-smile" class="text-info" />
|
||||
{:else if fpd.impact === -1}
|
||||
<Icon name="emoji-dizzy" class="text-danger" />
|
||||
{/if}
|
||||
|
||||
@@ -120,10 +120,13 @@
|
||||
|
||||
function matchJobTags(tags, availableTags, type, isAdmin, isSupport) {
|
||||
const jobTagIds = tags.map((t) => t.id)
|
||||
if (isAdmin || type == 'used') { // Always show used tags, admin also show all unused
|
||||
|
||||
if (type == 'used') { // Always show used tags
|
||||
return availableTags.filter((at) => jobTagIds.includes(at.id))
|
||||
} else { // ... for unused
|
||||
if (isSupport) { // ... show global tags for support
|
||||
if (isAdmin) { // ... show all tags for admin
|
||||
return availableTags.filter((at) => !jobTagIds.includes(at.id))
|
||||
} else if (isSupport) { // ... show global tags for support
|
||||
return availableTags.filter((at) => !jobTagIds.includes(at.id) && at.scope !== "admin")
|
||||
} else { // ... show only private tags for user, manager
|
||||
return availableTags.filter((at) => !jobTagIds.includes(at.id) && at.scope !== "admin" && at.scope !== "global")
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { Badge, Button, Icon } from "@sveltestrap/sveltestrap";
|
||||
import { Badge, Button, Icon, Tooltip } from "@sveltestrap/sveltestrap";
|
||||
import { scrambleNames, scramble } from "../utils.js";
|
||||
import Tag from "../helper/Tag.svelte";
|
||||
import TagManagement from "../helper/TagManagement.svelte";
|
||||
@@ -42,12 +42,30 @@
|
||||
let displayCheck = false;
|
||||
function clipJobId(jid) {
|
||||
displayCheck = true;
|
||||
navigator.clipboard
|
||||
.writeText(jid)
|
||||
.catch((reason) => console.error(reason));
|
||||
setTimeout(function () {
|
||||
displayCheck = false;
|
||||
}, 1500);
|
||||
// Navigator clipboard api needs a secure context (https)
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard
|
||||
.writeText(jid)
|
||||
.catch((reason) => console.error(reason));
|
||||
} else {
|
||||
// Workaround: Create, Fill, And Copy Content of Textarea
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = jid;
|
||||
textArea.style.position = "absolute";
|
||||
textArea.style.left = "-999999px";
|
||||
document.body.prepend(textArea);
|
||||
textArea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
textArea.remove();
|
||||
}
|
||||
}
|
||||
setTimeout(function () {
|
||||
displayCheck = false;
|
||||
}, 1000);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -58,13 +76,18 @@
|
||||
<a href="/monitoring/job/{job.id}" target="_blank">{job.jobId}</a>
|
||||
({job.cluster})
|
||||
</span>
|
||||
<Button outline color="secondary" size="sm" title="Copy JobID to Clipboard" on:click={clipJobId(job.jobId)} >
|
||||
<Button id={`${job.cluster}-${job.jobId}-clipboard`} outline color="secondary" size="sm" on:click={clipJobId(job.jobId)} >
|
||||
{#if displayCheck}
|
||||
<Icon name="clipboard2-check-fill"/> Copied
|
||||
<Icon name="clipboard2-check-fill"/>
|
||||
{:else}
|
||||
<Icon name="clipboard2"/> Job ID
|
||||
<Icon name="clipboard2"/>
|
||||
{/if}
|
||||
</Button>
|
||||
<Tooltip
|
||||
target={`${job.cluster}-${job.jobId}-clipboard`}
|
||||
placement="right">
|
||||
{ displayCheck ? 'Copied!' : 'Copy Job ID to Clipboard' }
|
||||
</Tooltip>
|
||||
</span>
|
||||
{#if job.metaData?.jobName}
|
||||
{#if job.metaData?.jobName.length <= 25}
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
: ["node"];
|
||||
let selectedResolution = resampleDefault;
|
||||
let zoomStates = {};
|
||||
let thresholdStates = {};
|
||||
|
||||
const cluster = getContext("clusters").find((c) => c.name == job.cluster);
|
||||
const client = getContextClient();
|
||||
@@ -80,6 +81,13 @@
|
||||
zoomStates[metric] = {...detail.lastZoomState}
|
||||
}
|
||||
|
||||
if ( // States have to differ, causes deathloop if just set
|
||||
detail?.lastThreshold &&
|
||||
thresholdStates[metric] !== detail.lastThreshold
|
||||
) { // Handle to correctly reset on summed metric scope change
|
||||
thresholdStates[metric] = detail.lastThreshold;
|
||||
}
|
||||
|
||||
if (detail?.newRes) { // Triggers GQL
|
||||
selectedResolution = detail.newRes
|
||||
}
|
||||
@@ -191,6 +199,7 @@
|
||||
numhwthreads={job.numHWThreads}
|
||||
numaccs={job.numAcc}
|
||||
zoomState={zoomStates[metric.data.name] || null}
|
||||
thresholdState={thresholdStates[metric.data.name] || null}
|
||||
/>
|
||||
{:else if metric.disabled == true && metric.data}
|
||||
<Card body color="info"
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
}
|
||||
|
||||
// removed arg "subcluster": input metricconfig and topology now directly derived from subcluster
|
||||
function findThresholds(
|
||||
function findJobAggregationThresholds(
|
||||
subClusterTopology,
|
||||
metricConfig,
|
||||
scope,
|
||||
@@ -60,10 +60,16 @@
|
||||
) {
|
||||
|
||||
if (!subClusterTopology || !metricConfig || !scope) {
|
||||
console.warn("Argument missing for findThresholds!");
|
||||
console.warn("Argument missing for findJobAggregationThresholds!");
|
||||
return null;
|
||||
}
|
||||
|
||||
// handle special *-stat scopes
|
||||
if (scope.match(/(.*)-stat$/)) {
|
||||
const statParts = scope.split('-');
|
||||
scope = statParts[0]
|
||||
}
|
||||
|
||||
if (
|
||||
(scope == "node" && isShared == false) ||
|
||||
metricConfig?.aggregation == "avg"
|
||||
@@ -78,19 +84,20 @@
|
||||
|
||||
|
||||
if (metricConfig?.aggregation == "sum") {
|
||||
let divisor = 1
|
||||
let divisor;
|
||||
if (isShared == true) { // Shared
|
||||
if (numaccs > 0) divisor = subClusterTopology.accelerators.length / numaccs;
|
||||
else if (numhwthreads > 0) divisor = subClusterTopology.node.length / numhwthreads;
|
||||
else if (numhwthreads > 0) divisor = subClusterTopology.core.length / numhwthreads;
|
||||
}
|
||||
else if (scope == 'socket') divisor = subClusterTopology.socket.length;
|
||||
else if (scope == "core") divisor = subClusterTopology.core.length;
|
||||
else if (scope == "accelerator")
|
||||
divisor = subClusterTopology.accelerators.length;
|
||||
else if (scope == "hwthread") divisor = subClusterTopology.node.length;
|
||||
else if (scope == 'node') divisor = 1; // Use as configured for nodes
|
||||
else if (scope == 'socket') divisor = subClusterTopology.socket.length;
|
||||
else if (scope == "memoryDomain") divisor = subClusterTopology.memoryDomain.length;
|
||||
else if (scope == "core") divisor = subClusterTopology.core.length;
|
||||
else if (scope == "hwthread") divisor = subClusterTopology.core.length; // alt. name for core
|
||||
else if (scope == "accelerator") divisor = subClusterTopology.accelerators.length;
|
||||
else {
|
||||
// console.log('TODO: how to calc thresholds for ', scope)
|
||||
return null;
|
||||
console.log('Unknown scope, return default aggregation thresholds ', scope)
|
||||
divisor = 1;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -130,6 +137,7 @@
|
||||
export let numhwthreads = 0;
|
||||
export let numaccs = 0;
|
||||
export let zoomState = null;
|
||||
export let thresholdState = null;
|
||||
|
||||
if (useStatsSeries == null) useStatsSeries = statisticsSeries != null;
|
||||
if (useStatsSeries == false && series == null) useStatsSeries = true;
|
||||
@@ -149,7 +157,7 @@
|
||||
caution: "rgba(255, 128, 0, 0.3)",
|
||||
alert: "rgba(255, 0, 0, 0.3)",
|
||||
};
|
||||
const thresholds = findThresholds(
|
||||
const thresholds = findJobAggregationThresholds(
|
||||
subClusterTopology,
|
||||
metricConfig,
|
||||
scope,
|
||||
@@ -468,12 +476,14 @@
|
||||
// console.log('Dispatch Zoom with Res from / to', timestep, closest)
|
||||
dispatch('zoom', {
|
||||
newRes: closest,
|
||||
lastZoomState: u?.scales
|
||||
lastZoomState: u?.scales,
|
||||
lastThreshold: thresholds?.normal
|
||||
});
|
||||
}
|
||||
} else {
|
||||
dispatch('zoom', {
|
||||
lastZoomState: u?.scales
|
||||
lastZoomState: u?.scales,
|
||||
lastThreshold: thresholds?.normal
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -498,16 +508,19 @@
|
||||
let timeoutId = null;
|
||||
|
||||
function render(ren_width, ren_height) {
|
||||
if (!uplot) { // Init uPlot
|
||||
if (!uplot) {
|
||||
opts.width = ren_width;
|
||||
opts.height = ren_height;
|
||||
if (zoomState) {
|
||||
if (zoomState && metricConfig?.aggregation == "avg") {
|
||||
opts.scales = {...zoomState}
|
||||
} else if (zoomState && metricConfig?.aggregation == "sum") {
|
||||
// Allow Zoom In === Ymin changed
|
||||
if (zoomState.y.min !== 0) { // scope change?: only use zoomState if thresholds match
|
||||
if ((thresholdState === thresholds?.normal)) { opts.scales = {...zoomState} };
|
||||
} // else: reset scaling to default
|
||||
}
|
||||
// console.log('Init Sizes ...', { width: opts.width, height: opts.height })
|
||||
uplot = new uPlot(opts, plotData, plotWrapper);
|
||||
} else { // Update size
|
||||
// console.log('Update uPlot ...', { width: ren_width, height: ren_height })
|
||||
} else {
|
||||
uplot.setSize({ width: ren_width, height: ren_height });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
if (footprintData) {
|
||||
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}'`)
|
||||
console.warn(`PolarPlot: No metric data for '${fpd.name}'`)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -72,6 +72,7 @@
|
||||
const getMetricConfig = getContext("getMetricConfig");
|
||||
|
||||
const getValuesForStatGeneric = (getStat) => labels.map(name => {
|
||||
// TODO: Requires Scaling if Shared Job
|
||||
const peak = getMetricConfig(cluster, subCluster, name).peak
|
||||
const metric = jobMetrics.find(m => m.name == name && m.scope == "node")
|
||||
const value = getStat(metric.metric) / peak
|
||||
@@ -79,6 +80,7 @@
|
||||
})
|
||||
|
||||
const getValuesForStatFootprint = (getStat) => labels.map(name => {
|
||||
// FootprintData 'Peak' is pre-scaled for Shared Jobs in JobSummary Component
|
||||
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
|
||||
@@ -86,14 +88,21 @@
|
||||
})
|
||||
|
||||
function getMax(metric) {
|
||||
let max = 0
|
||||
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
|
||||
let avg = 0;
|
||||
for (let series of metric.series)
|
||||
avg += series.statistics.avg
|
||||
return avg / metric.series.length
|
||||
@@ -104,6 +113,8 @@
|
||||
return getValuesForStatGeneric(getAvg)
|
||||
} else if (type === 'max') {
|
||||
return getValuesForStatGeneric(getMax)
|
||||
} else if (type === 'min') {
|
||||
return getValuesForStatGeneric(getMin)
|
||||
}
|
||||
console.log('Unknown Type For Polar Data')
|
||||
return []
|
||||
@@ -114,6 +125,8 @@
|
||||
return getValuesForStatFootprint(getAvg)
|
||||
} else if (type === 'max') {
|
||||
return getValuesForStatFootprint(getMax)
|
||||
} else if (type === 'min') {
|
||||
return getValuesForStatFootprint(getMin)
|
||||
}
|
||||
console.log('Unknown Type For Polar Data')
|
||||
return []
|
||||
@@ -124,25 +137,36 @@
|
||||
datasets: [
|
||||
{
|
||||
label: 'Max',
|
||||
data: footprintData ? loadDataForFootprint('max') : loadDataGeneric('max'), //
|
||||
data: footprintData ? loadDataForFootprint('max') : loadDataGeneric('max'), // Node Scope Only
|
||||
fill: 1,
|
||||
backgroundColor: 'rgba(0, 102, 255, 0.25)',
|
||||
borderColor: 'rgb(0, 102, 255)',
|
||||
pointBackgroundColor: 'rgb(0, 102, 255)',
|
||||
backgroundColor: 'rgba(0, 0, 255, 0.25)',
|
||||
borderColor: 'rgb(0, 0, 255)',
|
||||
pointBackgroundColor: 'rgb(0, 0, 255)',
|
||||
pointBorderColor: '#fff',
|
||||
pointHoverBackgroundColor: '#fff',
|
||||
pointHoverBorderColor: 'rgb(0, 102, 255)'
|
||||
pointHoverBorderColor: 'rgb(0, 0, 255)'
|
||||
},
|
||||
{
|
||||
label: 'Avg',
|
||||
data: footprintData ? loadDataForFootprint('avg') : loadDataGeneric('avg'), // getValuesForStat(getAvg)
|
||||
fill: true,
|
||||
backgroundColor: 'rgba(255, 153, 0, 0.25)',
|
||||
borderColor: 'rgb(255, 153, 0)',
|
||||
pointBackgroundColor: 'rgb(255, 153, 0)',
|
||||
data: footprintData ? loadDataForFootprint('avg') : loadDataGeneric('avg'), // Node Scope Only
|
||||
fill: 2,
|
||||
backgroundColor: 'rgba(255, 210, 0, 0.25)',
|
||||
borderColor: 'rgb(255, 210, 0)',
|
||||
pointBackgroundColor: 'rgb(255, 210, 0)',
|
||||
pointBorderColor: '#fff',
|
||||
pointHoverBackgroundColor: '#fff',
|
||||
pointHoverBorderColor: 'rgb(255, 153, 0)'
|
||||
pointHoverBorderColor: 'rgb(255, 210, 0)'
|
||||
},
|
||||
{
|
||||
label: 'Min',
|
||||
data: footprintData ? loadDataForFootprint('min') : loadDataGeneric('min'), // Node Scope Only
|
||||
fill: true,
|
||||
backgroundColor: 'rgba(255, 0, 0, 0.25)',
|
||||
borderColor: 'rgb(255, 0, 0)',
|
||||
pointBackgroundColor: 'rgb(255, 0, 0)',
|
||||
pointBorderColor: '#fff',
|
||||
pointHoverBackgroundColor: '#fff',
|
||||
pointHoverBorderColor: 'rgb(255, 0, 0)'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from "@urql/svelte";
|
||||
import { setContext, getContext, hasContext, onDestroy, tick } from "svelte";
|
||||
import { readable } from "svelte/store";
|
||||
import { round } from "mathjs";
|
||||
|
||||
/*
|
||||
* Call this function only at component initialization time!
|
||||
@@ -318,23 +319,34 @@ export function checkMetricsDisabled(ma, c, s) { // [m]etric[a]rray, [c]luster,
|
||||
return result
|
||||
}
|
||||
|
||||
export function getStatsItems() {
|
||||
export function getStatsItems(presetStats = []) {
|
||||
// console.time('stats')
|
||||
const globalMetrics = getContext("globalMetrics")
|
||||
const result = globalMetrics.map((gm) => {
|
||||
if (gm?.footprint) {
|
||||
// console.time('deep')
|
||||
const mc = getMetricConfigDeep(gm.name, null, null)
|
||||
// console.timeEnd('deep')
|
||||
if (mc) {
|
||||
return {
|
||||
field: gm.name + '_' + gm.footprint,
|
||||
text: gm.name + ' (' + gm.footprint + ')',
|
||||
metric: gm.name,
|
||||
from: 0,
|
||||
to: mc.peak,
|
||||
peak: mc.peak,
|
||||
enabled: false
|
||||
const presetEntry = presetStats.find((s) => s?.field === (gm.name + '_' + gm.footprint))
|
||||
if (presetEntry) {
|
||||
return {
|
||||
field: gm.name + '_' + gm.footprint,
|
||||
text: gm.name + ' (' + gm.footprint + ')',
|
||||
metric: gm.name,
|
||||
from: presetEntry.from,
|
||||
to: presetEntry.to,
|
||||
peak: mc.peak,
|
||||
enabled: true
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
field: gm.name + '_' + gm.footprint,
|
||||
text: gm.name + ' (' + gm.footprint + ')',
|
||||
metric: gm.name,
|
||||
from: 0,
|
||||
to: mc.peak,
|
||||
peak: mc.peak,
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -344,6 +356,38 @@ export function getStatsItems() {
|
||||
return [...result];
|
||||
};
|
||||
|
||||
export function findJobFootprintThresholds(job, stat, metricConfig) {
|
||||
if (!job || !metricConfig || !stat) {
|
||||
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
|
||||
};
|
||||
/*
|
||||
Footprints should be comparable:
|
||||
Always use unchanged single node thresholds for exclusive jobs and "avg" Footprints.
|
||||
For shared jobs, scale thresholds by the fraction of the job's HWThreads to the node's HWThreads.
|
||||
'stat' is one of: avg, min, max
|
||||
*/
|
||||
if (job.exclusive === 1 || stat === "avg") {
|
||||
return defaultThresholds
|
||||
} else {
|
||||
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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function getSortItems() {
|
||||
//console.time('sort')
|
||||
const globalMetrics = getContext("globalMetrics")
|
||||
|
||||
@@ -8,44 +8,6 @@
|
||||
- `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 {
|
||||
@@ -60,7 +22,7 @@
|
||||
TabPane
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import Polar from "../generic/plots/Polar.svelte";
|
||||
import { round } from "mathjs";
|
||||
import { findJobFootprintThresholds } from "../generic/utils.js";
|
||||
|
||||
export let job;
|
||||
export let jobMetrics;
|
||||
@@ -77,8 +39,7 @@
|
||||
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);
|
||||
const fmt = findJobFootprintThresholds(job, jf.stat, fmc);
|
||||
|
||||
// Define basic data -> Value: Use as Provided
|
||||
const fmBase = {
|
||||
@@ -94,21 +55,21 @@
|
||||
return {
|
||||
...fmBase,
|
||||
color: "danger",
|
||||
message: `Metric average way ${fmc.lowerIsBetter ? "above" : "below"} expected normal thresholds.`,
|
||||
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: `Metric average ${fmc.lowerIsBetter ? "above" : "below"} expected normal thresholds.`,
|
||||
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: "Metric average within expected thresholds.",
|
||||
message: "Footprint value within expected thresholds.",
|
||||
impact: 1,
|
||||
};
|
||||
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "peak")) {
|
||||
@@ -116,7 +77,7 @@
|
||||
...fmBase,
|
||||
color: "info",
|
||||
message:
|
||||
"Metric average above expected normal thresholds: Check for artifacts recommended.",
|
||||
"Footprint value above expected normal threshold: Check for artifacts recommended.",
|
||||
impact: 0,
|
||||
};
|
||||
} else {
|
||||
@@ -124,7 +85,7 @@
|
||||
...fmBase,
|
||||
color: "secondary",
|
||||
message:
|
||||
"Metric average above expected peak threshold: Check for artifacts!",
|
||||
"Footprint value above expected peak threshold: Check for artifacts!",
|
||||
impact: -1,
|
||||
};
|
||||
}
|
||||
@@ -142,25 +103,25 @@
|
||||
return a.impact - b.impact || ((a.name > b.name) ? 1 : ((b.name > a.name) ? -1 : 0));
|
||||
});;
|
||||
|
||||
function evalFootprint(mean, thresholds, lowerIsBetter, level) {
|
||||
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 mean <= thresholds.peak && mean > thresholds.normal;
|
||||
else return value <= thresholds.peak && value > thresholds.normal;
|
||||
case "alert":
|
||||
if (lowerIsBetter)
|
||||
return mean <= thresholds.peak && mean >= thresholds.alert;
|
||||
else return mean <= thresholds.alert && mean >= 0;
|
||||
return value <= thresholds.peak && value >= thresholds.alert;
|
||||
else return value <= thresholds.alert && value >= 0;
|
||||
case "caution":
|
||||
if (lowerIsBetter)
|
||||
return mean < thresholds.alert && mean >= thresholds.caution;
|
||||
else return mean <= thresholds.caution && mean > thresholds.alert;
|
||||
return value < thresholds.alert && value >= thresholds.caution;
|
||||
else return value <= thresholds.caution && value > thresholds.alert;
|
||||
case "normal":
|
||||
if (lowerIsBetter)
|
||||
return mean < thresholds.caution && mean >= 0;
|
||||
else return mean <= thresholds.normal && mean > thresholds.caution;
|
||||
return value < thresholds.caution && value >= 0;
|
||||
else return value <= thresholds.normal && value > thresholds.caution;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -244,10 +205,14 @@
|
||||
id={`footprint-${job.jobId}-${index}`}
|
||||
>
|
||||
<div class="mx-1">
|
||||
{#if fpd.impact === 3 || fpd.impact === -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" />
|
||||
@@ -256,7 +221,7 @@
|
||||
{:else if fpd.impact === 1}
|
||||
<Icon name="emoji-smile" class="text-success" />
|
||||
{:else if fpd.impact === 0}
|
||||
<Icon name="emoji-laughing" class="text-info" />
|
||||
<Icon name="emoji-smile" class="text-info" />
|
||||
{:else if fpd.impact === -1}
|
||||
<Icon name="emoji-dizzy" class="text-danger" />
|
||||
{/if}
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
let statsSeries = rawData.map((data) => data?.statisticsSeries ? data.statisticsSeries : null);
|
||||
let zoomState = null;
|
||||
let pendingZoomState = null;
|
||||
let thresholdState = null;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const statsPattern = /(.*)-stat$/;
|
||||
@@ -96,18 +97,24 @@
|
||||
(pendingZoomState?.x?.min !== detail?.lastZoomState?.x?.min) &&
|
||||
(pendingZoomState?.y?.max !== detail?.lastZoomState?.y?.max)
|
||||
) {
|
||||
pendingZoomState = {...detail.lastZoomState}
|
||||
pendingZoomState = {...detail.lastZoomState};
|
||||
}
|
||||
|
||||
if (detail?.lastThreshold) { // Handle to correctly reset on summed metric scope change
|
||||
thresholdState = detail.lastThreshold;
|
||||
} else {
|
||||
thresholdState = null;
|
||||
}
|
||||
|
||||
if (detail?.newRes) { // Triggers GQL
|
||||
pendingResolution = detail.newRes
|
||||
pendingResolution = detail.newRes;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let metricData;
|
||||
let selectedScopes = [...scopes]
|
||||
let selectedScopes = [...scopes];
|
||||
const dbid = job.id;
|
||||
const selectedMetrics = [metricName]
|
||||
const selectedMetrics = [metricName];
|
||||
|
||||
$: if (selectedScope || pendingResolution) {
|
||||
|
||||
@@ -206,9 +213,12 @@
|
||||
timestep={data.timestep}
|
||||
scope={selectedScope}
|
||||
metric={metricName}
|
||||
numaccs={job.numAcc}
|
||||
numhwthreads={job.numHWThreads}
|
||||
{series}
|
||||
{isShared}
|
||||
{zoomState}
|
||||
{thresholdState}
|
||||
/>
|
||||
{:else if statsSeries[selectedScopeIndex] != null && patternMatches}
|
||||
<Timeseries
|
||||
@@ -218,9 +228,12 @@
|
||||
timestep={data.timestep}
|
||||
scope={selectedScope}
|
||||
metric={metricName}
|
||||
numaccs={job.numAcc}
|
||||
numhwthreads={job.numHWThreads}
|
||||
{series}
|
||||
{isShared}
|
||||
{zoomState}
|
||||
{thresholdState}
|
||||
statisticsSeries={statsSeries[selectedScopeIndex]}
|
||||
useStatsSeries={!!statsSeries[selectedScopeIndex]}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user