mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-07-01 03:03:50 +02:00
Merge branch 'dev' into port-to-cclib
This commit is contained in:
commit
64da28e814
@ -562,8 +562,20 @@
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<!-- Note: Ignore '#snippet' Error in IDE -->
|
||||
{#snippet histoGridContent(item)}
|
||||
<Histogram
|
||||
data={convert2uplot(item.bins)}
|
||||
usesBins={true}
|
||||
title="Average Distribution of '{item.metric}'"
|
||||
xlabel={`${item.metric} bin maximum [${metricUnits[item.metric]}]`}
|
||||
xunit={`${metricUnits[item.metric]}`}
|
||||
ylabel="Normalized Hours"
|
||||
yunit="Hours"
|
||||
/>
|
||||
{/snippet}
|
||||
|
||||
<PlotGrid
|
||||
let:item
|
||||
items={metricsInHistograms.map((metric) => ({
|
||||
metric,
|
||||
...binsFromFootprint(
|
||||
@ -576,17 +588,8 @@
|
||||
),
|
||||
}))}
|
||||
itemsPerRow={ccconfig.plot_view_plotsPerRow}
|
||||
>
|
||||
<Histogram
|
||||
data={convert2uplot(item.bins)}
|
||||
usesBins={true}
|
||||
title="Average Distribution of '{item.metric}'"
|
||||
xlabel={`${item.metric} bin maximum [${metricUnits[item.metric]}]`}
|
||||
xunit={`${metricUnits[item.metric]}`}
|
||||
ylabel="Normalized Hours"
|
||||
yunit="Hours"
|
||||
/>
|
||||
</PlotGrid>
|
||||
gridContent={histoGridContent}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
@ -604,9 +607,19 @@
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
{#snippet metricsGridContent(item)}
|
||||
<ScatterPlot
|
||||
height={250}
|
||||
color={"rgba(0, 102, 204, 0.33)"}
|
||||
xLabel={`${item.m1} [${metricUnits[item.m1]}]`}
|
||||
yLabel={`${item.m2} [${metricUnits[item.m2]}]`}
|
||||
X={item.f1}
|
||||
Y={item.f2}
|
||||
S={$footprintsQuery.data.footprints.timeWeights.nodeHours}
|
||||
/>
|
||||
{/snippet}
|
||||
|
||||
<PlotGrid
|
||||
let:item
|
||||
let:width
|
||||
items={metricsInScatterplots.map(([m1, m2]) => ({
|
||||
m1,
|
||||
f1: $footprintsQuery.data.footprints.metrics.find(
|
||||
@ -618,18 +631,8 @@
|
||||
).data,
|
||||
}))}
|
||||
itemsPerRow={ccconfig.plot_view_plotsPerRow}
|
||||
>
|
||||
<ScatterPlot
|
||||
{width}
|
||||
height={250}
|
||||
color={"rgba(0, 102, 204, 0.33)"}
|
||||
xLabel={`${item.m1} [${metricUnits[item.m1]}]`}
|
||||
yLabel={`${item.m2} [${metricUnits[item.m2]}]`}
|
||||
X={item.f1}
|
||||
Y={item.f2}
|
||||
S={$footprintsQuery.data.footprints.timeWeights.nodeHours}
|
||||
/>
|
||||
</PlotGrid>
|
||||
gridContent={metricsGridContent}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
{/if}
|
||||
|
@ -9,10 +9,11 @@
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { getContext } from "svelte";
|
||||
import {
|
||||
queryStore,
|
||||
gql,
|
||||
getContextClient
|
||||
getContextClient,
|
||||
} from "@urql/svelte";
|
||||
import {
|
||||
Row,
|
||||
@ -26,7 +27,6 @@
|
||||
CardTitle,
|
||||
Button,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { getContext } from "svelte";
|
||||
import {
|
||||
init,
|
||||
groupByScope,
|
||||
@ -42,89 +42,131 @@
|
||||
import PlotGrid from "./generic/PlotGrid.svelte";
|
||||
import StatsTab from "./job/StatsTab.svelte";
|
||||
|
||||
export let dbid;
|
||||
export let username;
|
||||
export let authlevel;
|
||||
export let roles;
|
||||
|
||||
// Setup General
|
||||
|
||||
const ccconfig = getContext("cc-config")
|
||||
|
||||
let isMetricsSelectionOpen = false,
|
||||
selectedMetrics = [],
|
||||
selectedScopes = [],
|
||||
plots = {};
|
||||
|
||||
let totalMetrics = 0;
|
||||
let missingMetrics = [],
|
||||
missingHosts = [],
|
||||
somethingMissing = false;
|
||||
|
||||
// Setup GQL
|
||||
// First: Add Job Query to init function -> Only requires DBID as argument, received via URL-ID
|
||||
// Second: Trigger jobMetrics query with now received jobInfos (scopes: from job metadata, selectedMetrics: from config or all, job: from url-id)
|
||||
/* Svelte 5 Props */
|
||||
let {
|
||||
dbid,
|
||||
username,
|
||||
authlevel,
|
||||
roles
|
||||
} = $props();
|
||||
|
||||
/* Const Init */
|
||||
// Important: init() needs to be first const declaration or contextclient will not be initialized before "const client = ..."
|
||||
const { query: initq } = init(`
|
||||
job(id: "${dbid}") {
|
||||
id, jobId, user, project, cluster, startTime,
|
||||
duration, numNodes, numHWThreads, numAcc, energy,
|
||||
SMT, exclusive, partition, subCluster, arrayJobId,
|
||||
monitoringStatus, state, walltime,
|
||||
tags { id, type, scope, name },
|
||||
resources { hostname, hwthreads, accelerators },
|
||||
metaData,
|
||||
userData { name, email },
|
||||
concurrentJobs { items { id, jobId }, count, listQuery },
|
||||
footprint { name, stat, value },
|
||||
energyFootprint { hardware, metric, value }
|
||||
}
|
||||
`);
|
||||
|
||||
job(id: "${dbid}") {
|
||||
id, jobId, user, project, cluster, startTime,
|
||||
duration, numNodes, numHWThreads, numAcc, energy,
|
||||
SMT, exclusive, partition, subCluster, arrayJobId,
|
||||
monitoringStatus, state, walltime,
|
||||
tags { id, type, scope, name },
|
||||
resources { hostname, hwthreads, accelerators },
|
||||
metaData,
|
||||
userData { name, email },
|
||||
concurrentJobs { items { id, jobId }, count, listQuery },
|
||||
footprint { name, stat, value },
|
||||
energyFootprint { hardware, metric, value }
|
||||
}
|
||||
`);
|
||||
const client = getContextClient();
|
||||
const ccconfig = getContext("cc-config");
|
||||
/* Note: Actual metric data queried in <Metric> Component, only require base infos here -> reduce backend load by requesting just stats */
|
||||
const query = gql`
|
||||
query ($dbid: ID!, $selectedMetrics: [String!]!, $selectedScopes: [MetricScope!]!) {
|
||||
jobMetrics(id: $dbid, metrics: $selectedMetrics, scopes: $selectedScopes) {
|
||||
scopedJobStats(id: $dbid, metrics: $selectedMetrics, scopes: $selectedScopes) {
|
||||
name
|
||||
scope
|
||||
metric {
|
||||
unit {
|
||||
prefix
|
||||
base
|
||||
}
|
||||
timestep
|
||||
statisticsSeries {
|
||||
min
|
||||
mean
|
||||
median
|
||||
max
|
||||
}
|
||||
series {
|
||||
hostname
|
||||
id
|
||||
data
|
||||
statistics {
|
||||
min
|
||||
avg
|
||||
max
|
||||
}
|
||||
}
|
||||
stats {
|
||||
hostname
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
$: jobMetrics = queryStore({
|
||||
client: client,
|
||||
query: query,
|
||||
variables: { dbid, selectedMetrics, selectedScopes },
|
||||
/* State Init */
|
||||
let plots = $state({});
|
||||
let isMetricsSelectionOpen = $state(false);
|
||||
let selectedMetrics = $state([]);
|
||||
let selectedScopes = $state([]);
|
||||
let totalMetrics = $state(0);
|
||||
|
||||
/* Derived */
|
||||
const jobMetrics = $derived(queryStore({
|
||||
client: client,
|
||||
query: query,
|
||||
variables: { dbid, selectedMetrics, selectedScopes },
|
||||
})
|
||||
);
|
||||
|
||||
const missingMetrics = $derived.by(() => {
|
||||
if ($initq?.data && $jobMetrics?.data) {
|
||||
let job = $initq.data.job;
|
||||
let metrics = $jobMetrics.data.scopedJobStats;
|
||||
let metricNames = $initq.data.globalMetrics.reduce((names, gm) => {
|
||||
if (gm.availability.find((av) => av.cluster === job.cluster)) {
|
||||
names.push(gm.name);
|
||||
}
|
||||
return names;
|
||||
}, []);
|
||||
|
||||
return metricNames.filter(
|
||||
(metric) =>
|
||||
!metrics.some((jm) => jm.name == metric) &&
|
||||
selectedMetrics.includes(metric) &&
|
||||
!checkMetricDisabled(
|
||||
metric,
|
||||
$initq.data.job.cluster,
|
||||
$initq.data.job.subCluster,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
});
|
||||
|
||||
// Handle Job Query on Init -> is not executed anymore
|
||||
getContext("on-init")(() => {
|
||||
const missingHosts = $derived.by(() => {
|
||||
if ($initq?.data && $jobMetrics?.data) {
|
||||
let job = $initq.data.job;
|
||||
let metrics = $jobMetrics.data.scopedJobStats;
|
||||
let metricNames = $initq.data.globalMetrics.reduce((names, gm) => {
|
||||
if (gm.availability.find((av) => av.cluster === job.cluster)) {
|
||||
names.push(gm.name);
|
||||
}
|
||||
return names;
|
||||
}, []);
|
||||
|
||||
return job.resources
|
||||
.map(({ hostname }) => ({
|
||||
hostname: hostname,
|
||||
metrics: metricNames.filter(
|
||||
(metric) =>
|
||||
!metrics.some(
|
||||
(jm) =>
|
||||
jm.scope == "node" &&
|
||||
jm.stats.some((s) => s.hostname == hostname),
|
||||
),
|
||||
),
|
||||
}))
|
||||
.filter(({ metrics }) => metrics.length > 0);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
const somethingMissing = $derived(missingMetrics?.length > 0 || missingHosts?.length > 0);
|
||||
|
||||
/* Effects */
|
||||
$effect(() => {
|
||||
document.title = $initq.fetching
|
||||
? "Loading..."
|
||||
: $initq.error
|
||||
? "Error"
|
||||
: `Job ${$initq.data.job.jobId} - ClusterCockpit`;
|
||||
});
|
||||
|
||||
/* On Init */
|
||||
getContext("on-init")(() => {
|
||||
let job = $initq.data.job;
|
||||
if (!job) return;
|
||||
|
||||
const pendingMetrics = (
|
||||
ccconfig[`job_view_selectedMetrics:${job.cluster}:${job.subCluster}`] ||
|
||||
ccconfig[`job_view_selectedMetrics:${job.cluster}`]
|
||||
@ -154,52 +196,7 @@
|
||||
selectedScopes = [...new Set(pendingScopes)];
|
||||
});
|
||||
|
||||
// Interactive Document Title
|
||||
$: document.title = $initq.fetching
|
||||
? "Loading..."
|
||||
: $initq.error
|
||||
? "Error"
|
||||
: `Job ${$initq.data.job.jobId} - ClusterCockpit`;
|
||||
|
||||
// Find out what metrics or hosts are missing:
|
||||
$: if ($initq?.data && $jobMetrics?.data?.jobMetrics) {
|
||||
let job = $initq.data.job,
|
||||
metrics = $jobMetrics.data.jobMetrics,
|
||||
metricNames = $initq.data.globalMetrics.reduce((names, gm) => {
|
||||
if (gm.availability.find((av) => av.cluster === job.cluster)) {
|
||||
names.push(gm.name);
|
||||
}
|
||||
return names;
|
||||
}, []);
|
||||
|
||||
// Metric not found in JobMetrics && Metric not explicitly disabled in config or deselected: Was expected, but is Missing
|
||||
missingMetrics = metricNames.filter(
|
||||
(metric) =>
|
||||
!metrics.some((jm) => jm.name == metric) &&
|
||||
selectedMetrics.includes(metric) &&
|
||||
!checkMetricDisabled(
|
||||
metric,
|
||||
$initq.data.job.cluster,
|
||||
$initq.data.job.subCluster,
|
||||
),
|
||||
);
|
||||
missingHosts = job.resources
|
||||
.map(({ hostname }) => ({
|
||||
hostname: hostname,
|
||||
metrics: metricNames.filter(
|
||||
(metric) =>
|
||||
!metrics.some(
|
||||
(jm) =>
|
||||
jm.scope == "node" &&
|
||||
jm.metric.series.some((series) => series.hostname == hostname),
|
||||
),
|
||||
),
|
||||
}))
|
||||
.filter(({ metrics }) => metrics.length > 0);
|
||||
somethingMissing = missingMetrics.length > 0 || missingHosts.length > 0;
|
||||
}
|
||||
|
||||
// Helper
|
||||
/* Functions */
|
||||
const orderAndMap = (grouped, selectedMetrics) =>
|
||||
selectedMetrics.map((metric) => ({
|
||||
metric: metric,
|
||||
@ -293,7 +290,7 @@
|
||||
<Row class="mb-2">
|
||||
{#if $initq?.data}
|
||||
<Col xs="auto">
|
||||
<Button outline on:click={() => (isMetricsSelectionOpen = true)} color="primary">
|
||||
<Button outline onclick={() => (isMetricsSelectionOpen = true)} color="primary">
|
||||
Select Metrics (Selected {selectedMetrics.length} of {totalMetrics} available)
|
||||
</Button>
|
||||
</Col>
|
||||
@ -317,15 +314,9 @@
|
||||
<Spinner secondary />
|
||||
</Col>
|
||||
</Row>
|
||||
{:else if $initq?.data && $jobMetrics?.data?.jobMetrics}
|
||||
<PlotGrid
|
||||
let:item
|
||||
items={orderAndMap(
|
||||
groupByScope($jobMetrics.data.jobMetrics),
|
||||
selectedMetrics,
|
||||
)}
|
||||
itemsPerRow={ccconfig.plot_view_plotsPerRow}
|
||||
>
|
||||
{:else if $initq?.data && $jobMetrics?.data?.scopedJobStats}
|
||||
<!-- Note: Ignore '#snippet' Error in IDE -->
|
||||
{#snippet gridContent(item)}
|
||||
{#if item.data}
|
||||
<Metric
|
||||
bind:this={plots[item.metric]}
|
||||
@ -333,8 +324,7 @@
|
||||
metricName={item.metric}
|
||||
metricUnit={$initq.data.globalMetrics.find((gm) => gm.name == item.metric)?.unit}
|
||||
nativeScope={$initq.data.globalMetrics.find((gm) => gm.name == item.metric)?.scope}
|
||||
rawData={item.data.map((x) => x.metric)}
|
||||
scopes={item.data.map((x) => x.scope)}
|
||||
presetScopes={item.data.map((x) => x.scope)}
|
||||
isShared={$initq.data.job.exclusive != 1}
|
||||
/>
|
||||
{:else if item.disabled == true}
|
||||
@ -357,7 +347,16 @@
|
||||
</CardBody>
|
||||
</Card>
|
||||
{/if}
|
||||
</PlotGrid>
|
||||
{/snippet}
|
||||
|
||||
<PlotGrid
|
||||
items={orderAndMap(
|
||||
groupByScope($jobMetrics.data.scopedJobStats),
|
||||
selectedMetrics,
|
||||
)}
|
||||
itemsPerRow={ccconfig.plot_view_plotsPerRow}
|
||||
{gridContent}
|
||||
/>
|
||||
{/if}
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
@ -178,18 +178,18 @@
|
||||
{#if !showCompare}
|
||||
<JobList
|
||||
bind:this={jobList}
|
||||
bind:metrics
|
||||
bind:sorting
|
||||
bind:matchedListJobs
|
||||
bind:showFootprint
|
||||
bind:selectedJobs
|
||||
{metrics}
|
||||
{sorting}
|
||||
{showFootprint}
|
||||
{filterBuffer}
|
||||
/>
|
||||
{:else}
|
||||
<JobCompare
|
||||
bind:this={jobCompare}
|
||||
bind:metrics
|
||||
bind:matchedCompareJobs
|
||||
{metrics}
|
||||
{filterBuffer}
|
||||
/>
|
||||
{/if}
|
||||
@ -201,7 +201,8 @@
|
||||
presetSorting={sorting}
|
||||
applySorting={(newSort) =>
|
||||
sorting = {...newSort}
|
||||
}/>
|
||||
}
|
||||
/>
|
||||
|
||||
<MetricSelection
|
||||
bind:isOpen={isMetricsSelectionOpen}
|
||||
|
@ -204,20 +204,8 @@
|
||||
{:else if $nodeMetricsData.fetching || $initq.fetching}
|
||||
<Spinner />
|
||||
{:else}
|
||||
<PlotGrid
|
||||
let:item
|
||||
itemsPerRow={ccconfig.plot_view_plotsPerRow}
|
||||
items={$nodeMetricsData.data.nodeMetrics[0].metrics
|
||||
.map((m) => ({
|
||||
...m,
|
||||
disabled: checkMetricDisabled(
|
||||
m.name,
|
||||
cluster,
|
||||
$nodeMetricsData.data.nodeMetrics[0].subCluster,
|
||||
),
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name))}
|
||||
>
|
||||
<!-- Note: Ignore '#snippet' Error in IDE -->
|
||||
{#snippet gridContent(item)}
|
||||
<h4 style="text-align: center; padding-top:15px;">
|
||||
{item.name}
|
||||
{systemUnits[item.name] ? "(" + systemUnits[item.name] + ")" : ""}
|
||||
@ -246,7 +234,22 @@
|
||||
>No dataset returned for <code>{item.name}</code></Card
|
||||
>
|
||||
{/if}
|
||||
</PlotGrid>
|
||||
{/snippet}
|
||||
|
||||
<PlotGrid
|
||||
items={$nodeMetricsData.data.nodeMetrics[0].metrics
|
||||
.map((m) => ({
|
||||
...m,
|
||||
disabled: checkMetricDisabled(
|
||||
m.name,
|
||||
cluster,
|
||||
$nodeMetricsData.data.nodeMetrics[0].subCluster,
|
||||
),
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name))}
|
||||
itemsPerRow={ccconfig.plot_view_plotsPerRow}
|
||||
{gridContent}
|
||||
/>
|
||||
{/if}
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -676,22 +676,25 @@
|
||||
<!-- Selectable Stats as Histograms : Average Values of Running Jobs -->
|
||||
|
||||
{#if selectedHistograms}
|
||||
<!-- Note: Ignore '#snippet' Error in IDE -->
|
||||
{#snippet gridContent(item)}
|
||||
<Histogram
|
||||
data={convert2uplot(item.data)}
|
||||
usesBins={true}
|
||||
title="Distribution of '{item.metric}' averages"
|
||||
xlabel={`${item.metric} bin maximum ${item?.unit ? `[${item.unit}]` : ``}`}
|
||||
xunit={item.unit}
|
||||
ylabel="Number of Jobs"
|
||||
yunit="Jobs"
|
||||
/>
|
||||
{/snippet}
|
||||
|
||||
{#key $mainQuery.data.stats[0].histMetrics}
|
||||
<PlotGrid
|
||||
let:item
|
||||
items={$mainQuery.data.stats[0].histMetrics}
|
||||
itemsPerRow={2}
|
||||
>
|
||||
<Histogram
|
||||
data={convert2uplot(item.data)}
|
||||
usesBins={true}
|
||||
title="Distribution of '{item.metric}' averages"
|
||||
xlabel={`${item.metric} bin maximum ${item?.unit ? `[${item.unit}]` : ``}`}
|
||||
xunit={item.unit}
|
||||
ylabel="Number of Jobs"
|
||||
yunit="Jobs"
|
||||
/>
|
||||
</PlotGrid>
|
||||
{gridContent}
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
{/if}
|
||||
|
@ -335,22 +335,25 @@
|
||||
</Row>
|
||||
{:else}
|
||||
<hr class="my-2"/>
|
||||
<!-- Note: Ignore '#snippet' Error in IDE -->
|
||||
{#snippet gridContent(item)}
|
||||
<Histogram
|
||||
data={convert2uplot(item.data)}
|
||||
title="Distribution of '{item.metric} ({item.stat})' footprints"
|
||||
xlabel={`${item.metric} bin maximum ${item?.unit ? `[${item.unit}]` : ``}`}
|
||||
xunit={item.unit}
|
||||
ylabel="Number of Jobs"
|
||||
yunit="Jobs"
|
||||
usesBins
|
||||
/>
|
||||
{/snippet}
|
||||
|
||||
{#key $stats.data.jobsStatistics[0].histMetrics}
|
||||
<PlotGrid
|
||||
let:item
|
||||
items={$stats.data.jobsStatistics[0].histMetrics}
|
||||
itemsPerRow={3}
|
||||
>
|
||||
<Histogram
|
||||
data={convert2uplot(item.data)}
|
||||
title="Distribution of '{item.metric} ({item.stat})' footprints"
|
||||
xlabel={`${item.metric} bin maximum ${item?.unit ? `[${item.unit}]` : ``}`}
|
||||
xunit={item.unit}
|
||||
ylabel="Number of Jobs"
|
||||
yunit="Jobs"
|
||||
usesBins
|
||||
/>
|
||||
</PlotGrid>
|
||||
{gridContent}
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
{:else}
|
||||
@ -367,9 +370,9 @@
|
||||
<JobList
|
||||
bind:this={jobList}
|
||||
bind:matchedListJobs
|
||||
bind:metrics
|
||||
bind:sorting
|
||||
bind:showFootprint
|
||||
{metrics}
|
||||
{sorting}
|
||||
{showFootprint}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
@ -388,10 +391,10 @@
|
||||
presetMetrics={metrics}
|
||||
cluster={selectedCluster}
|
||||
configName="plot_list_selectedMetrics"
|
||||
footprintSelect
|
||||
applyMetrics={(newMetrics) =>
|
||||
metrics = [...newMetrics]
|
||||
}
|
||||
footprintSelect
|
||||
/>
|
||||
|
||||
<HistogramSelection
|
||||
|
@ -26,7 +26,7 @@
|
||||
/* Svelte 5 Props */
|
||||
let {
|
||||
matchedCompareJobs = $bindable(0),
|
||||
metrics = $bindable(ccconfig?.plot_list_selectedMetrics),
|
||||
metrics = ccconfig?.plot_list_selectedMetrics,
|
||||
filterBuffer = [],
|
||||
} = $props();
|
||||
|
||||
|
@ -14,7 +14,7 @@
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { getContext } from "svelte";
|
||||
import { getContext, untrack } from "svelte";
|
||||
import {
|
||||
queryStore,
|
||||
gql,
|
||||
@ -26,35 +26,22 @@
|
||||
import Pagination from "./joblist/Pagination.svelte";
|
||||
import JobListRow from "./joblist/JobListRow.svelte";
|
||||
|
||||
const ccconfig = getContext("cc-config"),
|
||||
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 matchedListJobs = 0;
|
||||
export let metrics = ccconfig.plot_list_selectedMetrics;
|
||||
export let showFootprint;
|
||||
export let filterBuffer = [];
|
||||
export let selectedJobs = [];
|
||||
|
||||
let usePaging = ccconfig.job_list_usePaging
|
||||
let itemsPerPage = usePaging ? ccconfig.plot_list_jobsPerPage : 10;
|
||||
let page = 1;
|
||||
let paging = { itemsPerPage, page };
|
||||
let filter = [...filterBuffer];
|
||||
let lastFilter = [];
|
||||
let lastSorting = null;
|
||||
let triggerMetricRefresh = false;
|
||||
|
||||
function getUnit(m) {
|
||||
const rawUnit = globalMetrics.find((gm) => gm.name === m)?.unit
|
||||
return (rawUnit?.prefix ? rawUnit.prefix : "") + (rawUnit?.base ? rawUnit.base : "")
|
||||
}
|
||||
/* Svelte 5 Props */
|
||||
let {
|
||||
matchedListJobs = $bindable(0),
|
||||
selectedJobs = $bindable([]),
|
||||
metrics = getContext("cc-config").plot_list_selectedMetrics,
|
||||
sorting = { field: "startTime", type: "col", order: "DESC" },
|
||||
showFootprint = false,
|
||||
filterBuffer = [],
|
||||
} = $props();
|
||||
|
||||
/* Const Init */
|
||||
const ccconfig = getContext("cc-config");
|
||||
const initialized = getContext("initialized");
|
||||
const globalMetrics = getContext("globalMetrics");
|
||||
const usePaging = ccconfig?.job_list_usePaging || false;
|
||||
const jobInfoColumnWidth = 250;
|
||||
const client = getContextClient();
|
||||
const query = gql`
|
||||
query (
|
||||
@ -107,58 +94,75 @@
|
||||
}
|
||||
`;
|
||||
|
||||
$: jobsStore = queryStore({
|
||||
client: client,
|
||||
query: query,
|
||||
variables: { paging, sorting, filter },
|
||||
/* State Init */
|
||||
let headerPaddingTop = $state(0);
|
||||
let jobs = $state([]);
|
||||
let filter = $state([...filterBuffer]);
|
||||
let page = $state(1);
|
||||
let itemsPerPage = $state(usePaging ? (ccconfig?.plot_list_jobsPerPage || 10) : 10);
|
||||
let triggerMetricRefresh = $state(false);
|
||||
let tableWidth = $state(0);
|
||||
|
||||
/* Derived */
|
||||
let paging = $derived({ itemsPerPage, page });
|
||||
const plotWidth = $derived.by(() => {
|
||||
return Math.floor(
|
||||
(tableWidth - jobInfoColumnWidth) / (metrics.length + (showFootprint ? 1 : 0)) - 10,
|
||||
);
|
||||
});
|
||||
|
||||
let jobs = [];
|
||||
$: if ($initialized && $jobsStore.data) {
|
||||
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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: if (!usePaging && (sorting || filter)) {
|
||||
// Continous Scroll: Reset list and paging if parameters change: Existing entries will not match new selections
|
||||
jobs = [];
|
||||
paging = { itemsPerPage: 10, page: 1 };
|
||||
}
|
||||
|
||||
$: matchedListJobs = $jobsStore.data != null ? $jobsStore.data.jobs.count : -1;
|
||||
|
||||
// Force refresh list with existing unchanged variables (== usually would not trigger reactivity)
|
||||
export function refreshJobs() {
|
||||
if (!usePaging) {
|
||||
jobs = []; // Empty Joblist before refresh, prevents infinite buildup
|
||||
paging = { itemsPerPage: 10, page: 1 };
|
||||
}
|
||||
jobsStore = queryStore({
|
||||
let jobsStore = $derived(queryStore({
|
||||
client: client,
|
||||
query: query,
|
||||
variables: { paging, sorting, filter },
|
||||
requestPolicy: "network-only",
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
/* Effects */
|
||||
$effect(() => {
|
||||
if (!usePaging) {
|
||||
window.addEventListener('scroll', () => {
|
||||
let {
|
||||
scrollTop,
|
||||
scrollHeight,
|
||||
clientHeight
|
||||
} = document.documentElement;
|
||||
|
||||
// Add 100 px offset to trigger load earlier
|
||||
if (scrollTop + clientHeight >= scrollHeight - 100 && $jobsStore?.data?.jobs?.hasNextPage) {
|
||||
page += 1
|
||||
};
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
//Triggers
|
||||
filter
|
||||
sorting
|
||||
// Reset Continous Jobs
|
||||
if (!usePaging) {
|
||||
page = 1;
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if ($jobsStore?.data) {
|
||||
untrack(() => {
|
||||
handleJobs($jobsStore.data.jobs.items);
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
/* Functions */
|
||||
// Force refresh list with existing unchanged variables
|
||||
export function refreshJobs() {
|
||||
if (usePaging) {
|
||||
paging = {...paging}
|
||||
} else {
|
||||
page = 1;
|
||||
}
|
||||
};
|
||||
|
||||
export function refreshAllMetrics() {
|
||||
// Refresh Job Metrics (Downstream will only query for running jobs)
|
||||
@ -175,11 +179,48 @@
|
||||
if (minRunningFor && minRunningFor > 0) {
|
||||
filters.push({ minRunningFor });
|
||||
}
|
||||
filter = filters;
|
||||
filter = [...filters];
|
||||
}
|
||||
page = 1;
|
||||
paging = paging = { page, itemsPerPage };
|
||||
}
|
||||
};
|
||||
|
||||
function handleJobs(newJobs) {
|
||||
if (newJobs) {
|
||||
if (usePaging) {
|
||||
// console.log('New Paging', $state.snapshot(paging))
|
||||
jobs = [...newJobs]
|
||||
} else {
|
||||
if ($state.snapshot(page) == 1) {
|
||||
// console.log('Page 1 Reset', [...newJobs])
|
||||
jobs = [...newJobs]
|
||||
} else {
|
||||
// console.log('Add Jobs', $state.snapshot(jobs), [...newJobs])
|
||||
jobs = jobs.concat([...newJobs])
|
||||
}
|
||||
}
|
||||
matchedListJobs = $jobsStore.data.jobs.count;
|
||||
} else {
|
||||
matchedListJobs = -1
|
||||
}
|
||||
};
|
||||
|
||||
function updateConfiguration(value, newPage) {
|
||||
updateConfigurationMutation({
|
||||
name: "plot_list_jobsPerPage",
|
||||
value: value.toString(),
|
||||
}).subscribe((res) => {
|
||||
if (res.fetching === false && !res.error) {
|
||||
itemsPerPage = value
|
||||
page = newPage // Trigger reload of jobList
|
||||
} else if (res.fetching === false && res.error) {
|
||||
throw res.error;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function getUnit(m) {
|
||||
const rawUnit = globalMetrics.find((gm) => gm.name === m)?.unit
|
||||
return (rawUnit?.prefix ? rawUnit.prefix : "") + (rawUnit?.base ? rawUnit.base : "")
|
||||
};
|
||||
|
||||
const updateConfigurationMutation = ({ name, value }) => {
|
||||
return mutationStore({
|
||||
@ -193,52 +234,11 @@
|
||||
});
|
||||
};
|
||||
|
||||
function updateConfiguration(value, page) {
|
||||
updateConfigurationMutation({
|
||||
name: "plot_list_jobsPerPage",
|
||||
value: value,
|
||||
}).subscribe((res) => {
|
||||
if (res.fetching === false && !res.error) {
|
||||
jobs = [] // Empty List
|
||||
paging = { itemsPerPage: value, page: page }; // Trigger reload of jobList
|
||||
} else if (res.fetching === false && res.error) {
|
||||
throw res.error;
|
||||
}
|
||||
});
|
||||
const equalsCheck = (a, b) => {
|
||||
return JSON.stringify(a) === JSON.stringify(b);
|
||||
}
|
||||
|
||||
if (!usePaging) {
|
||||
window.addEventListener('scroll', () => {
|
||||
let {
|
||||
scrollTop,
|
||||
scrollHeight,
|
||||
clientHeight
|
||||
} = document.documentElement;
|
||||
|
||||
// Add 100 px offset to trigger load earlier
|
||||
if (scrollTop + clientHeight >= scrollHeight - 100 && $jobsStore.data != null && $jobsStore.data.jobs.hasNextPage) {
|
||||
let pendingPaging = { ...paging }
|
||||
pendingPaging.page += 1
|
||||
paging = pendingPaging
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
let plotWidth = null;
|
||||
let tableWidth = null;
|
||||
let jobInfoColumnWidth = 250;
|
||||
|
||||
$: if (showFootprint) {
|
||||
plotWidth = Math.floor(
|
||||
(tableWidth - jobInfoColumnWidth) / (metrics.length + 1) - 10,
|
||||
);
|
||||
} else {
|
||||
plotWidth = Math.floor(
|
||||
(tableWidth - jobInfoColumnWidth) / metrics.length - 10,
|
||||
);
|
||||
}
|
||||
|
||||
let headerPaddingTop = 0;
|
||||
/* Init Header */
|
||||
stickyHeader(
|
||||
".cc-table-wrapper > table.table >thead > tr > th.position-sticky:nth-child(1)",
|
||||
(x) => (headerPaddingTop = x),
|
||||
@ -291,9 +291,9 @@
|
||||
</tr>
|
||||
{:else}
|
||||
{#each jobs as job (job.id)}
|
||||
<JobListRow bind:triggerMetricRefresh {job} {metrics} {plotWidth} {showFootprint} previousSelect={selectedJobs.includes(job.id)}
|
||||
on:select-job={({detail}) => selectedJobs = [...selectedJobs, detail]}
|
||||
on:unselect-job={({detail}) => selectedJobs = selectedJobs.filter(item => item !== detail)}
|
||||
<JobListRow {triggerMetricRefresh} {job} {metrics} {plotWidth} {showFootprint} previousSelect={selectedJobs.includes(job.id)}
|
||||
selectJob={(detail) => selectedJobs = [...selectedJobs, detail]}
|
||||
unselectJob={(detail) => selectedJobs = selectedJobs.filter(item => item !== detail)}
|
||||
/>
|
||||
{:else}
|
||||
<tr>
|
||||
@ -323,10 +323,10 @@
|
||||
totalItems={matchedListJobs}
|
||||
updatePaging={(detail) => {
|
||||
if (detail.itemsPerPage != itemsPerPage) {
|
||||
updateConfiguration(detail.itemsPerPage.toString(), detail.page);
|
||||
updateConfiguration(detail.itemsPerPage, detail.page);
|
||||
} else {
|
||||
jobs = []
|
||||
paging = { itemsPerPage: detail.itemsPerPage, page: detail.page };
|
||||
itemsPerPage = detail.itemsPerPage
|
||||
page = detail.page
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@ -6,32 +6,25 @@
|
||||
- `items [Any]`: List of plot components to render
|
||||
-->
|
||||
|
||||
<script>
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
<script>
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
|
||||
export let itemsPerRow
|
||||
export let items
|
||||
|
||||
/* Migtation Notes
|
||||
* Requirements
|
||||
* - Parent Components must be already Migrated
|
||||
* - TODO: Job.root.svelte, Node.root.svelte
|
||||
* - DONE: Analysis, Status, User
|
||||
*
|
||||
* How-To
|
||||
* - Define "Plot-Slotcode" as SV5 Snippet with argument "item" in parent (!)
|
||||
* - Pass new snippet as argument/prop to here
|
||||
* - @render snippet in items-loop with argument == item
|
||||
*/
|
||||
/* Svelte 5 Props */
|
||||
let {
|
||||
items,
|
||||
itemsPerRow,
|
||||
gridContent
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<Row cols={{ xs: 1, sm: 2, md: 3, lg: itemsPerRow}}>
|
||||
{#each items as item}
|
||||
<Col class="px-1">
|
||||
<slot {item}/>
|
||||
<!-- Note: Ignore '@' Error in IDE -->
|
||||
{@render gridContent(item)}
|
||||
</Col>
|
||||
{/each}
|
||||
</Row>
|
||||
|
@ -20,7 +20,7 @@
|
||||
username = null,
|
||||
authlevel= null,
|
||||
roles = null,
|
||||
isSelected = null,
|
||||
isSelected = $bindable(),
|
||||
showSelect = false,
|
||||
} = $props();
|
||||
|
||||
@ -89,10 +89,8 @@
|
||||
}}>
|
||||
{#if isSelected}
|
||||
<Icon name="check-square"/>
|
||||
{:else if isSelected == false}
|
||||
<Icon name="square"/>
|
||||
{:else}
|
||||
<Icon name="plus-square-dotted" />
|
||||
{:else }
|
||||
<Icon name="plus-square-dotted"/>
|
||||
{/if}
|
||||
</Button>
|
||||
<Tooltip
|
||||
|
@ -12,39 +12,37 @@
|
||||
|
||||
<script>
|
||||
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
||||
import { getContext, createEventDispatcher } from "svelte";
|
||||
import { getContext } from "svelte";
|
||||
import { Card, Spinner } from "@sveltestrap/sveltestrap";
|
||||
import { maxScope, checkMetricDisabled } from "../utils.js";
|
||||
import JobInfo from "./JobInfo.svelte";
|
||||
import MetricPlot from "../plots/MetricPlot.svelte";
|
||||
import JobFootprint from "../helper/JobFootprint.svelte";
|
||||
|
||||
export let job;
|
||||
export let metrics;
|
||||
export let plotWidth;
|
||||
export let plotHeight = 275;
|
||||
export let showFootprint;
|
||||
export let triggerMetricRefresh = false;
|
||||
export let previousSelect = false;
|
||||
/* Svelte 5 Props */
|
||||
let {
|
||||
triggerMetricRefresh = false,
|
||||
job,
|
||||
metrics,
|
||||
plotWidth,
|
||||
plotHeight = 275,
|
||||
showFootprint,
|
||||
previousSelect = false,
|
||||
selectJob,
|
||||
unselectJob
|
||||
} = $props();
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const resampleConfig = getContext("resampling") || null;
|
||||
const resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0;
|
||||
|
||||
let { id } = job;
|
||||
let scopes = job.numNodes == 1
|
||||
? job.numAcc >= 1
|
||||
/* Const Init */
|
||||
const client = getContextClient();
|
||||
const jobId = job.id;
|
||||
const cluster = getContext("clusters");
|
||||
const scopes = (job.numNodes == 1)
|
||||
? (job.numAcc >= 1)
|
||||
? ["core", "accelerator"]
|
||||
: ["core"]
|
||||
: ["node"];
|
||||
let selectedResolution = resampleDefault;
|
||||
let zoomStates = {};
|
||||
let thresholdStates = {};
|
||||
|
||||
$: isSelected = previousSelect || null;
|
||||
|
||||
const cluster = getContext("clusters").find((c) => c.name == job.cluster);
|
||||
const client = getContextClient();
|
||||
const resampleConfig = getContext("resampling") || null;
|
||||
const resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0;
|
||||
const query = gql`
|
||||
query ($id: ID!, $metrics: [String!]!, $scopes: [MetricScope!]!, $selectedResolution: Int) {
|
||||
jobMetrics(id: $id, metrics: $metrics, scopes: $scopes, resolution: $selectedResolution) {
|
||||
@ -77,52 +75,59 @@
|
||||
}
|
||||
`;
|
||||
|
||||
function handleZoom(detail, metric) {
|
||||
if ( // States have to differ, causes deathloop if just set
|
||||
(zoomStates[metric]?.x?.min !== detail?.lastZoomState?.x?.min) &&
|
||||
(zoomStates[metric]?.y?.max !== detail?.lastZoomState?.y?.max)
|
||||
) {
|
||||
zoomStates[metric] = {...detail.lastZoomState}
|
||||
/* State Init */
|
||||
let selectedResolution = $state(resampleDefault);
|
||||
let zoomStates = $state({});
|
||||
let thresholdStates = $state({});
|
||||
|
||||
/* Derived */
|
||||
let isSelected = $derived(previousSelect);
|
||||
let metricsQuery = $derived(queryStore({
|
||||
client: client,
|
||||
query: query,
|
||||
variables: { id: jobId, metrics, scopes, selectedResolution },
|
||||
})
|
||||
);
|
||||
|
||||
/* Effects */
|
||||
$effect(() => {
|
||||
if (job.state === 'running' && triggerMetricRefresh === true) {
|
||||
refreshMetrics();
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
$effect(() => {
|
||||
if (isSelected == true && previousSelect == false) {
|
||||
selectJob(jobId)
|
||||
} else if (isSelected == false && previousSelect == true) {
|
||||
unselectJob(jobId)
|
||||
}
|
||||
});
|
||||
|
||||
if (detail?.newRes) { // Triggers GQL
|
||||
selectedResolution = detail.newRes
|
||||
/* Functions */
|
||||
function handleZoom(detail, metric) {
|
||||
// Buffer last zoom state to allow seamless zoom on rerender
|
||||
// console.log('Update zoomState for/with:', metric, {...detail.lastZoomState})
|
||||
zoomStates[metric] = detail?.lastZoomState ? {...detail.lastZoomState} : null;
|
||||
// Handle to correctly reset on summed metric scope change
|
||||
// console.log('Update thresholdState for/with:', metric, detail.lastThreshold)
|
||||
thresholdStates[metric] = detail?.lastThreshold ? detail.lastThreshold : null;
|
||||
// Triggers GQL
|
||||
if (detail?.newRes) {
|
||||
// console.log('Update selectedResolution for/with:', metric, detail.newRes)
|
||||
selectedResolution = detail.newRes;
|
||||
}
|
||||
}
|
||||
|
||||
$: metricsQuery = queryStore({
|
||||
client: client,
|
||||
query: query,
|
||||
variables: { id, metrics, scopes, selectedResolution },
|
||||
});
|
||||
|
||||
function refreshMetrics() {
|
||||
metricsQuery = queryStore({
|
||||
client: client,
|
||||
query: query,
|
||||
variables: { id, metrics, scopes, selectedResolution },
|
||||
variables: { id: jobId, metrics, scopes, selectedResolution },
|
||||
// requestPolicy: 'network-only' // use default cache-first for refresh
|
||||
});
|
||||
}
|
||||
|
||||
$: if (job.state === 'running' && triggerMetricRefresh === true) {
|
||||
refreshMetrics();
|
||||
}
|
||||
|
||||
$: if (isSelected == true && previousSelect == false) {
|
||||
dispatch("select-job", job.id)
|
||||
} else if (isSelected == false && previousSelect == true) {
|
||||
dispatch("unselect-job", job.id)
|
||||
}
|
||||
|
||||
// Helper
|
||||
const selectScope = (jobMetrics) =>
|
||||
jobMetrics.reduce(
|
||||
(a, b) =>
|
||||
@ -146,6 +151,7 @@
|
||||
.map((jobMetric) => {
|
||||
if (jobMetric.data) {
|
||||
return {
|
||||
name: jobMetric.data.name,
|
||||
disabled: checkMetricDisabled(
|
||||
jobMetric.data.name,
|
||||
job.cluster,
|
||||
@ -157,7 +163,6 @@
|
||||
return jobMetric;
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<tr>
|
||||
@ -191,19 +196,19 @@
|
||||
/>
|
||||
</td>
|
||||
{/if}
|
||||
{#each sortAndSelectScope($metricsQuery.data.jobMetrics) as metric, i (metric || i)}
|
||||
{#each sortAndSelectScope($metricsQuery.data.jobMetrics) as metric, i (metric?.name || i)}
|
||||
<td>
|
||||
<!-- Subluster Metricconfig remove keyword for jobtables (joblist main, user joblist, project joblist) to be used here as toplevel case-->
|
||||
{#if metric.disabled == false && metric.data}
|
||||
<MetricPlot
|
||||
on:zoom={({detail}) => { handleZoom(detail, metric.data.name) }}
|
||||
on:zoom={({detail}) => handleZoom(detail, metric.data.name)}
|
||||
height={plotHeight}
|
||||
timestep={metric.data.metric.timestep}
|
||||
scope={metric.data.scope}
|
||||
series={metric.data.metric.series}
|
||||
statisticsSeries={metric.data.metric.statisticsSeries}
|
||||
metric={metric.data.name}
|
||||
{cluster}
|
||||
cluster={cluster.find((c) => c.name == job.cluster)}
|
||||
subCluster={job.subCluster}
|
||||
isShared={job.exclusive != 1}
|
||||
numhwthreads={job.numHWThreads}
|
||||
|
@ -440,7 +440,7 @@
|
||||
/* IF Zoom Enabled */
|
||||
if (resampleConfig) {
|
||||
u.over.addEventListener("dblclick", (e) => {
|
||||
// console.log('Dispatch Reset')
|
||||
// console.log('Dispatch: Zoom Reset')
|
||||
dispatch('zoom', {
|
||||
lastZoomState: {
|
||||
x: { time: false },
|
||||
@ -506,7 +506,7 @@
|
||||
});
|
||||
// Prevents non-required dispatches
|
||||
if (timestep !== closest) {
|
||||
// console.log('Dispatch Zoom with Res from / to', timestep, closest)
|
||||
// console.log('Dispatch: Zoom with Res from / to', timestep, closest)
|
||||
dispatch('zoom', {
|
||||
newRes: closest,
|
||||
lastZoomState: u?.scales,
|
||||
@ -514,6 +514,7 @@
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// console.log('Dispatch: Zoom Update States')
|
||||
dispatch('zoom', {
|
||||
lastZoomState: u?.scales,
|
||||
lastThreshold: thresholds?.normal
|
||||
|
@ -182,6 +182,6 @@
|
||||
|
||||
</script>
|
||||
|
||||
<div class="cc-plot">
|
||||
<canvas bind:this={canvasElement} width="{width}" height="{height}"></canvas>
|
||||
<div class="cc-plot" bind:clientWidth={width}>
|
||||
<canvas bind:this={canvasElement} {width} {height}></canvas>
|
||||
</div>
|
||||
|
@ -18,6 +18,7 @@ Changes #2: Rewritten for Svelte 5, removed bodyHandler
|
||||
-->
|
||||
|
||||
<script>
|
||||
/* Svelte 5 Props */
|
||||
let {
|
||||
sliderMin,
|
||||
sliderMax,
|
||||
@ -26,6 +27,7 @@ Changes #2: Rewritten for Svelte 5, removed bodyHandler
|
||||
changeRange
|
||||
} = $props();
|
||||
|
||||
/* State Init */
|
||||
let pendingValues = $state([fromPreset, toPreset]);
|
||||
let sliderFrom = $state(Math.max(((fromPreset == null ? sliderMin : fromPreset) - sliderMin) / (sliderMax - sliderMin), 0.));
|
||||
let sliderTo = $state(Math.min(((toPreset == null ? sliderMin : toPreset) - sliderMin) / (sliderMax - sliderMin), 1.));
|
||||
@ -34,7 +36,10 @@ Changes #2: Rewritten for Svelte 5, removed bodyHandler
|
||||
let leftHandle = $state();
|
||||
let sliderMain = $state();
|
||||
|
||||
/* Var Init */
|
||||
let timeoutId = null;
|
||||
|
||||
/* Functions */
|
||||
function queueChangeEvent() {
|
||||
if (timeoutId !== null) {
|
||||
clearTimeout(timeoutId)
|
||||
|
@ -31,33 +31,22 @@
|
||||
} from "../generic/utils.js";
|
||||
import Timeseries from "../generic/plots/MetricPlot.svelte";
|
||||
|
||||
export let job;
|
||||
export let metricName;
|
||||
export let metricUnit;
|
||||
export let nativeScope;
|
||||
export let scopes;
|
||||
export let rawData;
|
||||
export let isShared = false;
|
||||
/* Svelte 5 Props */
|
||||
let {
|
||||
job,
|
||||
metricName,
|
||||
metricUnit,
|
||||
nativeScope,
|
||||
presetScopes,
|
||||
isShared = false,
|
||||
} = $props();
|
||||
|
||||
/* Const Init */
|
||||
const client = getContextClient();
|
||||
const statsPattern = /(.*)-stat$/;
|
||||
const resampleConfig = getContext("resampling") || null;
|
||||
const resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0;
|
||||
|
||||
let selectedHost = null;
|
||||
let error = null;
|
||||
let selectedScope = minScope(scopes);
|
||||
let selectedResolution = null;
|
||||
let pendingResolution = resampleDefault;
|
||||
let selectedScopeIndex = scopes.findIndex((s) => s == minScope(scopes));
|
||||
let patternMatches = false;
|
||||
let nodeOnly = false; // If, after load-all, still only node scope returned
|
||||
let statsSeries = rawData.map((data) => data?.statisticsSeries ? data.statisticsSeries : null);
|
||||
let zoomState = null;
|
||||
let pendingZoomState = null;
|
||||
let thresholdState = null;
|
||||
|
||||
const statsPattern = /(.*)-stat$/;
|
||||
const unit = (metricUnit?.prefix ? metricUnit.prefix : "") + (metricUnit?.base ? metricUnit.base : "");
|
||||
const client = getContextClient();
|
||||
const subQuery = gql`
|
||||
query ($dbid: ID!, $selectedMetrics: [String!]!, $selectedScopes: [MetricScope!]!, $selectedResolution: Int) {
|
||||
singleUpdate: jobMetrics(id: $dbid, metrics: $selectedMetrics, scopes: $selectedScopes, resolution: $selectedResolution) {
|
||||
@ -90,84 +79,68 @@
|
||||
}
|
||||
`;
|
||||
|
||||
function handleZoom(detail) {
|
||||
if ( // States have to differ, causes deathloop if just set
|
||||
(pendingZoomState?.x?.min !== detail?.lastZoomState?.x?.min) &&
|
||||
(pendingZoomState?.y?.max !== detail?.lastZoomState?.y?.max)
|
||||
) {
|
||||
pendingZoomState = {...detail.lastZoomState};
|
||||
}
|
||||
/* State Init */
|
||||
let requestedScopes = $state(presetScopes);
|
||||
let selectedResolution = $state(resampleDefault);
|
||||
|
||||
if (detail?.lastThreshold) { // Handle to correctly reset on summed metric scope change
|
||||
thresholdState = detail.lastThreshold;
|
||||
} else {
|
||||
thresholdState = null;
|
||||
}
|
||||
let selectedHost = $state(null);
|
||||
let zoomState = $state(null);
|
||||
let thresholdState = $state(null);
|
||||
|
||||
if (detail?.newRes) { // Triggers GQL
|
||||
pendingResolution = detail.newRes;
|
||||
}
|
||||
};
|
||||
|
||||
let metricData;
|
||||
let selectedScopes = [...scopes];
|
||||
const dbid = job.id;
|
||||
const selectedMetrics = [metricName];
|
||||
|
||||
$: if (selectedScope || pendingResolution) {
|
||||
|
||||
if (resampleConfig && !selectedResolution) {
|
||||
// Skips reactive data load on init || Only if resampling is enabled
|
||||
selectedResolution = Number(pendingResolution)
|
||||
|
||||
} else {
|
||||
if (selectedScope == "load-all") {
|
||||
selectedScopes = [...scopes, "socket", "core", "accelerator"]
|
||||
}
|
||||
|
||||
if (resampleConfig && pendingResolution) {
|
||||
selectedResolution = Number(pendingResolution)
|
||||
}
|
||||
|
||||
metricData = queryStore({
|
||||
client: client,
|
||||
query: subQuery,
|
||||
variables: { dbid, selectedMetrics, selectedScopes, selectedResolution: (resampleConfig ? selectedResolution : 0) },
|
||||
// Never user network-only: causes reactive load-loop!
|
||||
});
|
||||
|
||||
if ($metricData && !$metricData.fetching) {
|
||||
rawData = $metricData.data.singleUpdate.map((x) => x.metric)
|
||||
scopes = $metricData.data.singleUpdate.map((x) => x.scope)
|
||||
statsSeries = rawData.map((data) => data?.statisticsSeries ? data.statisticsSeries : null)
|
||||
|
||||
// Keep Zoomlevel if ResChange By Zoom
|
||||
if (pendingZoomState) {
|
||||
zoomState = {...pendingZoomState}
|
||||
}
|
||||
|
||||
// On additional scope request
|
||||
if (selectedScope == "load-all") {
|
||||
// Set selected scope to min of returned scopes
|
||||
selectedScope = minScope(scopes)
|
||||
nodeOnly = (selectedScope == "node") // "node" still only scope after load-all
|
||||
}
|
||||
|
||||
patternMatches = statsPattern.exec(selectedScope)
|
||||
|
||||
if (!patternMatches) {
|
||||
selectedScopeIndex = scopes.findIndex((s) => s == selectedScope);
|
||||
} else {
|
||||
selectedScopeIndex = scopes.findIndex((s) => s == patternMatches[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: data = rawData[selectedScopeIndex];
|
||||
$: series = data?.series?.filter(
|
||||
(series) => selectedHost == null || series.hostname == selectedHost,
|
||||
/* Derived */
|
||||
const metricData = $derived(queryStore({
|
||||
client: client,
|
||||
query: subQuery,
|
||||
variables: {
|
||||
dbid: job.id,
|
||||
selectedMetrics: [metricName],
|
||||
selectedScopes: [...requestedScopes],
|
||||
selectedResolution
|
||||
},
|
||||
// Never user network-only: causes reactive load-loop!
|
||||
})
|
||||
);
|
||||
|
||||
const rawData = $derived($metricData?.data ? $metricData.data.singleUpdate.map((x) => x.metric) : []);
|
||||
const availableScopes = $derived($metricData?.data ? $metricData.data.singleUpdate.map((x) => x.scope) : presetScopes);
|
||||
let selectedScope = $derived(minScope(availableScopes));
|
||||
const patternMatches = $derived(statsPattern.exec(selectedScope));
|
||||
const selectedScopeIndex = $derived.by(() => {
|
||||
if (!patternMatches) {
|
||||
return availableScopes.findIndex((s) => s == selectedScope);
|
||||
} else {
|
||||
return availableScopes.findIndex((s) => s == patternMatches[1]);
|
||||
}
|
||||
});
|
||||
const selectedData = $derived(rawData[selectedScopeIndex]);
|
||||
const selectedSeries = $derived(rawData[selectedScopeIndex]?.series?.filter(
|
||||
(series) => selectedHost == null || series.hostname == selectedHost
|
||||
)
|
||||
);
|
||||
const statsSeries = $derived(rawData.map((rd) => rd?.statisticsSeries ? rd.statisticsSeries : null));
|
||||
|
||||
/* Effect */
|
||||
$effect(() => {
|
||||
// Only triggered once
|
||||
if (selectedScope == "load-all") {
|
||||
requestedScopes = ["node", "socket", "core", "accelerator"];
|
||||
}
|
||||
});
|
||||
|
||||
/* Functions */
|
||||
function handleZoom(detail) {
|
||||
// Buffer last zoom state to allow seamless zoom on rerender
|
||||
// console.log('Update zoomState with:', {...detail.lastZoomState})
|
||||
zoomState = detail?.lastZoomState ? {...detail.lastZoomState} : null;
|
||||
// Handle to correctly reset on summed metric scope change
|
||||
// console.log('Update thresholdState with:', detail.lastThreshold)
|
||||
thresholdState = detail?.lastThreshold ? detail.lastThreshold : null;
|
||||
// Triggers GQL
|
||||
if (detail?.newRes) {
|
||||
// console.log('Update selectedResolution with:', detail.newRes)
|
||||
selectedResolution = detail.newRes;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<InputGroup class="mt-2">
|
||||
@ -175,13 +148,13 @@
|
||||
{metricName} ({unit})
|
||||
</InputGroupText>
|
||||
<select class="form-select" bind:value={selectedScope}>
|
||||
{#each scopes as scope, index}
|
||||
{#each availableScopes as scope, index}
|
||||
<option value={scope}>{scope}</option>
|
||||
{#if statsSeries[index]}
|
||||
<option value={scope + '-stat'}>stats series ({scope})</option>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if scopes.length == 1 && nativeScope != "node" && !nodeOnly}
|
||||
{#if requestedScopes.length == 1 && nativeScope != "node"}
|
||||
<option value={"load-all"}>Load all...</option>
|
||||
{/if}
|
||||
</select>
|
||||
@ -194,37 +167,37 @@
|
||||
</select>
|
||||
{/if}
|
||||
</InputGroup>
|
||||
{#key series}
|
||||
{#if $metricData?.fetching == true}
|
||||
{#key selectedSeries}
|
||||
{#if $metricData.fetching}
|
||||
<Spinner />
|
||||
{:else if error != null}
|
||||
<Card body color="danger">{error.message}</Card>
|
||||
{:else if series != null && !patternMatches}
|
||||
{:else if $metricData.error}
|
||||
<Card body color="danger">{$metricData.error.message}</Card>
|
||||
{:else if selectedSeries != null && !patternMatches}
|
||||
<Timeseries
|
||||
on:zoom={({detail}) => { handleZoom(detail) }}
|
||||
on:zoom={({detail}) => handleZoom(detail)}
|
||||
cluster={job.cluster}
|
||||
subCluster={job.subCluster}
|
||||
timestep={data.timestep}
|
||||
timestep={selectedData.timestep}
|
||||
scope={selectedScope}
|
||||
metric={metricName}
|
||||
numaccs={job.numAcc}
|
||||
numhwthreads={job.numHWThreads}
|
||||
{series}
|
||||
series={selectedSeries}
|
||||
{isShared}
|
||||
{zoomState}
|
||||
{thresholdState}
|
||||
/>
|
||||
{:else if statsSeries[selectedScopeIndex] != null && patternMatches}
|
||||
<Timeseries
|
||||
on:zoom={({detail}) => { handleZoom(detail) }}
|
||||
on:zoom={({detail}) => handleZoom(detail)}
|
||||
cluster={job.cluster}
|
||||
subCluster={job.subCluster}
|
||||
timestep={data.timestep}
|
||||
timestep={selectedData.timestep}
|
||||
scope={selectedScope}
|
||||
metric={metricName}
|
||||
numaccs={job.numAcc}
|
||||
numhwthreads={job.numHWThreads}
|
||||
{series}
|
||||
series={selectedSeries}
|
||||
{isShared}
|
||||
{zoomState}
|
||||
{thresholdState}
|
||||
|
@ -108,7 +108,7 @@
|
||||
}));
|
||||
|
||||
/* Effects */
|
||||
$effect(() => {
|
||||
$effect(() => {
|
||||
if (!usePaging) {
|
||||
window.addEventListener('scroll', () => {
|
||||
let {
|
||||
|
@ -16,28 +16,37 @@
|
||||
CardBody,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputGroupText, } from "@sveltestrap/sveltestrap";
|
||||
InputGroupText,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import {
|
||||
scramble,
|
||||
scrambleNames, } from "../../generic/utils.js";
|
||||
scrambleNames,
|
||||
} from "../../generic/utils.js";
|
||||
|
||||
export let cluster;
|
||||
export let subCluster
|
||||
export let hostname;
|
||||
export let dataHealth;
|
||||
export let nodeJobsData = null;
|
||||
/* Svelte 5 Props */
|
||||
let {
|
||||
cluster,
|
||||
subCluster,
|
||||
hostname,
|
||||
dataHealth,
|
||||
nodeJobsData = null,
|
||||
} = $props();
|
||||
|
||||
/* Const Init */
|
||||
// Not at least one returned, selected metric: NodeHealth warning
|
||||
const healthWarn = !dataHealth.includes(true);
|
||||
// At least one non-returned selected metric: Metric config error?
|
||||
const metricWarn = dataHealth.includes(false);
|
||||
|
||||
let userList;
|
||||
let projectList;
|
||||
$: if (nodeJobsData) {
|
||||
userList = Array.from(new Set(nodeJobsData.jobs.items.map((j) => scrambleNames ? scramble(j.user) : j.user))).sort((a, b) => a.localeCompare(b));
|
||||
projectList = Array.from(new Set(nodeJobsData.jobs.items.map((j) => scrambleNames ? scramble(j.project) : j.project))).sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
/* Derived */
|
||||
const userList = $derived(nodeJobsData
|
||||
? Array.from(new Set(nodeJobsData.jobs.items.map((j) => scrambleNames ? scramble(j.user) : j.user))).sort((a, b) => a.localeCompare(b))
|
||||
: []
|
||||
);
|
||||
const projectList = $derived(nodeJobsData
|
||||
? Array.from(new Set(nodeJobsData.jobs.items.map((j) => scrambleNames ? scramble(j.project) : j.project))).sort((a, b) => a.localeCompare(b))
|
||||
: []);
|
||||
|
||||
</script>
|
||||
|
||||
<Card class="pb-3">
|
||||
|
@ -18,10 +18,14 @@
|
||||
import MetricPlot from "../../generic/plots/MetricPlot.svelte";
|
||||
import NodeInfo from "./NodeInfo.svelte";
|
||||
|
||||
export let cluster;
|
||||
export let nodeData;
|
||||
export let selectedMetrics;
|
||||
/* Svelte 5 Props */
|
||||
let {
|
||||
cluster,
|
||||
nodeData,
|
||||
selectedMetrics,
|
||||
} = $props();
|
||||
|
||||
/* Const Init */
|
||||
const client = getContextClient();
|
||||
const paging = { itemsPerPage: 50, page: 1 };
|
||||
const sorting = { field: "startTime", type: "col", order: "DESC" };
|
||||
@ -30,7 +34,6 @@
|
||||
{ node: { contains: nodeData.host } },
|
||||
{ state: ["running"] },
|
||||
];
|
||||
|
||||
const nodeJobsQuery = gql`
|
||||
query (
|
||||
$filter: [JobFilter!]!
|
||||
@ -53,13 +56,19 @@
|
||||
}
|
||||
`;
|
||||
|
||||
$: nodeJobsData = queryStore({
|
||||
client: client,
|
||||
query: nodeJobsQuery,
|
||||
variables: { paging, sorting, filter },
|
||||
});
|
||||
/* Derived */
|
||||
const nodeJobsData = $derived(queryStore({
|
||||
client: client,
|
||||
query: nodeJobsQuery,
|
||||
variables: { paging, sorting, filter },
|
||||
})
|
||||
);
|
||||
|
||||
// Helper
|
||||
let extendedLegendData = $derived($nodeJobsData?.data ? buildExtendedLegend() : null);
|
||||
let refinedData = $derived(nodeData?.metrics ? sortAndSelectScope(nodeData.metrics) : null);
|
||||
let dataHealth = $derived(refinedData.filter((rd) => rd.disabled === false).map((enabled) => (enabled.data.metric.series.length > 0)));
|
||||
|
||||
/* Functions */
|
||||
const selectScope = (nodeMetrics) =>
|
||||
nodeMetrics.reduce(
|
||||
(a, b) =>
|
||||
@ -89,15 +98,8 @@
|
||||
}
|
||||
});
|
||||
|
||||
let refinedData;
|
||||
let dataHealth;
|
||||
$: if (nodeData?.metrics) {
|
||||
refinedData = sortAndSelectScope(nodeData?.metrics)
|
||||
dataHealth = refinedData.filter((rd) => rd.disabled === false).map((enabled) => (enabled.data.metric.series.length > 0))
|
||||
}
|
||||
|
||||
let extendedLegendData = null;
|
||||
$: if ($nodeJobsData?.data) {
|
||||
function buildExtendedLegend() {
|
||||
let pendingExtendedLegendData = null
|
||||
// Build Extended for allocated nodes [Commented: Only Build extended Legend For Shared Nodes]
|
||||
if ($nodeJobsData.data.jobs.count >= 1) { // "&& !$nodeJobsData.data.jobs.items[0].exclusive)"
|
||||
const accSet = Array.from(new Set($nodeJobsData.data.jobs.items
|
||||
@ -107,11 +109,11 @@
|
||||
)
|
||||
)).flat(2)
|
||||
|
||||
extendedLegendData = {}
|
||||
pendingExtendedLegendData = {};
|
||||
for (const accId of accSet) {
|
||||
const matchJob = $nodeJobsData.data.jobs.items.find((i) => i.resources.find((r) => r.accelerators.includes(accId)))
|
||||
const matchUser = matchJob?.user ? matchJob.user : null
|
||||
extendedLegendData[accId] = {
|
||||
pendingExtendedLegendData[accId] = {
|
||||
user: (scrambleNames && matchUser)
|
||||
? scramble(matchUser)
|
||||
: (matchUser ? matchUser : '-'),
|
||||
@ -120,6 +122,7 @@
|
||||
}
|
||||
// Theoretically extendable for hwthreadIDs
|
||||
}
|
||||
return pendingExtendedLegendData;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user