Merge branch 'dev' into port-to-cclib

This commit is contained in:
Jan Eitzinger 2025-06-30 12:09:28 +02:00
commit 64da28e814
18 changed files with 566 additions and 567 deletions

View File

@ -562,8 +562,20 @@
</Row> </Row>
<Row> <Row>
<Col> <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 <PlotGrid
let:item
items={metricsInHistograms.map((metric) => ({ items={metricsInHistograms.map((metric) => ({
metric, metric,
...binsFromFootprint( ...binsFromFootprint(
@ -576,17 +588,8 @@
), ),
}))} }))}
itemsPerRow={ccconfig.plot_view_plotsPerRow} itemsPerRow={ccconfig.plot_view_plotsPerRow}
> gridContent={histoGridContent}
<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>
</Col> </Col>
</Row> </Row>
<br /> <br />
@ -604,9 +607,19 @@
</Row> </Row>
<Row> <Row>
<Col> <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 <PlotGrid
let:item
let:width
items={metricsInScatterplots.map(([m1, m2]) => ({ items={metricsInScatterplots.map(([m1, m2]) => ({
m1, m1,
f1: $footprintsQuery.data.footprints.metrics.find( f1: $footprintsQuery.data.footprints.metrics.find(
@ -618,18 +631,8 @@
).data, ).data,
}))} }))}
itemsPerRow={ccconfig.plot_view_plotsPerRow} itemsPerRow={ccconfig.plot_view_plotsPerRow}
> gridContent={metricsGridContent}
<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>
</Col> </Col>
</Row> </Row>
{/if} {/if}

View File

