mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-07-27 06:36:07 +02:00
Merge branch 'hotfix' into add_detailed_nodelist
This commit is contained in:
@@ -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")
|
||||
|
Reference in New Issue
Block a user