232 lines
6.9 KiB
Svelte

<!--
@component Data row for a single job displaying metric plots
Properties:
- `job Object`: The job object (GraphQL.Job)
- `metrics [String]`: Currently selected metrics
- `plotWidth Number`: Width of the sub-components
- `plotHeight Number?`: Height of the sub-components [Default: 275]
- `showFootprint Bool`: Display of footprint component for job
- `triggerMetricRefresh Bool?`: If changed to true from upstream, will trigger metric query
-->
<script>
import { queryStore, gql, getContextClient } from "@urql/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";
/* Svelte 5 Props */
let {
triggerMetricRefresh = false,
job,
metrics,
plotWidth,
plotHeight = 275,
showFootprint,
previousSelect = false,
selectJob,
unselectJob
} = $props();
/* 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"];
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) {
name
scope
metric {
unit {
prefix
base
}
timestep
statisticsSeries {
min
mean
median
max
}
series {
hostname
id
data
statistics {
min
avg
max
}
}
}
}
}
`;
/* 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();
}
});
$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() {
metricsQuery = queryStore({
client: client,
query: query,
variables: { id: jobId, metrics, scopes, selectedResolution },
// requestPolicy: 'network-only' // use default cache-first for refresh
});
}
const selectScope = (jobMetrics) =>
jobMetrics.reduce(
(a, b) =>
maxScope([a.scope, b.scope]) == a.scope
? job.numNodes > 1
? a
: b
: job.numNodes > 1
? b
: a,
jobMetrics[0],
);
const sortAndSelectScope = (jobMetrics) =>
metrics
.map((name) => jobMetrics.filter((jobMetric) => jobMetric.name == name))
.map((jobMetrics) => ({
disabled: false,
data: jobMetrics.length > 0 ? selectScope(jobMetrics) : null,
}))
.map((jobMetric) => {
if (jobMetric.data) {
return {
name: jobMetric.data.name,
disabled: checkMetricDisabled(
jobMetric.data.name,
job.cluster,
job.subCluster,
),
data: jobMetric.data,
};
} else {
return jobMetric;
}
});
</script>
<tr>
<td>
<JobInfo {job} bind:isSelected showSelect/>
</td>
{#if job.monitoringStatus == 0 || job.monitoringStatus == 2}
<td colspan={metrics.length}>
<Card body color="warning">Not monitored or archiving failed</Card>
</td>
{:else if $metricsQuery.fetching}
<td colspan={metrics.length} style="text-align: center;">
<Spinner secondary />
</td>
{:else if $metricsQuery.error}
<td colspan={metrics.length}>
<Card body color="danger" class="mb-3">
{$metricsQuery.error.message.length > 500
? $metricsQuery.error.message.substring(0, 499) + "..."
: $metricsQuery.error.message}
</Card>
</td>
{:else}
{#if showFootprint}
<td>
<JobFootprint
{job}
width={plotWidth}
height="{plotHeight}px"
displayTitle={false}
/>
</td>
{/if}
{#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)}
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.find((c) => c.name == job.cluster)}
subCluster={job.subCluster}
isShared={job.exclusive != 1}
numhwthreads={job.numHWThreads}
numaccs={job.numAcc}
zoomState={zoomStates[metric.data.name] || null}
thresholdState={thresholdStates[metric.data.name] || null}
/>
{:else if metric.disabled == true && metric.data}
<Card body color="info"
>Metric disabled for subcluster <code
>{metric.data.name}:{job.subCluster}</code
></Card
>
{:else}
<Card body color="warning">No dataset returned</Card>
{/if}
</td>
{/each}
{/if}
</tr>