@ -9,10 +9,11 @@
--> -->
<script> <script>
import { getContext } from "svelte";
import { import {
queryStore, queryStore,
gql, gql,
getContextClient getContextClient,
} from "@urql/svelte"; } from "@urql/svelte";
import { import {
Row, Row,
@ -26,7 +27,6 @@
CardTitle, CardTitle,
Button, Button,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { getContext } from "svelte";
import { import {
init, init,
groupByScope, groupByScope,
@ -42,29 +42,16 @@
import PlotGrid from "./generic/PlotGrid.svelte"; import PlotGrid from "./generic/PlotGrid.svelte";
import StatsTab from "./job/StatsTab.svelte"; import StatsTab from "./job/StatsTab.svelte";
export let dbid; /* Svelte 5 Props */
export let username; let {
export let authlevel; dbid,
export let roles; username,
authlevel,
// Setup General roles
} = $props();
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)
/* Const Init */
// Important: init() needs to be first const declaration or contextclient will not be initialized before "const client = ..."
const { query: initq } = init(` const { query: initq } = init(`
job(id: "${dbid}") { job(id: "${dbid}") {
id, jobId, user, project, cluster, startTime, id, jobId, user, project, cluster, startTime,
@ -80,51 +67,106 @@
energyFootprint { hardware, metric, value } energyFootprint { hardware, metric, value }
} }
`); `);
const client = getContextClient(); 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` const query = gql`
query ($dbid: ID!, $selectedMetrics: [String!]!, $selectedScopes: [MetricScope!]!) { query ($dbid: ID!, $selectedMetrics: [String!]!, $selectedScopes: [MetricScope!]!) {
jobMetrics(id: $dbid, metrics: $selectedMetrics, scopes: $selectedScopes) { scopedJobStats(id: $dbid, metrics: $selectedMetrics, scopes: $selectedScopes) {
name name
scope scope
metric { stats {
unit {
prefix
base
}
timestep
statisticsSeries {
min
mean
median
max
}
series {
hostname hostname
id
data
statistics {
min
avg
max
}
}
} }
} }
} }
`; `;
$: jobMetrics = queryStore({ /* 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, client: client,
query: query, query: query,
variables: { dbid, selectedMetrics, selectedScopes }, 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 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")(() => { getContext("on-init")(() => {
let job = $initq.data.job; let job = $initq.data.job;
if (!job) return; if (!job) return;
const pendingMetrics = ( const pendingMetrics = (
ccconfig[`job_view_selectedMetrics:${job.cluster}:${job.subCluster}`] || ccconfig[`job_view_selectedMetrics:${job.cluster}:${job.subCluster}`] ||
ccconfig[`job_view_selectedMetrics:${job.cluster}`] ccconfig[`job_view_selectedMetrics:${job.cluster}`]
@ -154,52 +196,7 @@
selectedScopes = [...new Set(pendingScopes)]; selectedScopes = [...new Set(pendingScopes)];
}); });
// Interactive Document Title /* Functions */
$: 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
const orderAndMap = (grouped, selectedMetrics) => const orderAndMap = (grouped, selectedMetrics) =>
selectedMetrics.map((metric) => ({ selectedMetrics.map((metric) => ({
metric: metric, metric: metric,
@ -293,7 +290,7 @@
<Row class="mb-2"> <Row class="mb-2">
{#if $initq?.data} {#if $initq?.data}
<Col xs="auto"> <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) Select Metrics (Selected {selectedMetrics.length} of {totalMetrics} available)
</Button> </Button>
</Col> </Col>
@ -317,15 +314,9 @@
<Spinner secondary /> <Spinner secondary />
</Col> </Col>
</Row> </Row>
{:else if $initq?.data && $jobMetrics?.data?.jobMetrics} {:else if $initq?.data && $jobMetrics?.data?.scopedJobStats}
<PlotGrid <!-- Note: Ignore '#snippet' Error in IDE -->
let:item {#snippet gridContent(item)}
items={orderAndMap(
groupByScope($jobMetrics.data.jobMetrics),
selectedMetrics,
)}
itemsPerRow={ccconfig.plot_view_plotsPerRow}
>
{#if item.data} {#if item.data}
<Metric <Metric
bind:this={plots[item.metric]} bind:this={plots[item.metric]}
@ -333,8 +324,7 @@
metricName={item.metric} metricName={item.metric}
metricUnit={$initq.data.globalMetrics.find((gm) => gm.name == item.metric)?.unit} metricUnit={$initq.data.globalMetrics.find((gm) => gm.name == item.metric)?.unit}
nativeScope={$initq.data.globalMetrics.find((gm) => gm.name == item.metric)?.scope} nativeScope={$initq.data.globalMetrics.find((gm) => gm.name == item.metric)?.scope}
rawData={item.data.map((x) => x.metric)} presetScopes={item.data.map((x) => x.scope)}
scopes={item.data.map((x) => x.scope)}
isShared={$initq.data.job.exclusive != 1} isShared={$initq.data.job.exclusive != 1}
/> />
{:else if item.disabled == true} {:else if item.disabled == true}
@ -357,7 +347,16 @@
</CardBody> </CardBody>
</Card> </Card>
{/if} {/if}
</PlotGrid> {/snippet}
<PlotGrid
items={orderAndMap(
groupByScope($jobMetrics.data.scopedJobStats),
selectedMetrics,
)}
itemsPerRow={ccconfig.plot_view_plotsPerRow}
{gridContent}
/>
{/if} {/if}
</CardBody> </CardBody>
</Card> </Card>

View File

@ -178,18 +178,18 @@
{#if !showCompare} {#if !showCompare}
<JobList <JobList
bind:this={jobList} bind:this={jobList}
bind:metrics
bind:sorting
bind:matchedListJobs bind:matchedListJobs
bind:showFootprint
bind:selectedJobs bind:selectedJobs
{metrics}
{sorting}
{showFootprint}
{filterBuffer} {filterBuffer}
/> />
{:else} {:else}
<JobCompare <JobCompare
bind:this={jobCompare} bind:this={jobCompare}
bind:metrics
bind:matchedCompareJobs bind:matchedCompareJobs
{metrics}
{filterBuffer} {filterBuffer}
/> />
{/if} {/if}
@ -201,7 +201,8 @@
presetSorting={sorting} presetSorting={sorting}
applySorting={(newSort) => applySorting={(newSort) =>
sorting = {...newSort} sorting = {...newSort}
}/> }
/>
<MetricSelection <MetricSelection
bind:isOpen={isMetricsSelectionOpen} bind:isOpen={isMetricsSelectionOpen}

View File

@ -204,20 +204,8 @@
{:else if $nodeMetricsData.fetching || $initq.fetching} {:else if $nodeMetricsData.fetching || $initq.fetching}
<Spinner /> <Spinner />
{:else} {:else}
<PlotGrid <!-- Note: Ignore '#snippet' Error in IDE -->
let:item {#snippet gridContent(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))}
>
<h4 style="text-align: center; padding-top:15px;"> <h4 style="text-align: center; padding-top:15px;">
{item.name} {item.name}
{systemUnits[item.name] ? "(" + systemUnits[item.name] + ")" : ""} {systemUnits[item.name] ? "(" + systemUnits[item.name] + ")" : ""}
@ -246,7 +234,22 @@
>No dataset returned for <code>{item.name}</code></Card >No dataset returned for <code>{item.name}</code></Card
> >
{/if} {/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} {/if}
</Col> </Col>
</Row> </Row>

View File

@ -676,12 +676,8 @@
<!-- Selectable Stats as Histograms : Average Values of Running Jobs --> <!-- Selectable Stats as Histograms : Average Values of Running Jobs -->
{#if selectedHistograms} {#if selectedHistograms}
{#key $mainQuery.data.stats[0].histMetrics} <!-- Note: Ignore '#snippet' Error in IDE -->
<PlotGrid {#snippet gridContent(item)}
let:item
items={$mainQuery.data.stats[0].histMetrics}
itemsPerRow={2}
>
<Histogram <Histogram
data={convert2uplot(item.data)} data={convert2uplot(item.data)}
usesBins={true} usesBins={true}
@ -691,7 +687,14 @@
ylabel="Number of Jobs" ylabel="Number of Jobs"
yunit="Jobs" yunit="Jobs"
/> />
</PlotGrid> {/snippet}
{#key $mainQuery.data.stats[0].histMetrics}
<PlotGrid
items={$mainQuery.data.stats[0].histMetrics}
itemsPerRow={2}
{gridContent}
/>
{/key} {/key}
{/if} {/if}
{/if} {/if}

View File

@ -335,12 +335,8 @@
</Row> </Row>
{:else} {:else}
<hr class="my-2"/> <hr class="my-2"/>
{#key $stats.data.jobsStatistics[0].histMetrics} <!-- Note: Ignore '#snippet' Error in IDE -->
<PlotGrid {#snippet gridContent(item)}
let:item
items={$stats.data.jobsStatistics[0].histMetrics}
itemsPerRow={3}
>
<Histogram <Histogram
data={convert2uplot(item.data)} data={convert2uplot(item.data)}
title="Distribution of '{item.metric} ({item.stat})' footprints" title="Distribution of '{item.metric} ({item.stat})' footprints"
@ -350,7 +346,14 @@
yunit="Jobs" yunit="Jobs"
usesBins usesBins
/> />
</PlotGrid> {/snippet}
{#key $stats.data.jobsStatistics[0].histMetrics}
<PlotGrid
items={$stats.data.jobsStatistics[0].histMetrics}
itemsPerRow={3}
{gridContent}
/>
{/key} {/key}
{/if} {/if}
{:else} {:else}
@ -367,9 +370,9 @@
<JobList <JobList
bind:this={jobList} bind:this={jobList}
bind:matchedListJobs bind:matchedListJobs
bind:metrics {metrics}
bind:sorting {sorting}
bind:showFootprint {showFootprint}
/> />
</Col> </Col>
</Row> </Row>
@ -388,10 +391,10 @@
presetMetrics={metrics} presetMetrics={metrics}
cluster={selectedCluster} cluster={selectedCluster}
configName="plot_list_selectedMetrics" configName="plot_list_selectedMetrics"
footprintSelect
applyMetrics={(newMetrics) => applyMetrics={(newMetrics) =>
metrics = [...newMetrics] metrics = [...newMetrics]
} }
footprintSelect
/> />
<HistogramSelection <HistogramSelection

View File

@ -26,7 +26,7 @@
/* Svelte 5 Props */ /* Svelte 5 Props */
let { let {
matchedCompareJobs = $bindable(0), matchedCompareJobs = $bindable(0),
metrics = $bindable(ccconfig?.plot_list_selectedMetrics), metrics = ccconfig?.plot_list_selectedMetrics,
filterBuffer = [], filterBuffer = [],
} = $props(); } = $props();

View File

@ -14,7 +14,7 @@
--> -->
<script> <script>
import { getContext } from "svelte"; import { getContext, untrack } from "svelte";
import { import {
queryStore, queryStore,
gql, gql,
@ -26,35 +26,22 @@
import Pagination from "./joblist/Pagination.svelte"; import Pagination from "./joblist/Pagination.svelte";
import JobListRow from "./joblist/JobListRow.svelte"; import JobListRow from "./joblist/JobListRow.svelte";
const ccconfig = getContext("cc-config"), /* Svelte 5 Props */
initialized = getContext("initialized"), let {
globalMetrics = getContext("globalMetrics"); matchedListJobs = $bindable(0),
selectedJobs = $bindable([]),
const equalsCheck = (a, b) => { metrics = getContext("cc-config").plot_list_selectedMetrics,
return JSON.stringify(a) === JSON.stringify(b); sorting = { field: "startTime", type: "col", order: "DESC" },
} showFootprint = false,
filterBuffer = [],
export let sorting = { field: "startTime", type: "col", order: "DESC" }; } = $props();
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 : "")
}
/* 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 client = getContextClient();
const query = gql` const query = gql`
query ( query (
@ -107,58 +94,75 @@
} }
`; `;
$: jobsStore = queryStore({ /* State Init */
client: client, let headerPaddingTop = $state(0);
query: query, let jobs = $state([]);
variables: { paging, sorting, filter }, 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 jobsStore = $derived(queryStore({
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({
client: client, client: client,
query: query, query: query,
variables: { paging, sorting, filter }, variables: { paging, sorting, filter },
requestPolicy: "network-only", 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() { export function refreshAllMetrics() {
// Refresh Job Metrics (Downstream will only query for running jobs) // Refresh Job Metrics (Downstream will only query for running jobs)
@ -175,11 +179,48 @@
if (minRunningFor && minRunningFor > 0) { if (minRunningFor && minRunningFor > 0) {
filters.push({ minRunningFor }); 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 }) => { const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({ return mutationStore({
@ -193,52 +234,11 @@
}); });
}; };
function updateConfiguration(value, page) { const equalsCheck = (a, b) => {
updateConfigurationMutation({ return JSON.stringify(a) === JSON.stringify(b);
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;
}
});
} }
if (!usePaging) { /* Init Header */
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;
stickyHeader( stickyHeader(
".cc-table-wrapper > table.table >thead > tr > th.position-sticky:nth-child(1)", ".cc-table-wrapper > table.table >thead > tr > th.position-sticky:nth-child(1)",
(x) => (headerPaddingTop = x), (x) => (headerPaddingTop = x),
@ -291,9 +291,9 @@
</tr> </tr>
{:else} {:else}
{#each jobs as job (job.id)} {#each jobs as job (job.id)}
<JobListRow bind:triggerMetricRefresh {job} {metrics} {plotWidth} {showFootprint} previousSelect={selectedJobs.includes(job.id)} <JobListRow {triggerMetricRefresh} {job} {metrics} {plotWidth} {showFootprint} previousSelect={selectedJobs.includes(job.id)}
on:select-job={({detail}) => selectedJobs = [...selectedJobs, detail]} selectJob={(detail) => selectedJobs = [...selectedJobs, detail]}
on:unselect-job={({detail}) => selectedJobs = selectedJobs.filter(item => item !== detail)} unselectJob={(detail) => selectedJobs = selectedJobs.filter(item => item !== detail)}
/> />
{:else} {:else}
<tr> <tr>
@ -323,10 +323,10 @@
totalItems={matchedListJobs} totalItems={matchedListJobs}
updatePaging={(detail) => { updatePaging={(detail) => {
if (detail.itemsPerPage != itemsPerPage) { if (detail.itemsPerPage != itemsPerPage) {
updateConfiguration(detail.itemsPerPage.toString(), detail.page); updateConfiguration(detail.itemsPerPage, detail.page);
} else { } else {
jobs = [] itemsPerPage = detail.itemsPerPage
paging = { itemsPerPage: detail.itemsPerPage, page: detail.page }; page = detail.page
} }
}} }}
/> />

View File

@ -6,32 +6,25 @@
- `items [Any]`: List of plot components to render - `items [Any]`: List of plot components to render
--> -->
<script> <script>
import { import {
Row, Row,
Col, Col,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
export let itemsPerRow /* Svelte 5 Props */
export let items let {
items,
/* Migtation Notes itemsPerRow,
* Requirements gridContent
* - Parent Components must be already Migrated } = $props();
* - 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
*/
</script> </script>
<Row cols={{ xs: 1, sm: 2, md: 3, lg: itemsPerRow}}> <Row cols={{ xs: 1, sm: 2, md: 3, lg: itemsPerRow}}>
{#each items as item} {#each items as item}
<Col class="px-1"> <Col class="px-1">
<slot {item}/> <!-- Note: Ignore '@' Error in IDE -->
{@render gridContent(item)}
</Col> </Col>
{/each} {/each}
</Row> </Row>

View File

@ -20,7 +20,7 @@
username = null, username = null,
authlevel= null, authlevel= null,
roles = null, roles = null,
isSelected = null, isSelected = $bindable(),
showSelect = false, showSelect = false,
} = $props(); } = $props();
@ -89,10 +89,8 @@
}}> }}>
{#if isSelected} {#if isSelected}
<Icon name="check-square"/> <Icon name="check-square"/>
{:else if isSelected == false} {:else }
<Icon name="square"/> <Icon name="plus-square-dotted"/>
{:else}
<Icon name="plus-square-dotted" />
{/if} {/if}
</Button> </Button>
<Tooltip <Tooltip

View File

@ -12,39 +12,37 @@
<script> <script>
import { queryStore, gql, getContextClient } from "@urql/svelte"; import { queryStore, gql, getContextClient } from "@urql/svelte";
import { getContext, createEventDispatcher } from "svelte"; import { getContext } from "svelte";
import { Card, Spinner } from "@sveltestrap/sveltestrap"; import { Card, Spinner } from "@sveltestrap/sveltestrap";
import { maxScope, checkMetricDisabled } from "../utils.js"; import { maxScope, checkMetricDisabled } from "../utils.js";
import JobInfo from "./JobInfo.svelte"; import JobInfo from "./JobInfo.svelte";
import MetricPlot from "../plots/MetricPlot.svelte"; import MetricPlot from "../plots/MetricPlot.svelte";
import JobFootprint from "../helper/JobFootprint.svelte"; import JobFootprint from "../helper/JobFootprint.svelte";
export let job; /* Svelte 5 Props */
export let metrics; let {
export let plotWidth; triggerMetricRefresh = false,
export let plotHeight = 275; job,
export let showFootprint; metrics,
export let triggerMetricRefresh = false; plotWidth,
export let previousSelect = false; plotHeight = 275,
showFootprint,
previousSelect = false,
selectJob,
unselectJob
} = $props();
const dispatch = createEventDispatcher(); /* Const Init */
const resampleConfig = getContext("resampling") || null; const client = getContextClient();
const resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0; const jobId = job.id;
const cluster = getContext("clusters");
let { id } = job; const scopes = (job.numNodes == 1)
let scopes = job.numNodes == 1 ? (job.numAcc >= 1)
? job.numAcc >= 1
? ["core", "accelerator"] ? ["core", "accelerator"]
: ["core"] : ["core"]
: ["node"]; : ["node"];
let selectedResolution = resampleDefault; const resampleConfig = getContext("resampling") || null;
let zoomStates = {}; const resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0;
let thresholdStates = {};
$: isSelected = previousSelect || null;
const cluster = getContext("clusters").find((c) => c.name == job.cluster);
const client = getContextClient();
const query = gql` const query = gql`
query ($id: ID!, $metrics: [String!]!, $scopes: [MetricScope!]!, $selectedResolution: Int) { query ($id: ID!, $metrics: [String!]!, $scopes: [MetricScope!]!, $selectedResolution: Int) {
jobMetrics(id: $id, metrics: $metrics, scopes: $scopes, resolution: $selectedResolution) { jobMetrics(id: $id, metrics: $metrics, scopes: $scopes, resolution: $selectedResolution) {
@ -77,52 +75,59 @@
} }
`; `;
function handleZoom(detail, metric) { /* State Init */
if ( // States have to differ, causes deathloop if just set let selectedResolution = $state(resampleDefault);
(zoomStates[metric]?.x?.min !== detail?.lastZoomState?.x?.min) && let zoomStates = $state({});
(zoomStates[metric]?.y?.max !== detail?.lastZoomState?.y?.max) let thresholdStates = $state({});
) {
zoomStates[metric] = {...detail.lastZoomState}
}
if ( // States have to differ, causes deathloop if just set /* Derived */
detail?.lastThreshold && let isSelected = $derived(previousSelect);
thresholdStates[metric] !== detail.lastThreshold let metricsQuery = $derived(queryStore({
) { // Handle to correctly reset on summed metric scope change
thresholdStates[metric] = detail.lastThreshold;
}
if (detail?.newRes) { // Triggers GQL
selectedResolution = detail.newRes
}
}
$: metricsQuery = queryStore({
client: client, client: client,
query: query, query: query,
variables: { id, metrics, scopes, selectedResolution }, variables: { id: jobId, metrics, scopes, selectedResolution },
})
);
/* Effects */
$effect(() => {
if (job.state === 'running' && triggerMetricRefresh === true) {
refreshMetrics();
}
}); });
$effect(() => {
if (isSelected == true && previousSelect == false) {
selectJob(jobId)
} else if (isSelected == false && previousSelect == true) {
unselectJob(jobId)
}
});
/* 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;
}
}
function refreshMetrics() { function refreshMetrics() {
metricsQuery = queryStore({ metricsQuery = queryStore({
client: client, client: client,
query: query, query: query,
variables: { id, metrics, scopes, selectedResolution }, variables: { id: jobId, metrics, scopes, selectedResolution },
// requestPolicy: 'network-only' // use default cache-first for refresh // 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) => const selectScope = (jobMetrics) =>
jobMetrics.reduce( jobMetrics.reduce(
(a, b) => (a, b) =>
@ -146,6 +151,7 @@
.map((jobMetric) => { .map((jobMetric) => {
if (jobMetric.data) { if (jobMetric.data) {
return { return {
name: jobMetric.data.name,
disabled: checkMetricDisabled( disabled: checkMetricDisabled(
jobMetric.data.name, jobMetric.data.name,
job.cluster, job.cluster,
@ -157,7 +163,6 @@
return jobMetric; return jobMetric;
} }
}); });
</script> </script>
<tr> <tr>
@ -191,19 +196,19 @@
/> />
</td> </td>
{/if} {/if}
{#each sortAndSelectScope($metricsQuery.data.jobMetrics) as metric, i (metric || i)} {#each sortAndSelectScope($metricsQuery.data.jobMetrics) as metric, i (metric?.name || i)}
<td> <td>
<!-- Subluster Metricconfig remove keyword for jobtables (joblist main, user joblist, project joblist) to be used here as toplevel case--> <!-- 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} {#if metric.disabled == false && metric.data}
<MetricPlot <MetricPlot
on:zoom={({detail}) => { handleZoom(detail, metric.data.name) }} on:zoom={({detail}) => handleZoom(detail, metric.data.name)}
height={plotHeight} height={plotHeight}
timestep={metric.data.metric.timestep} timestep={metric.data.metric.timestep}
scope={metric.data.scope} scope={metric.data.scope}
series={metric.data.metric.series} series={metric.data.metric.series}
statisticsSeries={metric.data.metric.statisticsSeries} statisticsSeries={metric.data.metric.statisticsSeries}
metric={metric.data.name} metric={metric.data.name}
{cluster} cluster={cluster.find((c) => c.name == job.cluster)}
subCluster={job.subCluster} subCluster={job.subCluster}
isShared={job.exclusive != 1} isShared={job.exclusive != 1}
numhwthreads={job.numHWThreads} numhwthreads={job.numHWThreads}

View File

@ -440,7 +440,7 @@
/* IF Zoom Enabled */ /* IF Zoom Enabled */
if (resampleConfig) { if (resampleConfig) {
u.over.addEventListener("dblclick", (e) => { u.over.addEventListener("dblclick", (e) => {
// console.log('Dispatch Reset') // console.log('Dispatch: Zoom Reset')
dispatch('zoom', { dispatch('zoom', {
lastZoomState: { lastZoomState: {
x: { time: false }, x: { time: false },
@ -506,7 +506,7 @@
}); });
// Prevents non-required dispatches // Prevents non-required dispatches
if (timestep !== closest) { 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', { dispatch('zoom', {
newRes: closest, newRes: closest,
lastZoomState: u?.scales, lastZoomState: u?.scales,
@ -514,6 +514,7 @@
}); });
} }
} else { } else {
// console.log('Dispatch: Zoom Update States')
dispatch('zoom', { dispatch('zoom', {
lastZoomState: u?.scales, lastZoomState: u?.scales,
lastThreshold: thresholds?.normal lastThreshold: thresholds?.normal

View File

@ -182,6 +182,6 @@
</script> </script>
<div class="cc-plot"> <div class="cc-plot" bind:clientWidth={width}>
<canvas bind:this={canvasElement} width="{width}" height="{height}"></canvas> <canvas bind:this={canvasElement} {width} {height}></canvas>
</div> </div>

View File

@ -18,6 +18,7 @@ Changes #2: Rewritten for Svelte 5, removed bodyHandler
--> -->
<script> <script>
/* Svelte 5 Props */
let { let {
sliderMin, sliderMin,
sliderMax, sliderMax,
@ -26,6 +27,7 @@ Changes #2: Rewritten for Svelte 5, removed bodyHandler
changeRange changeRange
} = $props(); } = $props();
/* State Init */
let pendingValues = $state([fromPreset, toPreset]); let pendingValues = $state([fromPreset, toPreset]);
let sliderFrom = $state(Math.max(((fromPreset == null ? sliderMin : fromPreset) - sliderMin) / (sliderMax - sliderMin), 0.)); 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.)); 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 leftHandle = $state();
let sliderMain = $state(); let sliderMain = $state();
/* Var Init */
let timeoutId = null; let timeoutId = null;
/* Functions */
function queueChangeEvent() { function queueChangeEvent() {
if (timeoutId !== null) { if (timeoutId !== null) {
clearTimeout(timeoutId) clearTimeout(timeoutId)

View File

@ -31,33 +31,22 @@
} from "../generic/utils.js"; } from "../generic/utils.js";
import Timeseries from "../generic/plots/MetricPlot.svelte"; import Timeseries from "../generic/plots/MetricPlot.svelte";
export let job; /* Svelte 5 Props */
export let metricName; let {
export let metricUnit; job,
export let nativeScope; metricName,
export let scopes; metricUnit,
export let rawData; nativeScope,
export let isShared = false; presetScopes,
isShared = false,
} = $props();
/* Const Init */
const client = getContextClient();
const statsPattern = /(.*)-stat$/;
const resampleConfig = getContext("resampling") || null; const resampleConfig = getContext("resampling") || null;
const resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0; 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 unit = (metricUnit?.prefix ? metricUnit.prefix : "") + (metricUnit?.base ? metricUnit.base : "");
const client = getContextClient();
const subQuery = gql` const subQuery = gql`
query ($dbid: ID!, $selectedMetrics: [String!]!, $selectedScopes: [MetricScope!]!, $selectedResolution: Int) { query ($dbid: ID!, $selectedMetrics: [String!]!, $selectedScopes: [MetricScope!]!, $selectedResolution: Int) {
singleUpdate: jobMetrics(id: $dbid, metrics: $selectedMetrics, scopes: $selectedScopes, resolution: $selectedResolution) { singleUpdate: jobMetrics(id: $dbid, metrics: $selectedMetrics, scopes: $selectedScopes, resolution: $selectedResolution) {
@ -90,84 +79,68 @@
} }
`; `;
function handleZoom(detail) { /* State Init */
if ( // States have to differ, causes deathloop if just set let requestedScopes = $state(presetScopes);
(pendingZoomState?.x?.min !== detail?.lastZoomState?.x?.min) && let selectedResolution = $state(resampleDefault);
(pendingZoomState?.y?.max !== detail?.lastZoomState?.y?.max)
) {
pendingZoomState = {...detail.lastZoomState};
}
if (detail?.lastThreshold) { // Handle to correctly reset on summed metric scope change let selectedHost = $state(null);
thresholdState = detail.lastThreshold; let zoomState = $state(null);
} else { let thresholdState = $state(null);
thresholdState = null;
}
if (detail?.newRes) { // Triggers GQL /* Derived */
pendingResolution = detail.newRes; const metricData = $derived(queryStore({
}
};
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, client: client,
query: subQuery, query: subQuery,
variables: { dbid, selectedMetrics, selectedScopes, selectedResolution: (resampleConfig ? selectedResolution : 0) }, variables: {
dbid: job.id,
selectedMetrics: [metricName],
selectedScopes: [...requestedScopes],
selectedResolution
},
// Never user network-only: causes reactive load-loop! // 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"];
}
}); });
if ($metricData && !$metricData.fetching) { /* Functions */
rawData = $metricData.data.singleUpdate.map((x) => x.metric) function handleZoom(detail) {
scopes = $metricData.data.singleUpdate.map((x) => x.scope) // Buffer last zoom state to allow seamless zoom on rerender
statsSeries = rawData.map((data) => data?.statisticsSeries ? data.statisticsSeries : null) // console.log('Update zoomState with:', {...detail.lastZoomState})
zoomState = detail?.lastZoomState ? {...detail.lastZoomState} : null;
// Keep Zoomlevel if ResChange By Zoom // Handle to correctly reset on summed metric scope change
if (pendingZoomState) { // console.log('Update thresholdState with:', detail.lastThreshold)
zoomState = {...pendingZoomState} thresholdState = detail?.lastThreshold ? detail.lastThreshold : null;
// Triggers GQL
if (detail?.newRes) {
// console.log('Update selectedResolution with:', detail.newRes)
selectedResolution = detail.newRes;
} }
};
// 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,
);
</script> </script>
<InputGroup class="mt-2"> <InputGroup class="mt-2">
@ -175,13 +148,13 @@
{metricName} ({unit}) {metricName} ({unit})
</InputGroupText> </InputGroupText>
<select class="form-select" bind:value={selectedScope}> <select class="form-select" bind:value={selectedScope}>
{#each scopes as scope, index} {#each availableScopes as scope, index}
<option value={scope}>{scope}</option> <option value={scope}>{scope}</option>
{#if statsSeries[index]} {#if statsSeries[index]}
<option value={scope + '-stat'}>stats series ({scope})</option> <option value={scope + '-stat'}>stats series ({scope})</option>
{/if} {/if}
{/each} {/each}
{#if scopes.length == 1 && nativeScope != "node" && !nodeOnly} {#if requestedScopes.length == 1 && nativeScope != "node"}
<option value={"load-all"}>Load all...</option> <option value={"load-all"}>Load all...</option>
{/if} {/if}
</select> </select>
@ -194,37 +167,37 @@
</select> </select>
{/if} {/if}
</InputGroup> </InputGroup>
{#key series} {#key selectedSeries}
{#if $metricData?.fetching == true} {#if $metricData.fetching}
<Spinner /> <Spinner />
{:else if error != null} {:else if $metricData.error}
<Card body color="danger">{error.message}</Card> <Card body color="danger">{$metricData.error.message}</Card>
{:else if series != null && !patternMatches} {:else if selectedSeries != null && !patternMatches}
<Timeseries <Timeseries
on:zoom={({detail}) => { handleZoom(detail) }} on:zoom={({detail}) => handleZoom(detail)}
cluster={job.cluster} cluster={job.cluster}
subCluster={job.subCluster} subCluster={job.subCluster}
timestep={data.timestep} timestep={selectedData.timestep}
scope={selectedScope} scope={selectedScope}
metric={metricName} metric={metricName}
numaccs={job.numAcc} numaccs={job.numAcc}
numhwthreads={job.numHWThreads} numhwthreads={job.numHWThreads}
{series} series={selectedSeries}
{isShared} {isShared}
{zoomState} {zoomState}
{thresholdState} {thresholdState}
/> />
{:else if statsSeries[selectedScopeIndex] != null && patternMatches} {:else if statsSeries[selectedScopeIndex] != null && patternMatches}
<Timeseries <Timeseries
on:zoom={({detail}) => { handleZoom(detail) }} on:zoom={({detail}) => handleZoom(detail)}
cluster={job.cluster} cluster={job.cluster}
subCluster={job.subCluster} subCluster={job.subCluster}
timestep={data.timestep} timestep={selectedData.timestep}
scope={selectedScope} scope={selectedScope}
metric={metricName} metric={metricName}
numaccs={job.numAcc} numaccs={job.numAcc}
numhwthreads={job.numHWThreads} numhwthreads={job.numHWThreads}
{series} series={selectedSeries}
{isShared} {isShared}
{zoomState} {zoomState}
{thresholdState} {thresholdState}

View File

@ -16,28 +16,37 @@
CardBody, CardBody,
Input, Input,
InputGroup, InputGroup,
InputGroupText, } from "@sveltestrap/sveltestrap"; InputGroupText,
} from "@sveltestrap/sveltestrap";
import { import {
scramble, scramble,
scrambleNames, } from "../../generic/utils.js"; scrambleNames,
} from "../../generic/utils.js";
export let cluster; /* Svelte 5 Props */
export let subCluster let {
export let hostname; cluster,
export let dataHealth; subCluster,
export let nodeJobsData = null; hostname,
dataHealth,
nodeJobsData = null,
} = $props();
/* Const Init */
// Not at least one returned, selected metric: NodeHealth warning // Not at least one returned, selected metric: NodeHealth warning
const healthWarn = !dataHealth.includes(true); const healthWarn = !dataHealth.includes(true);
// At least one non-returned selected metric: Metric config error? // At least one non-returned selected metric: Metric config error?
const metricWarn = dataHealth.includes(false); const metricWarn = dataHealth.includes(false);
let userList; /* Derived */
let projectList; const userList = $derived(nodeJobsData
$: if (nodeJobsData) { ? Array.from(new Set(nodeJobsData.jobs.items.map((j) => scrambleNames ? scramble(j.user) : j.user))).sort((a, b) => a.localeCompare(b))
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)); );
} 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> </script>
<Card class="pb-3"> <Card class="pb-3">

View File

@ -18,10 +18,14 @@
import MetricPlot from "../../generic/plots/MetricPlot.svelte"; import MetricPlot from "../../generic/plots/MetricPlot.svelte";
import NodeInfo from "./NodeInfo.svelte"; import NodeInfo from "./NodeInfo.svelte";
export let cluster; /* Svelte 5 Props */
export let nodeData; let {
export let selectedMetrics; cluster,
nodeData,
selectedMetrics,
} = $props();
/* Const Init */
const client = getContextClient(); const client = getContextClient();
const paging = { itemsPerPage: 50, page: 1 }; const paging = { itemsPerPage: 50, page: 1 };
const sorting = { field: "startTime", type: "col", order: "DESC" }; const sorting = { field: "startTime", type: "col", order: "DESC" };
@ -30,7 +34,6 @@
{ node: { contains: nodeData.host } }, { node: { contains: nodeData.host } },
{ state: ["running"] }, { state: ["running"] },
]; ];
const nodeJobsQuery = gql` const nodeJobsQuery = gql`
query ( query (
$filter: [JobFilter!]! $filter: [JobFilter!]!
@ -53,13 +56,19 @@
} }
`; `;
$: nodeJobsData = queryStore({ /* Derived */
const nodeJobsData = $derived(queryStore({
client: client, client: client,
query: nodeJobsQuery, query: nodeJobsQuery,
variables: { paging, sorting, filter }, 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) => const selectScope = (nodeMetrics) =>
nodeMetrics.reduce( nodeMetrics.reduce(
(a, b) => (a, b) =>
@ -89,15 +98,8 @@
} }
}); });
let refinedData; function buildExtendedLegend() {
let dataHealth; let pendingExtendedLegendData = null
$: 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) {
// Build Extended for allocated nodes [Commented: Only Build extended Legend For Shared Nodes] // 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)" if ($nodeJobsData.data.jobs.count >= 1) { // "&& !$nodeJobsData.data.jobs.items[0].exclusive)"
const accSet = Array.from(new Set($nodeJobsData.data.jobs.items const accSet = Array.from(new Set($nodeJobsData.data.jobs.items
@ -107,11 +109,11 @@
) )
)).flat(2) )).flat(2)
extendedLegendData = {} pendingExtendedLegendData = {};
for (const accId of accSet) { for (const accId of accSet) {
const matchJob = $nodeJobsData.data.jobs.items.find((i) => i.resources.find((r) => r.accelerators.includes(accId))) const matchJob = $nodeJobsData.data.jobs.items.find((i) => i.resources.find((r) => r.accelerators.includes(accId)))
const matchUser = matchJob?.user ? matchJob.user : null const matchUser = matchJob?.user ? matchJob.user : null
extendedLegendData[accId] = { pendingExtendedLegendData[accId] = {
user: (scrambleNames && matchUser) user: (scrambleNames && matchUser)
? scramble(matchUser) ? scramble(matchUser)
: (matchUser ? matchUser : '-'), : (matchUser ? matchUser : '-'),
@ -120,6 +122,7 @@
} }
// Theoretically extendable for hwthreadIDs // Theoretically extendable for hwthreadIDs
} }
return pendingExtendedLegendData;
} }
</script> </script>