Merge branch 'dev' of github.com:ClusterCockpit/cc-backend into dev

This commit is contained in:
2026-01-22 20:31:18 +01:00
10 changed files with 445 additions and 776 deletions

View File

@@ -305,8 +305,13 @@ func (auth *Authentication) SaveSession(rw http.ResponseWriter, r *http.Request,
if auth.SessionMaxAge != 0 { if auth.SessionMaxAge != 0 {
session.Options.MaxAge = int(auth.SessionMaxAge.Seconds()) session.Options.MaxAge = int(auth.SessionMaxAge.Seconds())
} }
if config.Keys.HTTPSCertFile == "" && config.Keys.HTTPSKeyFile == "" { if r.TLS == nil && r.Header.Get("X-Forwarded-Proto") != "https" {
cclog.Warn("HTTPS not configured - session cookies will not have Secure flag set (insecure for production)") // If neither TLS or an encrypted reverse proxy are used, do not mark cookies as secure.
cclog.Warn("Authenticating with unencrypted request. Session cookies will not have Secure flag set (insecure for production)")
if r.Header.Get("X-Forwarded-Proto") == "" {
// This warning will not be printed if e.g. X-Forwarded-Proto == http
cclog.Warn("If you are using a reverse proxy, make sure X-Forwarded-Proto is set")
}
session.Options.Secure = false session.Options.Secure = false
} }
session.Options.SameSite = http.SameSiteStrictMode session.Options.SameSite = http.SameSiteStrictMode

View File

@@ -6,9 +6,6 @@
--> -->
<script> <script>
// import {
// getContext
// } from "svelte"
import { import {
queryStore, queryStore,
gql, gql,
@@ -55,9 +52,6 @@
let to = $state(new Date(Date.now())); let to = $state(new Date(Date.now()));
let stackedFrom = $state(Math.floor(Date.now() / 1000) - 14400); let stackedFrom = $state(Math.floor(Date.now() / 1000) - 14400);
let colWidthStates = $state(0); let colWidthStates = $state(0);
let colWidthRoof = $state(0);
let colWidthTotals = $state(0);
let colWidthStacked = $state(0);
/* Derived */ /* Derived */
// States for Stacked charts // States for Stacked charts
@@ -354,274 +348,288 @@
</script> </script>
<Card style="height: 98vh;"> <Row>
<CardBody class="align-content-center p-2"> <Col>
<Row> <Refresher
hideSelector
initially={60}
onRefresh={(interval) => {
from = new Date(Date.now() - 5 * 60 * 1000);
to = new Date(Date.now());
clusterFrom = new Date(Date.now() - (8 * 60 * 60 * 1000))
if (interval) stackedFrom += Math.floor(interval / 1000);
else stackedFrom += 1 // Workaround: TimeSelection not linked, just trigger new data on manual refresh
}}
/>
</Col>
</Row>
{#if $statusQuery.fetching || $statesTimed.fetching}
<Row class="justify-content-center">
<Col xs="auto">
<Spinner />
</Col>
</Row>
{:else if $statusQuery.error || $statesTimed.error}
<Row class="mb-2">
<Col class="d-flex justify-content-end">
<Button color="secondary" href="/">
<Icon name="x"/>
</Button>
</Col>
</Row>
<Row cols={{xs:1, md:2}}>
{#if $statusQuery.error}
<Col> <Col>
<Refresher <Card color="danger"><CardBody>Error Requesting Status Data: {$statusQuery.error.message}</CardBody></Card>
hideSelector
initially={60}
onRefresh={(interval) => {
from = new Date(Date.now() - 5 * 60 * 1000);
to = new Date(Date.now());
clusterFrom = new Date(Date.now() - (8 * 60 * 60 * 1000))
if (interval) stackedFrom += Math.floor(interval / 1000);
else stackedFrom += 1 // Workaround: TimeSelection not linked, just trigger new data on manual refresh
}}
/>
</Col> </Col>
</Row> {/if}
{#if $statusQuery.fetching || $statesTimed.fetching} {#if $statesTimed.error}
<Row class="justify-content-center"> <Col>
<Col xs="auto"> <Card color="danger"><CardBody>Error Requesting Node Scheduler States: {$statesTimed.error.message}</CardBody></Card>
<Spinner /> </Col>
</Col> {/if}
</Row> </Row>
{:else if $statusQuery.error || $statesTimed.error} {:else}
<Row class="mb-2"> <!-- View Supposed to be Viewed at Max Viewport Size -->
<Col class="d-flex justify-content-end"> <div class="align-content-center p-2">
<Button color="secondary" href="/"> <Row cols={{xs:1, md:2}} style="height: 24vh; margin-bottom: 1rem;">
<Icon name="x"/> <Col> <!-- General Cluster Info Card -->
</Button>
</Col>
</Row>
<Row cols={{xs:1, md:2}}>
{#if $statusQuery.error}
<Col>
<Card color="danger"><CardBody>Error Requesting Status Data: {$statusQuery.error.message}</CardBody></Card>
</Col>
{/if}
{#if $statesTimed.error}
<Col>
<Card color="danger"><CardBody>Error Requesting Node Scheduler States: {$statesTimed.error.message}</CardBody></Card>
</Col>
{/if}
</Row>
{:else}
<Row cols={{xs:1, md:2}}>
<Col> <!-- General Cluster Info Card -->
<Card class="h-100">
<CardHeader>
<Row>
<Col xs="11" class="text-center">
<h2 class="mb-0">Cluster {presetCluster.charAt(0).toUpperCase() + presetCluster.slice(1)}</h2>
</Col>
<Col xs="1" class="d-flex justify-content-end">
<Button color="light" href="/">
<Icon name="x"/>
</Button>
</Col>
</Row>
</CardHeader>
<CardBody>
<h4>CPU(s)</h4><p><strong>{[...clusterInfo?.processorTypes].join(', ')}</strong></p>
</CardBody>
</Card>
</Col>
<Col> <!-- Utilization Info Card -->
<Card class="h-100"> <Card class="h-100">
<CardBody> <CardHeader>
<Row class="mb-1"> <Row>
<Col xs={4} class="d-inline-flex align-items-center justify-content-center"> <Col xs="11" class="text-center">
<Badge color="primary" style="font-size:x-large;margin-right:0.25rem;"> <h2 class="mb-0">Cluster {presetCluster.charAt(0).toUpperCase() + presetCluster.slice(1)}</h2>
{clusterInfo?.runningJobs} </Col>
</Badge> <Col xs="1" class="d-flex justify-content-end">
<div style="font-size:large;"> <Button color="light" href="/">
Running Jobs <Icon name="x"/>
</div> </Button>
</Col> </Col>
<Col xs={4} class="d-inline-flex align-items-center justify-content-center"> </Row>
<Badge color="primary" style="font-size:x-large;margin-right:0.25rem;"> </CardHeader>
{clusterInfo?.activeUsers} <CardBody>
</Badge> <h4>CPU(s)</h4><p><strong>{[...clusterInfo?.processorTypes].join(', ')}</strong></p>
<div style="font-size:large;"> </CardBody>
Active Users </Card>
</div> </Col>
</Col>
<Col xs={4} class="d-inline-flex align-items-center justify-content-center"> <Col> <!-- Utilization Info Card -->
<Badge color="primary" style="font-size:x-large;margin-right:0.25rem;"> <Card class="h-100">
{clusterInfo?.allocatedNodes} <CardBody>
</Badge> <Row class="mb-1">
<div style="font-size:large;"> <Col xs={4} class="d-inline-flex align-items-center justify-content-center">
Active Nodes <Badge color="primary" style="font-size:x-large;margin-right:0.25rem;">
</div> {clusterInfo?.runningJobs}
</Col> </Badge>
</Row> <div style="font-size:large;">
<Row class="mt-1 mb-2"> Running Jobs
</div>
</Col>
<Col xs={4} class="d-inline-flex align-items-center justify-content-center">
<Badge color="primary" style="font-size:x-large;margin-right:0.25rem;">
{clusterInfo?.activeUsers}
</Badge>
<div style="font-size:large;">
Active Users
</div>
</Col>
<Col xs={4} class="d-inline-flex align-items-center justify-content-center">
<Badge color="primary" style="font-size:x-large;margin-right:0.25rem;">
{clusterInfo?.allocatedNodes}
</Badge>
<div style="font-size:large;">
Active Nodes
</div>
</Col>
</Row>
<Row class="mt-1 mb-2">
<Col xs={4} class="d-inline-flex align-items-center justify-content-center">
<Badge color="secondary" style="font-size:x-large;margin-right:0.25rem;">
{clusterInfo?.flopRate} {clusterInfo?.flopRateUnit}
</Badge>
<div style="font-size:large;">
Total Flop Rate
</div>
</Col>
<Col xs={4} class="d-inline-flex align-items-center justify-content-center">
<Badge color="secondary" style="font-size:x-large;margin-right:0.25rem;">
{clusterInfo?.memBwRate} {clusterInfo?.memBwRateUnit}
</Badge>
<div style="font-size:large;">
Total Memory Bandwidth
</div>
</Col>
{#if clusterInfo?.totalAccs !== 0}
<Col xs={4} class="d-inline-flex align-items-center justify-content-center"> <Col xs={4} class="d-inline-flex align-items-center justify-content-center">
<Badge color="secondary" style="font-size:x-large;margin-right:0.25rem;"> <Badge color="secondary" style="font-size:x-large;margin-right:0.25rem;">
{clusterInfo?.flopRate} {clusterInfo?.flopRateUnit} {clusterInfo?.gpuPwr} {clusterInfo?.gpuPwrUnit}
</Badge> </Badge>
<div style="font-size:large;"> <div style="font-size:large;">
Total Flop Rate Total GPU Power
</div> </div>
</Col> </Col>
{:else}
<Col xs={4} class="d-inline-flex align-items-center justify-content-center"> <Col xs={4} class="d-inline-flex align-items-center justify-content-center">
<Badge color="secondary" style="font-size:x-large;margin-right:0.25rem;"> <Badge color="secondary" style="font-size:x-large;margin-right:0.25rem;">
{clusterInfo?.memBwRate} {clusterInfo?.memBwRateUnit} {clusterInfo?.cpuPwr} {clusterInfo?.cpuPwrUnit}
</Badge> </Badge>
<div style="font-size:large;"> <div style="font-size:large;">
Total Memory Bandwidth Total CPU Power
</div> </div>
</Col> </Col>
{#if clusterInfo?.totalAccs !== 0} {/if}
<Col xs={4} class="d-inline-flex align-items-center justify-content-center"> </Row>
<Badge color="secondary" style="font-size:x-large;margin-right:0.25rem;"> <Row class="my-1 align-items-baseline">
{clusterInfo?.gpuPwr} {clusterInfo?.gpuPwrUnit} <Col xs={2} style="font-size:large;">
</Badge> Active Cores
<div style="font-size:large;"> </Col>
Total GPU Power <Col xs={8}>
</div> <Progress multi max={clusterInfo?.totalCores} style="height:2.5rem;font-size:x-large;">
</Col> <Progress bar color="success" value={clusterInfo?.allocatedCores} title={`${clusterInfo?.allocatedCores} active`}>{formatNumber(clusterInfo?.allocatedCores)}</Progress>
{:else} <Progress bar color="light" value={clusterInfo?.idleCores} title={`${clusterInfo?.idleCores} idle`}>{formatNumber(clusterInfo?.idleCores)}</Progress>
<Col xs={4} class="d-inline-flex align-items-center justify-content-center"> </Progress>
<Badge color="secondary" style="font-size:x-large;margin-right:0.25rem;"> </Col>
{clusterInfo?.cpuPwr} {clusterInfo?.cpuPwrUnit} <Col xs={2} style="font-size:large;">
</Badge> Idle Cores
<div style="font-size:large;"> </Col>
Total CPU Power </Row>
</div> {#if clusterInfo?.totalAccs !== 0}
</Col>
{/if}
</Row>
<Row class="my-1 align-items-baseline"> <Row class="my-1 align-items-baseline">
<Col xs={2} style="font-size:large;"> <Col xs={2} style="font-size:large;">
Active Cores Active GPU
</Col> </Col>
<Col xs={8}> <Col xs={8}>
<Progress multi max={clusterInfo?.totalCores} style="height:2.5rem;font-size:x-large;"> <Progress multi max={clusterInfo?.totalAccs} style="height:2.5rem;font-size:x-large;">
<Progress bar color="success" value={clusterInfo?.allocatedCores} title={`${clusterInfo?.allocatedCores} active`}>{formatNumber(clusterInfo?.allocatedCores)}</Progress> <Progress bar color="success" value={clusterInfo?.allocatedAccs} title={`${clusterInfo?.allocatedAccs} active`}>{formatNumber(clusterInfo?.allocatedAccs)}</Progress>
<Progress bar color="light" value={clusterInfo?.idleCores} title={`${clusterInfo?.idleCores} idle`}>{formatNumber(clusterInfo?.idleCores)}</Progress> <Progress bar color="light" value={clusterInfo?.idleAccs} title={`${clusterInfo?.idleAccs} idle`}>{formatNumber(clusterInfo?.idleAccs)}</Progress>
</Progress> </Progress>
</Col> </Col>
<Col xs={2} style="font-size:large;"> <Col xs={2} style="font-size:large;">
Idle Cores Idle GPU
</Col> </Col>
</Row> </Row>
{#if clusterInfo?.totalAccs !== 0} {/if}
<Row class="my-1 align-items-baseline"> </CardBody>
<Col xs={2} style="font-size:large;"> </Card>
Active GPU </Col>
</Col> </Row>
<Col xs={8}>
<Progress multi max={clusterInfo?.totalAccs} style="height:2.5rem;font-size:x-large;">
<Progress bar color="success" value={clusterInfo?.allocatedAccs} title={`${clusterInfo?.allocatedAccs} active`}>{formatNumber(clusterInfo?.allocatedAccs)}</Progress>
<Progress bar color="light" value={clusterInfo?.idleAccs} title={`${clusterInfo?.idleAccs} idle`}>{formatNumber(clusterInfo?.idleAccs)}</Progress>
</Progress>
</Col>
<Col xs={2} style="font-size:large;">
Idle GPU
</Col>
</Row>
{/if}
</CardBody>
</Card>
</Col>
<Col> <!-- Total Cluster Metric in Time SUMS--> <Row cols={{xs:1, md:2}} style="height: 35vh; margin-bottom: 1rem;">
<div bind:clientWidth={colWidthTotals}> <!-- Total Cluster Metric in Time SUMS-->
<Col class="text-center">
<h5 class="mt-2 mb-0">
Cluster Utilization (
<span style="color: #0000ff;">
{`${$statusQuery?.data?.clusterMetrics?.metrics[0]?.name} (${$statusQuery?.data?.clusterMetrics?.metrics[0]?.unit?.prefix}${$statusQuery?.data?.clusterMetrics?.metrics[0]?.unit?.base})`}
</span>,
<span style="color: #ff0000;">
{`${$statusQuery?.data?.clusterMetrics?.metrics[1]?.name} (${$statusQuery?.data?.clusterMetrics?.metrics[1]?.unit?.prefix}${$statusQuery?.data?.clusterMetrics?.metrics[1]?.unit?.base})`}
</span>
)
</h5>
<div>
{#key $statusQuery?.data?.clusterMetrics}
<DoubleMetric <DoubleMetric
width={colWidthTotals}
timestep={$statusQuery?.data?.clusterMetrics[0]?.timestep || 60} timestep={$statusQuery?.data?.clusterMetrics[0]?.timestep || 60}
numNodes={$statusQuery?.data?.clusterMetrics?.nodeCount || 0} numNodes={$statusQuery?.data?.clusterMetrics?.nodeCount || 0}
metricData={$statusQuery?.data?.clusterMetrics?.metrics || []} metricData={$statusQuery?.data?.clusterMetrics?.metrics || []}
cluster={presetCluster} publicMode
fixLinewidth={2}
/> />
</div> {/key}
</Col> </div>
</Col>
<Col> <!-- Nodes Roofline --> <Col> <!-- Nodes Roofline -->
<div bind:clientWidth={colWidthRoof}> <div>
{#key $statusQuery?.data?.nodeMetrics} {#key $statusQuery?.data?.nodeMetrics}
<Roofline <Roofline
colorBackground colorBackground
useColors={false} useColors={false}
useLegend={false} useLegend={false}
allowSizeChange allowSizeChange
width={colWidthRoof} cluster={presetCluster}
height={300} subCluster={clusterInfo?.roofData ? clusterInfo.roofData : null}
cluster={presetCluster} roofData={transformNodesStatsToData($statusQuery?.data?.nodeMetrics)}
subCluster={clusterInfo?.roofData ? clusterInfo.roofData : null} nodesData={transformNodesStatsToInfo($statusQuery?.data?.nodeMetrics)}
roofData={transformNodesStatsToData($statusQuery?.data?.nodeMetrics)} fixTitle="Node Utilization"
nodesData={transformNodesStatsToInfo($statusQuery?.data?.nodeMetrics)} yMinimum={1.0}
fixTitle="Node Utilization" height={330}
yMinimum={1.0} />
/> {/key}
{/key} </div>
</div> </Col>
</Col> </Row>
<Col> <!-- Pie Last States -->
<Row> <Row cols={{xs:1, md:2}} style="height: 35vh;">
{#if refinedStateData.length > 0} <Col> <!-- Pie Last States -->
<Col class="px-3 mt-2 mt-lg-0"> <Row>
<div bind:clientWidth={colWidthStates}> {#if refinedStateData.length > 0}
{#key refinedStateData} <Col class="px-3 mt-2 mt-lg-0">
<Pie <div bind:clientWidth={colWidthStates}>
canvasId="hpcpie-slurm"
size={colWidthStates * 0.66}
sliceLabel="Nodes"
quantities={refinedStateData.map(
(sd) => sd.count,
)}
entities={refinedStateData.map(
(sd) => sd.state,
)}
fixColors={refinedStateData.map(
(sd) => colors['nodeStates'][sd.state],
)}
/>
{/key}
</div>
</Col>
<Col class="px-4 py-2">
{#key refinedStateData} {#key refinedStateData}
<Table> <Pie
<tr class="mb-2"> canvasId="hpcpie-slurm"
<th></th> size={colWidthStates * 0.66}
<th class="h4">State</th> sliceLabel="Nodes"
<th class="h4">Count</th> quantities={refinedStateData.map(
</tr> (sd) => sd.count,
{#each refinedStateData as sd, i} )}
<tr> entities={refinedStateData.map(
<td><Icon name="circle-fill" style="color: {colors['nodeStates'][sd.state]}; font-size: 30px;"/></td> (sd) => sd.state,
<td class="h5">{sd.state.charAt(0).toUpperCase() + sd.state.slice(1)}</td> )}
<td class="h5">{sd.count}</td> fixColors={refinedStateData.map(
</tr> (sd) => colors['nodeStates'][sd.state],
{/each} )}
</Table> />
{/key} {/key}
</Col> </div>
{:else} </Col>
<Col> <Col class="px-4 py-2">
<Card body color="warning" class="mx-4 my-2" {#key refinedStateData}
>Cannot render state status: No state data returned for <code>Pie Chart</code></Card <Table>
> <tr class="mb-2">
</Col> <th></th>
{/if} <th class="h4">State</th>
</Row> <th class="h4">Count</th>
</Col> </tr>
{#each refinedStateData as sd, i}
<tr>
<td><Icon name="circle-fill" style="color: {colors['nodeStates'][sd.state]}; font-size: 30px;"/></td>
<td class="h5">{sd.state.charAt(0).toUpperCase() + sd.state.slice(1)}</td>
<td class="h5">{sd.count}</td>
</tr>
{/each}
</Table>
{/key}
</Col>
{:else}
<Col>
<Card body color="warning" class="mx-4 my-2"
>Cannot render state status: No state data returned for <code>Pie Chart</code></Card
>
</Col>
{/if}
</Row>
</Col>
<Col> <!-- Stacked SchedState --> <Col> <!-- Stacked SchedState -->
<div bind:clientWidth={colWidthStacked}> <div>
{#key $statesTimed?.data?.nodeStatesTimed} {#key $statesTimed?.data?.nodeStatesTimed}
<Stacked <Stacked
data={$statesTimed?.data?.nodeStatesTimed} data={$statesTimed?.data?.nodeStatesTimed}
width={colWidthStacked} height={300}
height={260} ylabel="Nodes"
ylabel="Nodes" yunit = "#Count"
yunit = "#Count" title = "Cluster Status"
title = "Cluster Status" stateType = "Node"
stateType = "Node" />
/> {/key}
{/key} </div>
</div> </Col>
</Col> </Row>
</Row> </div>
{/if} {/if}
</CardBody>
</Card>

View File

@@ -229,7 +229,7 @@
</DropdownToggle> </DropdownToggle>
<DropdownMenu class="dropdown-menu-lg-end"> <DropdownMenu class="dropdown-menu-lg-end">
<NavbarLinks <NavbarLinks
{clustersNames} {clusterNames}
{subclusterMap} {subclusterMap}
direction="right" direction="right"
links={views.filter( links={views.filter(

View File

@@ -4,123 +4,56 @@
Only width/height should change reactively. Only width/height should change reactively.
Properties: Properties:
- `metric String`: The metric name - `metricData [Data]`: Two series of metric data including unit info
- `scope String?`: Scope of the displayed data [Default: node] - `timestep Number`: Data timestep
- `numNodes Number`: Number of nodes from which metric data is aggregated
- `cluster String`: Cluster name of the parent job / data [Default: ""]
- `forNode Bool?`: If this plot is used for node data display; will render x-axis as negative time with $now as maximum [Default: true]
- `enableFlip Bool?`: Whether to use legend tooltip flipping based on canvas size [Default: false]
- `publicMode Bool?`: Disables tooltip legend and enables larger colored axis labels [Default: false]
- `height Number?`: The plot height [Default: 300] - `height Number?`: The plot height [Default: 300]
- `timestep Number`: The timestep used for X-axis rendering
- `series [GraphQL.Series]`: The metric data object
- `statisticsSeries [GraphQL.StatisticsSeries]?`: Min/Max/Median representation of metric data [Default: null]
- `cluster String?`: Cluster name of the parent job / data [Default: ""]
- `subCluster String`: Name of the subCluster of the parent job
- `isShared Bool?`: If this job used shared resources; for additional legend display [Default: false]
- `forNode Bool?`: If this plot is used for node data display; will render x-axis as negative time with $now as maximum [Default: false]
- `numhwthreads Number?`: Number of job HWThreads [Default: 0]
- `numaccs Number?`: Number of job Accelerators [Default: 0]
- `zoomState Object?`: The last zoom state to preserve on user zoom [Default: null]
- `thersholdState Object?`: The last threshold state to preserve on user zoom [Default: null]
- `extendedLegendData Object?`: Additional information to be rendered in an extended legend [Default: null]
- `onZoom Func`: Callback function to handle zoom-in event
--> -->
<script> <script>
import uPlot from "uplot"; import uPlot from "uplot";
import { formatNumber, formatDurationTime } from "../units.js"; import { formatNumber, formatDurationTime } from "../units.js";
import { getContext, onMount, onDestroy } from "svelte"; import { getContext, onDestroy } from "svelte";
import { Card } from "@sveltestrap/sveltestrap"; import { Card } from "@sveltestrap/sveltestrap";
/* Svelte 5 Props */ /* Svelte 5 Props */
let { let {
// metric, metricData,
width = 0,
height = 300,
fixLinewidth = null,
timestep, timestep,
numNodes, numNodes,
metricData, cluster,
// useStatsSeries = false,
// statisticsSeries = null,
cluster = "",
forNode = true, forNode = true,
// zoomState = null,
// thresholdState = null,
enableFlip = false, enableFlip = false,
// onZoom publicMode = false,
height = 300,
} = $props(); } = $props();
/* Const Init */ /* Const Init */
const clusterCockpitConfig = getContext("cc-config"); const clusterCockpitConfig = getContext("cc-config");
// const resampleConfig = getContext("resampling"); const fixedLineColors = ["#0000ff", "#ff0000"]; // Plot only uses 2 Datasets: High Contrast
// const subClusterTopology = getContext("getHardwareTopology")(cluster, subCluster);
// const metricConfig = getContext("getMetricConfig")(cluster, subCluster, metric);
const lineColors = clusterCockpitConfig.plotConfiguration_colorScheme;
// const cbmode = clusterCockpitConfig?.plotConfiguration_colorblindMode || false;
const renderSleepTime = 200; const renderSleepTime = 200;
// const normalLineColor = "#000000";
// const backgroundColors = {
// normal: "rgba(255, 255, 255, 1.0)",
// caution: cbmode ? "rgba(239, 230, 69, 0.3)" : "rgba(255, 128, 0, 0.3)",
// alert: cbmode ? "rgba(225, 86, 44, 0.3)" : "rgba(255, 0, 0, 0.3)",
// };
/* Var Init */ /* Var Init */
let timeoutId = null; let timeoutId = null;
/* State Init */ /* State Init */
let plotWrapper = $state(null); let plotWrapper = $state(null);
let width = $state(0); // Wrapper Width
let uplot = $state(null); let uplot = $state(null);
/* Derived */ /* Derived */
const lineWidth = $derived(fixLinewidth ? fixLinewidth : clusterCockpitConfig.plotConfiguration_lineWidth / window.devicePixelRatio);
// const usesMeanStatsSeries = $derived((statisticsSeries?.mean && statisticsSeries.mean.length != 0));
// const resampleTrigger = $derived(resampleConfig?.trigger ? Number(resampleConfig.trigger) : null);
// const resampleResolutions = $derived(resampleConfig?.resolutions ? [...resampleConfig.resolutions] : null);
// const resampleMinimum = $derived(resampleConfig?.resolutions ? Math.min(...resampleConfig.resolutions) : null);
// const thresholds = $derived(findJobAggregationThresholds(
// subClusterTopology,
// metricConfig,
// scope,
// numhwthreads,
// numaccs
// ));
const longestSeries = $derived.by(() => {
// if (useStatsSeries) {
// return usesMeanStatsSeries ? statisticsSeries?.mean?.length : statisticsSeries?.median?.length;
// } else {
return metricData.reduce((n, m) => Math.max(n, m.data.length), 0);
// }
});
const maxX = $derived(longestSeries * timestep); const maxX = $derived(longestSeries * timestep);
// const maxY = $derived.by(() => { const lineWidth = $derived(publicMode ? 2 : clusterCockpitConfig.plotConfiguration_lineWidth / window.devicePixelRatio);
// let pendingY = 0; const longestSeries = $derived.by(() => {
// // if (useStatsSeries) { return metricData.reduce((n, m) => Math.max(n, m.data.length), 0);
// // pendingY = statisticsSeries.max.reduce( });
// // (max, x) => Math.max(max, x),
// // thresholds?.normal,
// // ) || thresholds?.normal
// // } else {
// pendingY = series.reduce(
// (max, series) => Math.max(max, series?.statistics?.max),
// thresholds?.normal,
// ) || thresholds?.normal;
// // }
// if (pendingY >= 10 * thresholds.peak) { // Derive Plot Params
// // Hard y-range render limit if outliers in series data let plotData = $derived.by(() => {
// return (10 * thresholds.peak);
// } else {
// return pendingY;
// }
// });
// const plotBands = $derived.by(() => {
// if (useStatsSeries) {
// return [
// { series: [2, 3], fill: cbmode ? "rgba(0,0,255,0.1)" : "rgba(0,255,0,0.1)" },
// { series: [3, 1], fill: cbmode ? "rgba(0,255,0,0.1)" : "rgba(255,0,0,0.1)" },
// ];
// };
// return null;
// })
const plotData = $derived.by(() => {
let pendingData = [new Array(longestSeries)]; let pendingData = [new Array(longestSeries)];
// X // X
if (forNode === true) { if (forNode === true) {
@@ -135,25 +68,15 @@
}; };
}; };
// Y // Y
// if (useStatsSeries) { for (let i = 0; i < metricData.length; i++) {
// pendingData.push(statisticsSeries.min); pendingData.push(metricData[i]?.data);
// pendingData.push(statisticsSeries.max); };
// if (usesMeanStatsSeries) {
// pendingData.push(statisticsSeries.mean);
// } else {
// pendingData.push(statisticsSeries.median);
// }
// } else {
for (let i = 0; i < metricData.length; i++) {
pendingData.push(metricData[i]?.data);
};
// };
return pendingData; return pendingData;
}) })
const plotSeries = $derived.by(() => {
let plotSeries = $derived.by(() => {
// X
let pendingSeries = [ let pendingSeries = [
// Note: X-Legend Will not be shown as soon as Y-Axis are in extendedMode
{ {
label: "Runtime", label: "Runtime",
value: (u, ts, sidx, didx) => value: (u, ts, sidx, didx) =>
@@ -161,87 +84,108 @@
} }
]; ];
// Y // Y
// if (useStatsSeries) { for (let i = 0; i < metricData.length; i++) {
// pendingSeries.push({ pendingSeries.push({
// label: "min", label: publicMode ? null : `${metricData[i]?.name} (${metricData[i]?.unit?.prefix}${metricData[i]?.unit?.base})`,
// scale: "y", scale: `y${i+1}`,
// width: lineWidth, width: lineWidth,
// stroke: cbmode ? "rgb(0,255,0)" : "red", stroke: fixedLineColors[i],
// }); });
// pendingSeries.push({
// label: "max",
// scale: "y",
// width: lineWidth,
// stroke: cbmode ? "rgb(0,0,255)" : "green",
// });
// pendingSeries.push({
// label: usesMeanStatsSeries ? "mean" : "median",
// scale: "y",
// width: lineWidth,
// stroke: "black",
// });
// } else {
for (let i = 0; i < metricData.length; i++) {
// Default
// if (!extendedLegendData) {
pendingSeries.push({
label: `${metricData[i]?.name} (${metricData[i]?.unit?.prefix}${metricData[i]?.unit?.base})`,
scale: `y${i+1}`,
width: lineWidth,
stroke: lineColor(i, metricData.length),
});
// }
// Extended Legend For NodeList
// else {
// pendingSeries.push({
// label:
// scope === "node"
// ? series[i].hostname
// : scope === "accelerator"
// ? 'Acc #' + (i + 1) // series[i].id.slice(9, 14) | Too Hardware Specific
// : scope + " #" + (i + 1),
// scale: "y",
// width: lineWidth,
// stroke: lineColor(i, series?.length),
// values: (u, sidx, idx) => {
// // "i" = "sidx - 1" : sidx contains x-axis-data
// if (idx == null)
// return {
// time: '-',
// value: '-',
// user: '-',
// job: '-'
// };
// if (series[i].id in extendedLegendData) {
// return {
// time: formatDurationTime(plotData[0][idx], forNode),
// value: plotData[sidx][idx],
// user: extendedLegendData[series[i].id].user,
// job: extendedLegendData[series[i].id].job,
// };
// } else {
// return {
// time: formatDurationTime(plotData[0][idx], forNode),
// value: plotData[sidx][idx],
// user: '-',
// job: '-',
// };
// }
// }
// });
// }
// };
}; };
return pendingSeries; return pendingSeries;
}) })
/* Effects */ // Set Options
// $effect(() => { function getOpts(optWidth, optHeight) {
// if (!useStatsSeries && statisticsSeries != null) useStatsSeries = true; let baseOpts = {
// }) width: optWidth,
height: optHeight,
series: plotSeries,
axes: [
{
scale: "x",
incrs: timeIncrs(timestep, maxX, forNode),
values: (_, vals) => vals.map((v) => formatDurationTime(v, forNode)),
},
{
scale: "y1",
grid: { show: true },
values: (u, vals) => vals.map((v) => formatNumber(v)),
},
{
side: 1,
scale: "y2",
grid: { show: false },
values: (u, vals) => vals.map((v) => formatNumber(v)),
},
],
// bands: plotBands,
padding: [5, 10, -20, 0],
hooks: {},
scales: {
x: { time: false },
y1: { auto: true },
y2: { auto: true },
},
legend: {
show: !publicMode,
live: !publicMode
},
cursor: {
drag: { x: true, y: true },
}
}
if (publicMode) {
// X
baseOpts.axes[0].space = 60;
baseOpts.axes[0].font = '16px Arial';
// Y1
baseOpts.axes[1].space = 50;
baseOpts.axes[1].size = 60;
baseOpts.axes[1].font = '16px Arial';
baseOpts.axes[1].stroke = fixedLineColors[0];
// Y2
baseOpts.axes[2].space = 40;
baseOpts.axes[2].size = 60;
baseOpts.axes[2].font = '16px Arial';
baseOpts.axes[2].stroke = fixedLineColors[1];
} else {
baseOpts.title = 'Cluster Utilization';
baseOpts.plugins = [legendAsTooltipPlugin()];
// X
baseOpts.axes[0].label = 'Time';
// Y1
baseOpts.axes[1].label = `${metricData[0]?.name} (${metricData[0]?.unit?.prefix}${metricData[0]?.unit?.base})`;
// Y2
baseOpts.axes[2].label = `${metricData[1]?.name} (${metricData[1]?.unit?.prefix}${metricData[1]?.unit?.base})`;
baseOpts.hooks.draw = [
(u) => {
// Draw plot type label:
let textl = `Cluster ${cluster}`
let textr = `Sums of ${numNodes} nodes`
u.ctx.save();
u.ctx.textAlign = "start"; // 'end'
u.ctx.fillStyle = "black";
u.ctx.fillText(textl, u.bbox.left + 10, u.bbox.top + (forNode ? 0 : 10));
u.ctx.textAlign = "end";
u.ctx.fillStyle = "black";
u.ctx.fillText(
textr,
u.bbox.left + u.bbox.width - 10,
u.bbox.top + (forNode ? 0 : 10),
);
u.ctx.restore();
return;
},
]
}
return baseOpts;
};
/* Effects */
// This updates plot on all size changes if wrapper (== data) exists // This updates plot on all size changes if wrapper (== data) exists
$effect(() => { $effect(() => {
if (plotWrapper) { if (plotWrapper) {
@@ -262,73 +206,6 @@
} }
} }
// removed arg "subcluster": input metricconfig and topology now directly derived from subcluster
// function findJobAggregationThresholds(
// subClusterTopology,
// metricConfig,
// scope,
// numhwthreads,
// numaccs
// ) {
// if (!subClusterTopology || !metricConfig || !scope) {
// console.warn("Argument missing for findJobAggregationThresholds!");
// return null;
// }
// // handle special *-stat scopes
// if (scope.match(/(.*)-stat$/)) {
// const statParts = scope.split('-');
// scope = statParts[0]
// }
// if (metricConfig?.aggregation == "avg") {
// // Return as Configured
// return {
// normal: metricConfig.normal,
// caution: metricConfig.caution,
// alert: metricConfig.alert,
// peak: metricConfig.peak,
// };
// }
// if (metricConfig?.aggregation == "sum") {
// // Scale Thresholds
// let fraction;
// if (numaccs > 0) fraction = subClusterTopology.accelerators.length / numaccs;
// else if (numhwthreads > 0) fraction = subClusterTopology.core.length / numhwthreads;
// else fraction = 1; // Fallback
// let divisor;
// // Exclusive: Fraction = 1; Shared: Fraction > 1
// if (scope == 'node') divisor = fraction;
// // Cap divisor at number of available sockets or domains
// else if (scope == 'socket') divisor = (fraction < subClusterTopology.socket.length) ? subClusterTopology.socket.length : fraction;
// else if (scope == "memoryDomain") divisor = (fraction < subClusterTopology.memoryDomain.length) ? subClusterTopology.socket.length : fraction;
// // Use Maximum Division for Smallest Scopes
// else if (scope == "core") divisor = subClusterTopology.core.length;
// else if (scope == "hwthread") divisor = subClusterTopology.core.length; // alt. name for core
// else if (scope == "accelerator") divisor = subClusterTopology.accelerators.length;
// else {
// console.log('Unknown scope, return default aggregation thresholds for sum', scope)
// divisor = 1;
// }
// return {
// peak: metricConfig.peak / divisor,
// normal: metricConfig.normal / divisor,
// caution: metricConfig.caution / divisor,
// alert: metricConfig.alert / divisor,
// };
// }
// console.warn(
// "Missing or unkown aggregation mode (sum/avg) for metric:",
// metricConfig,
// );
// return null;
// }
// UPLOT PLUGIN // converts the legend into a simple tooltip // UPLOT PLUGIN // converts the legend into a simple tooltip
function legendAsTooltipPlugin({ function legendAsTooltipPlugin({
className, className,
@@ -408,219 +285,22 @@
} }
} }
// RETURN BG COLOR FROM THRESHOLD function onSizeChange(chgWidth, chgHeight) {
// function backgroundColor() {
// if (
// clusterCockpitConfig.plotConfiguration_colorBackground == false ||
// // !thresholds ||
// !(series && series.every((s) => s.statistics != null))
// )
// return backgroundColors.normal;
// let cond =
// thresholds.alert < thresholds.caution
// ? (a, b) => a <= b
// : (a, b) => a >= b;
// let avg =
// series.reduce((sum, series) => sum + series.statistics.avg, 0) /
// series.length;
// if (Number.isNaN(avg)) return backgroundColors.normal;
// if (cond(avg, thresholds.alert)) return backgroundColors.alert;
// if (cond(avg, thresholds.caution)) return backgroundColors.caution;
// return backgroundColors.normal;
// }
function lineColor(i, n) {
if (n && n >= lineColors.length) return lineColors[i % lineColors.length];
else return lineColors[Math.floor((i / n) * lineColors.length)];
}
function render(ren_width, ren_height) {
// Set Options
const opts = {
width,
height,
title: 'Cluster Utilization',
plugins: [legendAsTooltipPlugin()],
series: plotSeries,
axes: [
{
scale: "x",
space: 35,
incrs: timeIncrs(timestep, maxX, forNode),
label: "Time",
values: (_, vals) => vals.map((v) => formatDurationTime(v, forNode)),
},
{
scale: "y1",
grid: { show: true },
label: `${metricData[0]?.name} (${metricData[0]?.unit?.prefix}${metricData[0]?.unit?.base})`,
values: (u, vals) => vals.map((v) => formatNumber(v)),
},
{
side: 1,
scale: "y2",
grid: { show: false },
label: `${metricData[1]?.name} (${metricData[1]?.unit?.prefix}${metricData[1]?.unit?.base})`,
values: (u, vals) => vals.map((v) => formatNumber(v)),
},
],
// bands: plotBands,
padding: [5, 10, -20, 0],
hooks: {
// init: [
// (u) => {
// /* IF Zoom Enabled */
// if (resampleConfig && !forNode) {
// u.over.addEventListener("dblclick", (e) => {
// // console.log('Dispatch: Zoom Reset')
// onZoom({
// lastZoomState: {
// x: { time: false },
// y: { auto: true }
// }
// });
// });
// };
// },
// ],
draw: [
(u) => {
// Draw plot type label:
let textl = `Cluster ${cluster}`
// let textl = `${scope}${plotSeries.length > 2 ? "s" : ""}${
// useStatsSeries
// ? (usesMeanStatsSeries ? ": min/mean/max" : ": min/median/max")
// : metricConfig != null && scope != metricConfig.scope
// ? ` (${metricConfig.aggregation})`
// : ""
// }`;
let textr = `Sums of ${numNodes} nodes`
//let textr = `${isShared && scope != "core" && scope != "accelerator" ? "[Shared]" : ""}`;
u.ctx.save();
u.ctx.textAlign = "start"; // 'end'
u.ctx.fillStyle = "black";
u.ctx.fillText(textl, u.bbox.left + 10, u.bbox.top + (forNode ? 0 : 10));
u.ctx.textAlign = "end";
u.ctx.fillStyle = "black";
u.ctx.fillText(
textr,
u.bbox.left + u.bbox.width - 10,
u.bbox.top + (forNode ? 0 : 10),
);
// u.ctx.fillText(text, u.bbox.left + u.bbox.width - 10, u.bbox.top + u.bbox.height - 10) // Recipe for bottom right
// if (!thresholds) {
u.ctx.restore();
return;
// }
// let y = u.valToPos(thresholds.normal, "y", true);
// u.ctx.save();
// u.ctx.lineWidth = lineWidth;
// u.ctx.strokeStyle = normalLineColor;
// u.ctx.setLineDash([5, 5]);
// u.ctx.beginPath();
// u.ctx.moveTo(u.bbox.left, y);
// u.ctx.lineTo(u.bbox.left + u.bbox.width, y);
// u.ctx.stroke();
// u.ctx.restore();
},
],
// setScale: [
// (u, key) => { // If ZoomResample is Configured && Not System/Node View
// if (resampleConfig && !forNode && key === 'x') {
// const numX = (u.series[0].idxs[1] - u.series[0].idxs[0])
// if (numX <= resampleTrigger && timestep !== resampleMinimum) {
// /* Get closest zoom level; prevents multiple iterative zoom requests for big zoom-steps (e.g. 600 -> 300 -> 120 -> 60) */
// // Which resolution to theoretically request to achieve 30 or more visible data points:
// const target = (numX * timestep) / resampleTrigger
// // Which configured resolution actually matches the closest to theoretical target:
// const closest = resampleResolutions.reduce(function(prev, curr) {
// return (Math.abs(curr - target) < Math.abs(prev - target) ? curr : prev);
// });
// // Prevents non-required dispatches
// if (timestep !== closest) {
// // console.log('Dispatch: Zoom with Res from / to', timestep, closest)
// onZoom({
// newRes: closest,
// lastZoomState: u?.scales,
// lastThreshold: thresholds?.normal
// });
// }
// } else {
// // console.log('Dispatch: Zoom Update States')
// onZoom({
// lastZoomState: u?.scales,
// lastThreshold: thresholds?.normal
// });
// };
// };
// },
// ]
},
scales: {
x: { time: false },
y1: { auto: true },
y1: { auto: true },
},
legend: {
// Display legend until max 12 Y-dataseries
show: true, // metricData.length <= 12 || useStatsSeries,
live: true // But This Plot always for 2 Data-Series
},
cursor: {
drag: { x: true, y: true },
}
};
// Handle Render
if (!uplot) {
opts.width = ren_width;
opts.height = ren_height;
// if (plotSync) {
// opts.cursor.sync = {
// key: plotSync.key,
// scales: ["x", null],
// }
// }
// if (zoomState && metricConfig?.aggregation == "avg") {
// opts.scales = {...zoomState}
// } else if (zoomState && metricConfig?.aggregation == "sum") {
// // Allow Zoom In === Ymin changed
// if (zoomState.y.min !== 0) { // scope change?: only use zoomState if thresholds match
// if ((thresholdState === thresholds?.normal)) { opts.scales = {...zoomState} };
// } // else: reset scaling to default
// }
uplot = new uPlot(opts, plotData, plotWrapper);
} else {
uplot.setSize({ width: ren_width, height: ren_height });
}
}
function onSizeChange(chg_width, chg_height) {
if (!uplot) return;
if (timeoutId != null) clearTimeout(timeoutId); if (timeoutId != null) clearTimeout(timeoutId);
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
timeoutId = null; timeoutId = null;
render(chg_width, chg_height); render(chgWidth, chgHeight);
}, renderSleepTime); }, renderSleepTime);
} }
/* On Mount */ function render(renWidth, renHeight) {
onMount(() => { if (!uplot) {
if (plotWrapper) { let opts = getOpts(renWidth, renHeight);
render(width, height); uplot = new uPlot(opts, plotData, plotWrapper);
} else {
uplot.setSize({ width: renWidth, height: renHeight });
} }
}); }
/* On Destroy */ /* On Destroy */
onDestroy(() => { onDestroy(() => {
@@ -635,8 +315,12 @@
<div bind:this={plotWrapper} bind:clientWidth={width} <div bind:this={plotWrapper} bind:clientWidth={width}
class={forNode ? 'py-2 rounded' : 'rounded'} class={forNode ? 'py-2 rounded' : 'rounded'}
></div> ></div>
{:else if cluster}
<Card body color="warning" class="mx-4"
>Cannot render plot: No series data returned for <code>{cluster}</code>.</Card
>
{:else} {:else}
<Card body color="warning" class="mx-4" <Card body color="warning" class="mx-4"
>Cannot render plot: No series data returned for <code>{cluster}</code></Card >Cannot render plot: No series data returned.</Card
> >
{/if} {/if}

View File

@@ -56,7 +56,6 @@
/* Const Init */ /* Const Init */
const clusterCockpitConfig = getContext("cc-config"); const clusterCockpitConfig = getContext("cc-config");
const resampleConfig = getContext("resampling"); const resampleConfig = getContext("resampling");
const lineColors = clusterCockpitConfig.plotConfiguration_colorScheme;
const lineWidth = clusterCockpitConfig.plotConfiguration_lineWidth / window.devicePixelRatio; const lineWidth = clusterCockpitConfig.plotConfiguration_lineWidth / window.devicePixelRatio;
const cbmode = clusterCockpitConfig?.plotConfiguration_colorblindMode || false; const cbmode = clusterCockpitConfig?.plotConfiguration_colorblindMode || false;
const renderSleepTime = 200; const renderSleepTime = 200;
@@ -200,7 +199,7 @@
: scope + " #" + (i + 1), : scope + " #" + (i + 1),
scale: "y", scale: "y",
width: lineWidth, width: lineWidth,
stroke: lineColor(i, series?.length), stroke: lineColor(i, clusterCockpitConfig.plotConfiguration_colorScheme),
}); });
} }
// Extended Legend For NodeList // Extended Legend For NodeList
@@ -214,7 +213,7 @@
: scope + " #" + (i + 1), : scope + " #" + (i + 1),
scale: "y", scale: "y",
width: lineWidth, width: lineWidth,
stroke: lineColor(i, series?.length), stroke: lineColor(i, clusterCockpitConfig.plotConfiguration_colorScheme),
values: (u, sidx, idx) => { values: (u, sidx, idx) => {
// "i" = "sidx - 1" : sidx contains x-axis-data // "i" = "sidx - 1" : sidx contains x-axis-data
if (idx == null) if (idx == null)
@@ -446,9 +445,8 @@
return backgroundColors.normal; return backgroundColors.normal;
} }
function lineColor(i, n) { function lineColor(index, colors) {
if (n && n >= lineColors.length) return lineColors[i % lineColors.length]; return colors[index % colors.length];
else return lineColors[Math.floor((i / n) * lineColors.length)];
} }
function render(ren_width, ren_height) { function render(ren_width, ren_height) {

View File

@@ -40,8 +40,7 @@
useColors = true, useColors = true,
useLegend = true, useLegend = true,
colorBackground = false, colorBackground = false,
width = 600, height = 300,
height = 380,
} = $props(); } = $props();
/* Const Init */ /* Const Init */
@@ -53,11 +52,12 @@
/* State Init */ /* State Init */
let plotWrapper = $state(null); let plotWrapper = $state(null);
let width = $state(0); // Wrapper Width
let uplot = $state(null); let uplot = $state(null);
/* Effect */ /* Effect */
$effect(() => { $effect(() => {
if (allowSizeChange) sizeChanged(width, height); if (allowSizeChange) onSizeChange(width, height);
}); });
// Copied Example Vars for Uplot Bubble // Copied Example Vars for Uplot Bubble
@@ -517,11 +517,11 @@
} }
// Main Functions // Main Functions
function sizeChanged() { function onSizeChange() {
if (timeoutId != null) clearTimeout(timeoutId); if (timeoutId != null) clearTimeout(timeoutId);
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
timeoutId = null; timeoutId = null;
if (uplot) uplot.destroy(); if (uplot) uplot.destroy(); // Prevents Multi-Render
render(roofData, jobsData, nodesData); render(roofData, jobsData, nodesData);
}, 200); }, 200);
} }
@@ -995,7 +995,7 @@
</script> </script>
{#if roofData != null} {#if roofData != null}
<div bind:this={plotWrapper} class="p-2"></div> <div bind:this={plotWrapper} bind:clientWidth={width} class="p-2"></div>
{:else} {:else}
<Card class="mx-4 my-2" body color="warning">Cannot render roofline: No data!</Card> <Card class="mx-4 my-2" body color="warning">Cannot render roofline: No data!</Card>
{/if} {/if}

View File

@@ -2,7 +2,6 @@
@component Node State/Health Data Stacked Plot Component, based on uPlot; states by timestamp @component Node State/Health Data Stacked Plot Component, based on uPlot; states by timestamp
Properties: Properties:
- `width Number?`: The plot width [Default: 0]
- `height Number?`: The plot height [Default: 300] - `height Number?`: The plot height [Default: 300]
- `data [Array]`: The data object [Default: null] - `data [Array]`: The data object [Default: null]
- `xlabel String?`: Plot X axis label [Default: ""] - `xlabel String?`: Plot X axis label [Default: ""]
@@ -15,12 +14,11 @@
<script> <script>
import uPlot from "uplot"; import uPlot from "uplot";
import { formatUnixTime } from "../units.js"; import { formatUnixTime } from "../units.js";
import { getContext, onMount, onDestroy } from "svelte"; import { getContext, onDestroy } from "svelte";
import { Card } from "@sveltestrap/sveltestrap"; import { Card } from "@sveltestrap/sveltestrap";
/* Svelte 5 Props */ /* Svelte 5 Props */
let { let {
width = 0,
height = 300, height = 300,
data = null, data = null,
xlabel = null, xlabel = null,
@@ -129,17 +127,17 @@
}; };
} }
function getStackedOpts(title, width, height, series, data) { function getStackedOpts(optTitle, optWidth, optHeight, optSeries, optData) {
let opts = { let opts = {
width, width: optWidth,
height, height: optHeight,
title, title: optTitle,
plugins: [legendAsTooltipPlugin()], plugins: [legendAsTooltipPlugin()],
series, series: optSeries,
axes: [ axes: [
{ {
scale: "x", scale: "x",
space: 25, // Tick Spacing // space: 25, // Tick Spacing
rotate: 30, rotate: 30,
show: true, show: true,
label: xlabel, label: xlabel,
@@ -168,25 +166,25 @@
} }
}; };
let stacked = stack(data, i => false); let stacked = stack(optData, i => false);
opts.bands = stacked.bands; opts.bands = stacked.bands;
opts.cursor = opts.cursor || {}; opts.cursor = opts.cursor || {};
opts.cursor.dataIdx = (u, seriesIdx, closestIdx, xValue) => { opts.cursor.dataIdx = (u, seriesIdx, closestIdx, xValue) => {
return data[seriesIdx][closestIdx] == null ? null : closestIdx; return optData[seriesIdx][closestIdx] == null ? null : closestIdx;
}; };
opts.series.forEach(s => { opts.series.forEach(s => {
// Format Time Info from Unix TS to LocalTimeString // Format Time Info from Unix TS to LocalTimeString
s.value = (u, v, si, i) => (si === 0) ? formatUnixTime(data[si][i]) : data[si][i]; s.value = (u, v, si, i) => (si === 0) ? formatUnixTime(optData[si][i]) : optData[si][i];
s.points = s.points || {}; s.points = s.points || {};
// scan raw unstacked data to return only real points // scan raw unstacked optData to return only real points
s.points.filter = (u, seriesIdx, show, gaps) => { s.points.filter = (u, seriesIdx, show, gaps) => {
if (show) { if (show) {
let pts = []; let pts = [];
data[seriesIdx].forEach((v, i) => { optData[seriesIdx].forEach((v, i) => {
v != null && pts.push(i); v != null && pts.push(i);
}); });
return pts; return pts;
@@ -206,7 +204,7 @@
opts.hooks = { opts.hooks = {
setSeries: [ setSeries: [
(u, i) => { (u, i) => {
let stacked = stack(data, i => !u.series[i].show); let stacked = stack(optData, i => !u.series[i].show);
u.delBand(null); u.delBand(null);
stacked.bands.forEach(b => u.addBand(b)); stacked.bands.forEach(b => u.addBand(b));
u.setData(stacked.data); u.setData(stacked.data);
@@ -298,10 +296,11 @@
/* Var Init */ /* Var Init */
let timeoutId = null; let timeoutId = null;
let uplot = null;
/* State Init */ /* State Init */
let plotWrapper = $state(null); let plotWrapper = $state(null);
let width = $state(0); // Wrapper Width
let uplot = $state(null);
/* Effects */ /* Effects */
$effect(() => { $effect(() => {
@@ -311,6 +310,14 @@
}); });
/* Functions */ /* Functions */
function onSizeChange(chg_width, chg_height) {
if (timeoutId != null) clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
timeoutId = null;
render(chg_width, chg_height);
}, 200);
}
function render(ren_width, ren_height) { function render(ren_width, ren_height) {
if (!uplot) { if (!uplot) {
let { opts, data } = getStackedOpts(title, ren_width, ren_height, plotSeries, collectData); let { opts, data } = getStackedOpts(title, ren_width, ren_height, plotSeries, collectData);
@@ -320,22 +327,6 @@
} }
} }
function onSizeChange(chg_width, chg_height) {
if (!uplot) return;
if (timeoutId != null) clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
timeoutId = null;
render(chg_width, chg_height);
}, 200);
}
/* On Mount */
onMount(() => {
if (plotWrapper) {
render(width, height);
}
});
/* On Destroy */ /* On Destroy */
onDestroy(() => { onDestroy(() => {
if (timeoutId != null) clearTimeout(timeoutId); if (timeoutId != null) clearTimeout(timeoutId);

View File

@@ -57,7 +57,7 @@ export function formatUnixTime(t, withDate = false) {
return t; return t;
} else { } else {
if (withDate) return new Date(t * 1000).toLocaleString(); if (withDate) return new Date(t * 1000).toLocaleString();
else return new Date(t * 1000).toLocaleTimeString(); else return new Date(t * 1000).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
} }
} }
} }

View File

@@ -58,10 +58,6 @@
let to = $state(new Date(Date.now())); let to = $state(new Date(Date.now()));
let stackedFrom = $state(Math.floor(Date.now() / 1000) - 14400); let stackedFrom = $state(Math.floor(Date.now() / 1000) - 14400);
let colWidthJobs = $state(0); let colWidthJobs = $state(0);
let colWidthRoof = $state(0);
let colWidthTotals =$state(0);
let colWidthStacked1 = $state(0);
let colWidthStacked2 = $state(0);
/* Derived */ /* Derived */
// States for Stacked charts // States for Stacked charts
@@ -531,13 +527,11 @@
</Col> </Col>
<Col> <!-- Job Roofline --> <Col> <!-- Job Roofline -->
<div bind:clientWidth={colWidthRoof}> <div>
{#key $statusQuery?.data?.jobsMetricStats} {#key $statusQuery?.data?.jobsMetricStats}
<Roofline <Roofline
useColors={true} useColors={true}
allowSizeChange allowSizeChange
width={colWidthRoof}
height={300}
subCluster={clusterInfo?.roofData ? clusterInfo.roofData : null} subCluster={clusterInfo?.roofData ? clusterInfo.roofData : null}
roofData={transformJobsStatsToData($statusQuery?.data?.jobsMetricStats)} roofData={transformJobsStatsToData($statusQuery?.data?.jobsMetricStats)}
jobsData={transformJobsStatsToInfo($statusQuery?.data?.jobsMetricStats)} jobsData={transformJobsStatsToInfo($statusQuery?.data?.jobsMetricStats)}
@@ -547,24 +541,23 @@
</Col> </Col>
<Col> <!-- Total Cluster Metric in Time SUMS--> <Col> <!-- Total Cluster Metric in Time SUMS-->
<div bind:clientWidth={colWidthTotals}> <div>
<DoubleMetric {#key $statusQuery?.data?.clusterMetrics}
width={colWidthTotals} <DoubleMetric
timestep={$statusQuery?.data?.clusterMetrics[0]?.timestep || 60} timestep={$statusQuery?.data?.clusterMetrics[0]?.timestep || 60}
numNodes={$statusQuery?.data?.clusterMetrics?.nodeCount || 0} numNodes={$statusQuery?.data?.clusterMetrics?.nodeCount || 0}
metricData={$statusQuery?.data?.clusterMetrics?.metrics || []} metricData={$statusQuery?.data?.clusterMetrics?.metrics || []}
cluster={presetCluster} cluster={presetCluster}
fixLinewidth={2} />
/> {/key}
</div> </div>
</Col> </Col>
<Col> <!-- Stacked SchedState --> <Col> <!-- Stacked SchedState -->
<div bind:clientWidth={colWidthStacked1}> <div>
{#key $statesTimed?.data?.nodeStates} {#key $statesTimed?.data?.nodeStates}
<Stacked <Stacked
data={$statesTimed?.data?.nodeStates} data={$statesTimed?.data?.nodeStates}
width={colWidthStacked1}
height={330} height={330}
xlabel="Time" xlabel="Time"
ylabel="Nodes" ylabel="Nodes"
@@ -577,11 +570,10 @@
</Col> </Col>
<Col> <!-- Stacked Healthstate --> <Col> <!-- Stacked Healthstate -->
<div bind:clientWidth={colWidthStacked2}> <div>
{#key $statesTimed?.data?.healthStates} {#key $statesTimed?.data?.healthStates}
<Stacked <Stacked
data={$statesTimed?.data?.healthStates} data={$statesTimed?.data?.healthStates}
width={colWidthStacked2}
height={330} height={330}
xlabel="Time" xlabel="Time"
ylabel="Nodes" ylabel="Nodes"

View File

@@ -42,9 +42,6 @@
/* State Init */ /* State Init */
let pieWidth = $state(0); let pieWidth = $state(0);
let stackedWidth1 = $state(0);
let stackedWidth2 = $state(0);
let plotWidths = $state([]);
let from = $state(new Date(Date.now() - 5 * 60 * 1000)); let from = $state(new Date(Date.now() - 5 * 60 * 1000));
let to = $state(new Date(Date.now())); let to = $state(new Date(Date.now()));
let stackedFrom = $state(Math.floor(Date.now() / 1000) - 14400); let stackedFrom = $state(Math.floor(Date.now() / 1000) - 14400);
@@ -414,14 +411,13 @@
{#if $statesTimed.data} {#if $statesTimed.data}
<Row cols={{ md: 2 , sm: 1}} class="mb-3 justify-content-center"> <Row cols={{ md: 2 , sm: 1}} class="mb-3 justify-content-center">
<Col class="px-3 mt-2 mt-lg-0"> <Col class="px-3 mt-2 mt-lg-0">
<div bind:clientWidth={stackedWidth1}> <div>
{#key $statesTimed?.data?.nodeStates} {#key $statesTimed?.data?.nodeStates}
<h4 class="text-center"> <h4 class="text-center">
{cluster.charAt(0).toUpperCase() + cluster.slice(1)} Node States Over Time {cluster.charAt(0).toUpperCase() + cluster.slice(1)} Node States Over Time
</h4> </h4>
<Stacked <Stacked
data={$statesTimed?.data?.nodeStates} data={$statesTimed?.data?.nodeStates}
width={stackedWidth1 * 0.95}
xlabel="Time" xlabel="Time"
ylabel="Nodes" ylabel="Nodes"
yunit = "#Count" yunit = "#Count"
@@ -432,14 +428,13 @@
</div> </div>
</Col> </Col>
<Col class="px-3 mt-2 mt-lg-0"> <Col class="px-3 mt-2 mt-lg-0">
<div bind:clientWidth={stackedWidth2}> <div>
{#key $statesTimed?.data?.healthStates} {#key $statesTimed?.data?.healthStates}
<h4 class="text-center"> <h4 class="text-center">
{cluster.charAt(0).toUpperCase() + cluster.slice(1)} Health States Over Time {cluster.charAt(0).toUpperCase() + cluster.slice(1)} Health States Over Time
</h4> </h4>
<Stacked <Stacked
data={$statesTimed?.data?.healthStates} data={$statesTimed?.data?.healthStates}
width={stackedWidth2 * 0.95}
xlabel="Time" xlabel="Time"
ylabel="Nodes" ylabel="Nodes"
yunit = "#Count" yunit = "#Count"
@@ -628,13 +623,11 @@
</Card> </Card>
</Col> </Col>
<Col class="px-3 mt-2 mt-lg-0"> <Col class="px-3 mt-2 mt-lg-0">
<div bind:clientWidth={plotWidths[i]}> <div>
{#key $statusQuery?.data?.nodeMetrics} {#key $statusQuery?.data?.nodeMetrics}
<Roofline <Roofline
useColors={true} useColors={true}
allowSizeChange allowSizeChange
width={plotWidths[i] - 10}
height={300}
cluster={cluster} cluster={cluster}
subCluster={subCluster} subCluster={subCluster}
roofData={transformNodesStatsToData($statusQuery?.data?.nodeMetrics.filter( roofData={transformNodesStatsToData($statusQuery?.data?.nodeMetrics.filter(
@@ -650,13 +643,11 @@
</div> </div>
</Col> </Col>
<Col class="px-3 mt-2 mt-lg-0"> <Col class="px-3 mt-2 mt-lg-0">
<div bind:clientWidth={plotWidths[i]}> <div>
{#key $statusQuery?.data?.jobsMetricStats} {#key $statusQuery?.data?.jobsMetricStats}
<Roofline <Roofline
useColors={true} useColors={true}
allowSizeChange allowSizeChange
width={plotWidths[i] - 10}
height={300}
subCluster={subCluster} subCluster={subCluster}
roofData={transformJobsStatsToData($statusQuery?.data?.jobsMetricStats.filter( roofData={transformJobsStatsToData($statusQuery?.data?.jobsMetricStats.filter(
(data) => data.subCluster == subCluster.name, (data) => data.subCluster == subCluster.name,