Merge pull request #484 from ClusterCockpit/dev

Dev
This commit is contained in:
Jan Eitzinger
2026-02-07 06:23:44 +01:00
committed by GitHub
25 changed files with 488 additions and 555 deletions

View File

@@ -6,9 +6,7 @@
package metricstore package metricstore
import ( import (
"cmp"
"fmt" "fmt"
"slices"
"time" "time"
cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger" cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger"
@@ -16,25 +14,18 @@ import (
) )
// HealthCheckResponse represents the result of a health check operation. // HealthCheckResponse represents the result of a health check operation.
//
// Status indicates the monitoring state (Full, Partial, Failed).
// Error contains any error encountered during the health check.
type HealthCheckResponse struct { type HealthCheckResponse struct {
Status schema.MonitoringState Status schema.MonitoringState
Error error Error error
} }
// MaxMissingDataPoints is a threshold that allows a node to be healthy with certain number of data points missing. // MaxMissingDataPoints is the threshold for stale data detection.
// Suppose a node does not receive last 5 data points, then healthCheck endpoint will still say a // A buffer is considered healthy if the gap between its last data point
// node is healthy. Anything more than 5 missing points in metrics of the node will deem the node unhealthy. // and the current time is within MaxMissingDataPoints * frequency.
const MaxMissingDataPoints int64 = 5 const MaxMissingDataPoints int64 = 5
// isBufferHealthy checks if a buffer has received data for the last MaxMissingDataPoints. // bufferExists returns true if the buffer is non-nil and contains data.
//
// Returns true if the buffer is healthy (recent data within threshold), false otherwise.
// A nil buffer or empty buffer is considered unhealthy.
func (b *buffer) bufferExists() bool { func (b *buffer) bufferExists() bool {
// Check if the buffer is empty
if b == nil || b.data == nil || len(b.data) == 0 { if b == nil || b.data == nil || len(b.data) == 0 {
return false return false
} }
@@ -42,234 +33,140 @@ func (b *buffer) bufferExists() bool {
return true return true
} }
// isBufferHealthy checks if a buffer has received data for the last MaxMissingDataPoints. // isBufferHealthy returns true if the buffer has recent data within
// // MaxMissingDataPoints * frequency of the current time.
// Returns true if the buffer is healthy (recent data within threshold), false otherwise.
// A nil buffer or empty buffer is considered unhealthy.
func (b *buffer) isBufferHealthy() bool { func (b *buffer) isBufferHealthy() bool {
// Get the last endtime of the buffer
bufferEnd := b.start + b.frequency*int64(len(b.data)) bufferEnd := b.start + b.frequency*int64(len(b.data))
t := time.Now().Unix() t := time.Now().Unix()
// Check if the buffer has recent data (within MaxMissingDataPoints threshold) return t-bufferEnd <= MaxMissingDataPoints*b.frequency
if t-bufferEnd > MaxMissingDataPoints*b.frequency {
return false
}
return true
} }
// MergeUniqueSorted merges two lists, sorts them, and removes duplicates. // collectMetricStatus walks the subtree rooted at l and classifies each
// Requires 'cmp.Ordered' because we need to sort the data. // expected metric into the healthy or degraded map.
func mergeList[string cmp.Ordered](list1, list2 []string) []string {
// 1. Combine both lists
result := append(list1, list2...)
// 2. Sort the combined list
slices.Sort(result)
// 3. Compact removes consecutive duplicates (standard in Go 1.21+)
// e.g. [1, 1, 2, 3, 3] -> [1, 2, 3]
result = slices.Compact(result)
return result
}
// getHealthyMetrics recursively collects healthy and degraded metrics at this level and below.
// //
// A metric is considered: // Classification rules (evaluated per buffer, pessimistic):
// - Healthy: buffer has recent data within MaxMissingDataPoints threshold AND has few/no NaN values // - A single stale buffer marks the metric as degraded permanently.
// - Degraded: buffer exists and has recent data, but contains more than MaxMissingDataPoints NaN values // - A healthy buffer only counts if no stale buffer has been seen.
// // - Metrics absent from the global config or without any buffer remain
// This routine walks the entire subtree starting from the current level. // in neither map and are later reported as missing.
// func (l *Level) collectMetricStatus(m *MemoryStore, expectedMetrics []string, healthy, degraded map[string]bool) {
// Parameters:
// - m: MemoryStore containing the global metric configuration
//
// Returns:
// - []string: Flat list of healthy metric names from this level and all children
// - []string: Flat list of degraded metric names (exist but have too many missing values)
// - error: Non-nil only for internal errors during recursion
//
// The routine mirrors healthCheck() but provides more granular classification:
// - healthCheck() finds problems (stale/missing)
// - getHealthyMetrics() separates healthy from degraded metrics
func (l *Level) getHealthyMetrics(m *MemoryStore, expectedMetrics []string) ([]string, []string, error) {
l.lock.RLock() l.lock.RLock()
defer l.lock.RUnlock() defer l.lock.RUnlock()
globalMetrics := m.Metrics for _, metricName := range expectedMetrics {
if degraded[metricName] {
continue // already degraded, cannot improve
}
mc := m.Metrics[metricName]
b := l.metrics[mc.offset]
if b.bufferExists() {
if !b.isBufferHealthy() {
degraded[metricName] = true
delete(healthy, metricName)
} else if !degraded[metricName] {
healthy[metricName] = true
}
}
}
for _, lvl := range l.children {
lvl.collectMetricStatus(m, expectedMetrics, healthy, degraded)
}
}
// getHealthyMetrics walks the complete subtree rooted at l and classifies
// each expected metric by comparing the collected status against the
// expected list.
//
// Returns:
// - missingList: metrics not found in global config or without any buffer
// - degradedList: metrics with at least one stale buffer in the subtree
func (l *Level) getHealthyMetrics(m *MemoryStore, expectedMetrics []string) ([]string, []string) {
healthy := make(map[string]bool, len(expectedMetrics))
degraded := make(map[string]bool)
l.collectMetricStatus(m, expectedMetrics, healthy, degraded)
missingList := make([]string, 0) missingList := make([]string, 0)
degradedList := make([]string, 0) degradedList := make([]string, 0)
// Phase 1: Check metrics at this level
for _, metricName := range expectedMetrics { for _, metricName := range expectedMetrics {
offset := globalMetrics[metricName].offset if healthy[metricName] {
b := l.metrics[offset] continue
}
if !b.bufferExists() { if degraded[metricName] {
missingList = append(missingList, metricName)
} else if !b.isBufferHealthy() {
degradedList = append(degradedList, metricName) degradedList = append(degradedList, metricName)
} else {
missingList = append(missingList, metricName)
} }
} }
// Phase 2: Recursively check child levels return degradedList, missingList
for _, lvl := range l.children {
childMissing, childDegraded, err := lvl.getHealthyMetrics(m, expectedMetrics)
if err != nil {
return nil, nil, err
}
missingList = mergeList(missingList, childMissing)
degradedList = mergeList(degradedList, childDegraded)
}
return missingList, degradedList, nil
} }
// GetHealthyMetrics returns healthy and degraded metrics for a specific node as flat lists. // GetHealthyMetrics returns missing and degraded metric lists for a node.
// //
// This routine walks the metric tree starting from the specified node selector // It walks the metric tree starting from the node identified by selector
// and collects all metrics that have received data within the last MaxMissingDataPoints // and classifies each expected metric:
// (default: 5 data points). Metrics are classified into two categories: // - Missing: no buffer anywhere in the subtree, or metric not in global config
// - Degraded: at least one stale buffer exists in the subtree
// //
// - Healthy: Buffer has recent data AND contains few/no NaN (missing) values // Metrics present in expectedMetrics but absent from both returned lists
// - Degraded: Buffer has recent data BUT contains more than MaxMissingDataPoints NaN values // are considered fully healthy.
//
// The returned lists include both node-level metrics (e.g., "load", "mem_used") and
// hardware-level metrics (e.g., "cpu_user", "gpu_temp") in flat slices.
//
// Parameters:
// - selector: Hierarchical path to the target node, typically []string{cluster, hostname}.
// Example: []string{"emmy", "node001"} navigates to the "node001" host in the "emmy" cluster.
// The selector must match the hierarchy used during metric ingestion.
//
// Returns:
// - []string: Flat list of healthy metric names (recent data, few missing values)
// - []string: Flat list of degraded metric names (recent data, many missing values)
// - error: Non-nil if the node is not found or internal errors occur
//
// Example usage:
//
// selector := []string{"emmy", "node001"}
// healthyMetrics, degradedMetrics, err := ms.GetHealthyMetrics(selector)
// if err != nil {
// // Node not found or internal error
// return err
// }
// fmt.Printf("Healthy metrics: %v\n", healthyMetrics)
// // Output: ["load", "mem_used", "cpu_user", ...]
// fmt.Printf("Degraded metrics: %v\n", degradedMetrics)
// // Output: ["gpu_temp", "network_rx", ...] (metrics with many NaN values)
//
// Note: This routine provides more granular classification than HealthCheck:
// - HealthCheck reports stale/missing metrics (problems)
// - GetHealthyMetrics separates fully healthy from degraded metrics (quality levels)
func (m *MemoryStore) GetHealthyMetrics(selector []string, expectedMetrics []string) ([]string, []string, error) { func (m *MemoryStore) GetHealthyMetrics(selector []string, expectedMetrics []string) ([]string, []string, error) {
lvl := m.root.findLevel(selector) lvl := m.root.findLevel(selector)
if lvl == nil { if lvl == nil {
return nil, nil, fmt.Errorf("[METRICSTORE]> error while GetHealthyMetrics, host not found: %#v", selector) return nil, nil, fmt.Errorf("[METRICSTORE]> GetHealthyMetrics: host not found: %#v", selector)
} }
missingList, degradedList, err := lvl.getHealthyMetrics(m, expectedMetrics) degradedList, missingList := lvl.getHealthyMetrics(m, expectedMetrics)
if err != nil { return degradedList, missingList, nil
return nil, nil, err
}
return missingList, degradedList, nil
} }
// HealthCheck performs health checks on multiple nodes and returns their monitoring states. // HealthCheck evaluates multiple nodes against a set of expected metrics
// and returns a monitoring state per node.
// //
// This routine provides a batch health check interface that evaluates multiple nodes // States:
// against a specific set of expected metrics. For each node, it determines the overall // - MonitoringStateFull: all expected metrics are healthy
// monitoring state based on which metrics are healthy, degraded, or missing. // - MonitoringStatePartial: some metrics are missing or degraded
// // - MonitoringStateFailed: node not found, or no healthy metrics at all
// Health Status Classification:
// - MonitoringStateFull: All expected metrics are healthy (recent data, few missing values)
// - MonitoringStatePartial: Some metrics are degraded (many missing values) or missing
// - MonitoringStateFailed: Node not found or all expected metrics are missing/stale
//
// Parameters:
// - cluster: Cluster name (first element of selector path)
// - nodes: List of node hostnames to check
// - expectedMetrics: List of metric names that should be present on each node
//
// Returns:
// - map[string]schema.MonitoringState: Map keyed by hostname containing monitoring state for each node
// - error: Non-nil only for internal errors (individual node failures are captured as MonitoringStateFailed)
//
// Example usage:
//
// cluster := "emmy"
// nodes := []string{"node001", "node002", "node003"}
// expectedMetrics := []string{"load", "mem_used", "cpu_user", "cpu_system"}
// healthStates, err := ms.HealthCheck(cluster, nodes, expectedMetrics)
// if err != nil {
// return err
// }
// for hostname, state := range healthStates {
// fmt.Printf("Node %s: %s\n", hostname, state)
// }
//
// Note: This routine is optimized for batch operations where you need to check
// the same set of metrics across multiple nodes.
func (m *MemoryStore) HealthCheck(cluster string, func (m *MemoryStore) HealthCheck(cluster string,
nodes []string, expectedMetrics []string, nodes []string, expectedMetrics []string,
) (map[string]schema.MonitoringState, error) { ) (map[string]schema.MonitoringState, error) {
results := make(map[string]schema.MonitoringState, len(nodes)) results := make(map[string]schema.MonitoringState, len(nodes))
// Create a set of expected metrics for fast lookup
expectedSet := make(map[string]bool, len(expectedMetrics))
for _, metric := range expectedMetrics {
expectedSet[metric] = true
}
// Check each node
for _, hostname := range nodes { for _, hostname := range nodes {
selector := []string{cluster, hostname} selector := []string{cluster, hostname}
status := schema.MonitoringStateFull
healthyCount := 0
degradedCount := 0
missingCount := 0
// Get healthy and degraded metrics for this node degradedList, missingList, err := m.GetHealthyMetrics(selector, expectedMetrics)
missingList, degradedList, err := m.GetHealthyMetrics(selector, expectedMetrics)
if err != nil { if err != nil {
// Node not found or internal error
results[hostname] = schema.MonitoringStateFailed results[hostname] = schema.MonitoringStateFailed
continue continue
} }
missingCount = len(missingList) degradedCount := len(degradedList)
degradedCount = len(degradedList) missingCount := len(missingList)
uniqueList := mergeList(missingList, degradedList)
healthyCount = len(expectedMetrics) - len(uniqueList) healthyCount := len(expectedMetrics) - degradedCount - missingCount
// Debug log missing and degraded metrics
if missingCount > 0 {
cclog.ComponentDebug("metricstore", "HealthCheck: node", hostname, "missing metrics:", missingList)
}
if degradedCount > 0 { if degradedCount > 0 {
cclog.ComponentDebug("metricstore", "HealthCheck: node", hostname, "degraded metrics:", degradedList) cclog.ComponentInfo("metricstore", "HealthCheck: node ", hostname, "degraded metrics:", degradedList)
}
if missingCount > 0 {
cclog.ComponentInfo("metricstore", "HealthCheck: node ", hostname, "missing metrics:", missingList)
} }
// Determine overall health status switch {
if missingCount > 0 || degradedCount > 0 { case degradedCount == 0 && missingCount == 0:
if healthyCount == 0 { results[hostname] = schema.MonitoringStateFull
// No healthy metrics at all case healthyCount == 0:
status = schema.MonitoringStateFailed results[hostname] = schema.MonitoringStateFailed
} else { default:
// Some healthy, some degraded/missing results[hostname] = schema.MonitoringStatePartial
status = schema.MonitoringStatePartial
} }
} }
// else: all metrics healthy, status remains MonitoringStateFull
results[hostname] = status
}
return results, nil return results, nil
} }

View File

@@ -303,39 +303,39 @@ func TestGetHealthyMetrics(t *testing.T) {
name string name string
selector []string selector []string
expectedMetrics []string expectedMetrics []string
wantMissing []string
wantDegraded []string wantDegraded []string
wantMissing []string
wantErr bool wantErr bool
}{ }{
{ {
name: "mixed health states", name: "mixed health states",
selector: []string{"testcluster", "testnode"}, selector: []string{"testcluster", "testnode"},
expectedMetrics: []string{"load", "mem_used", "cpu_user"}, expectedMetrics: []string{"load", "mem_used", "cpu_user"},
wantMissing: []string{"cpu_user"},
wantDegraded: []string{"mem_used"}, wantDegraded: []string{"mem_used"},
wantMissing: []string{"cpu_user"},
wantErr: false, wantErr: false,
}, },
{ {
name: "node not found", name: "node not found",
selector: []string{"testcluster", "nonexistent"}, selector: []string{"testcluster", "nonexistent"},
expectedMetrics: []string{"load"}, expectedMetrics: []string{"load"},
wantMissing: nil,
wantDegraded: nil, wantDegraded: nil,
wantMissing: nil,
wantErr: true, wantErr: true,
}, },
{ {
name: "check only healthy metric", name: "check only healthy metric",
selector: []string{"testcluster", "testnode"}, selector: []string{"testcluster", "testnode"},
expectedMetrics: []string{"load"}, expectedMetrics: []string{"load"},
wantMissing: []string{},
wantDegraded: []string{}, wantDegraded: []string{},
wantMissing: []string{},
wantErr: false, wantErr: false,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
missing, degraded, err := ms.GetHealthyMetrics(tt.selector, tt.expectedMetrics) degraded, missing, err := ms.GetHealthyMetrics(tt.selector, tt.expectedMetrics)
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
t.Errorf("GetHealthyMetrics() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("GetHealthyMetrics() error = %v, wantErr %v", err, tt.wantErr)
@@ -346,17 +346,6 @@ func TestGetHealthyMetrics(t *testing.T) {
return return
} }
// Check missing list
if len(missing) != len(tt.wantMissing) {
t.Errorf("GetHealthyMetrics() missing = %v, want %v", missing, tt.wantMissing)
} else {
for i, m := range tt.wantMissing {
if missing[i] != m {
t.Errorf("GetHealthyMetrics() missing[%d] = %v, want %v", i, missing[i], m)
}
}
}
// Check degraded list // Check degraded list
if len(degraded) != len(tt.wantDegraded) { if len(degraded) != len(tt.wantDegraded) {
t.Errorf("GetHealthyMetrics() degraded = %v, want %v", degraded, tt.wantDegraded) t.Errorf("GetHealthyMetrics() degraded = %v, want %v", degraded, tt.wantDegraded)
@@ -367,6 +356,17 @@ func TestGetHealthyMetrics(t *testing.T) {
} }
} }
} }
// Check missing list
if len(missing) != len(tt.wantMissing) {
t.Errorf("GetHealthyMetrics() missing = %v, want %v", missing, tt.wantMissing)
} else {
for i, m := range tt.wantMissing {
if missing[i] != m {
t.Errorf("GetHealthyMetrics() missing[%d] = %v, want %v", i, missing[i], m)
}
}
}
}) })
} }
} }

View File

@@ -12,6 +12,7 @@ API_USER="demo" # User for JWT generation
# BASE NETWORK CONFIG # BASE NETWORK CONFIG
SERVICE_ADDRESS="http://localhost:8080" SERVICE_ADDRESS="http://localhost:8080"
NATS_SERVER="nats://0.0.0.0:4222" NATS_SERVER="nats://0.0.0.0:4222"
REST_URL="${SERVICE_ADDRESS}/api/write"
# NATS CREDENTIALS # NATS CREDENTIALS
NATS_USER="root" NATS_USER="root"
@@ -27,8 +28,15 @@ JWT_STATIC="eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzU3Nzg4NDQsImlhdCI
ALEX_HOSTS="a0603 a0903 a0832 a0329 a0702 a0122 a1624 a0731 a0224 a0704 a0631 a0225 a0222 a0427 a0603 a0429 a0833 a0705 a0901 a0601 a0227 a0804 a0322 a0226 a0126 a0129 a0605 a0801 a0934 a1622 a0902 a0428 a0537 a1623 a1722 a0228 a0701 a0326 a0327 a0123 a0321 a1621 a0323 a0124 a0534 a0931 a0324 a0933 a0424 a0905 a0128 a0532 a0805 a0521 a0535 a0932 a0127 a0325 a0633 a0831 a0803 a0426 a0425 a0229 a1721 a0602 a0632 a0223 a0422 a0423 a0536 a0328 a0703 anvme7 a0125 a0221 a0604 a0802 a0522 a0531 a0533 a0904" ALEX_HOSTS="a0603 a0903 a0832 a0329 a0702 a0122 a1624 a0731 a0224 a0704 a0631 a0225 a0222 a0427 a0603 a0429 a0833 a0705 a0901 a0601 a0227 a0804 a0322 a0226 a0126 a0129 a0605 a0801 a0934 a1622 a0902 a0428 a0537 a1623 a1722 a0228 a0701 a0326 a0327 a0123 a0321 a1621 a0323 a0124 a0534 a0931 a0324 a0933 a0424 a0905 a0128 a0532 a0805 a0521 a0535 a0932 a0127 a0325 a0633 a0831 a0803 a0426 a0425 a0229 a1721 a0602 a0632 a0223 a0422 a0423 a0536 a0328 a0703 anvme7 a0125 a0221 a0604 a0802 a0522 a0531 a0533 a0904"
FRITZ_HOSTS="f0201 f0202 f0203 f0204 f0205 f0206 f0207 f0208 f0209 f0210 f0211 f0212 f0213 f0214 f0215 f0217 f0218 f0219 f0220 f0221 f0222 f0223 f0224 f0225 f0226 f0227 f0228 f0229 f0230 f0231 f0232 f0233 f0234 f0235 f0236 f0237 f0238 f0239 f0240 f0241 f0242 f0243 f0244 f0245 f0246 f0247 f0248 f0249 f0250 f0251 f0252 f0253 f0254 f0255 f0256 f0257 f0258 f0259 f0260 f0261 f0262 f0263 f0264 f0378" FRITZ_HOSTS="f0201 f0202 f0203 f0204 f0205 f0206 f0207 f0208 f0209 f0210 f0211 f0212 f0213 f0214 f0215 f0217 f0218 f0219 f0220 f0221 f0222 f0223 f0224 f0225 f0226 f0227 f0228 f0229 f0230 f0231 f0232 f0233 f0234 f0235 f0236 f0237 f0238 f0239 f0240 f0241 f0242 f0243 f0244 f0245 f0246 f0247 f0248 f0249 f0250 f0251 f0252 f0253 f0254 f0255 f0256 f0257 f0258 f0259 f0260 f0261 f0262 f0263 f0264 f0378"
METRICS_STD="cpu_load cpu_user flops_any cpu_irq cpu_system ipc cpu_idle cpu_iowait core_power clock" ALEX_METRICS_HWTHREAD="cpu_user flops_any clock core_power ipc"
METRICS_NODE="cpu_irq cpu_load mem_cached net_bytes_in cpu_user cpu_idle nfs4_read mem_used nfs4_write nfs4_total ib_xmit ib_xmit_pkts net_bytes_out cpu_iowait ib_recv cpu_system ib_recv_pkts" ALEX_METRICS_SOCKET="mem_bw cpu_power"
ALEX_METRICS_ACC="acc_utilization acc_mem_used acc_power nv_mem_util nv_temp nv_sm_clock"
ALEX_METRICS_NODE="cpu_load mem_used net_bytes_in net_bytes_out"
FRITZ_METRICS_HWTHREAD="cpu_user flops_any flops_sp flops_dp clock ipc vectorization_ratio"
FRITZ_METRICS_SOCKET="mem_bw cpu_power mem_power"
FRITZ_METRICS_NODE="cpu_load mem_used ib_recv ib_xmit ib_recv_pkts ib_xmit_pkts nfs4_read nfs4_total"
ACCEL_IDS="00000000:49:00.0 00000000:0E:00.0 00000000:D1:00.0 00000000:90:00.0 00000000:13:00.0 00000000:96:00.0 00000000:CC:00.0 00000000:4F:00.0" ACCEL_IDS="00000000:49:00.0 00000000:0E:00.0 00000000:D1:00.0 00000000:90:00.0 00000000:13:00.0 00000000:96:00.0 00000000:CC:00.0 00000000:4F:00.0"
# ========================================== # ==========================================
@@ -36,9 +44,6 @@ ACCEL_IDS="00000000:49:00.0 00000000:0E:00.0 00000000:D1:00.0 00000000:90:00.0 0
# ========================================== # ==========================================
if [ "$CONNECTION_SCOPE" == "INTERNAL" ]; then if [ "$CONNECTION_SCOPE" == "INTERNAL" ]; then
# 1. Set URL for Internal Mode
REST_URL="${SERVICE_ADDRESS}/metricstore/api/write"
# 2. Generate JWT dynamically # 2. Generate JWT dynamically
echo "Setup: INTERNAL mode selected." echo "Setup: INTERNAL mode selected."
echo "Generating JWT for user: $API_USER" echo "Generating JWT for user: $API_USER"
@@ -49,9 +54,6 @@ if [ "$CONNECTION_SCOPE" == "INTERNAL" ]; then
exit 1 exit 1
fi fi
else else
# 1. Set URL for External Mode
REST_URL="${SERVICE_ADDRESS}/api/write"
# 2. Use Static JWT # 2. Use Static JWT
echo "Setup: EXTERNAL mode selected." echo "Setup: EXTERNAL mode selected."
echo "Using static JWT." echo "Using static JWT."
@@ -96,7 +98,7 @@ while [ true ]; do
# 1. ALEX: HWTHREAD # 1. ALEX: HWTHREAD
echo "Generating Alex: hwthread" echo "Generating Alex: hwthread"
{ {
for metric in $METRICS_STD; do for metric in $ALEX_METRICS_HWTHREAD; do
for hostname in $ALEX_HOSTS; do for hostname in $ALEX_HOSTS; do
for id in {0..127}; do for id in {0..127}; do
echo "$metric,cluster=alex,hostname=$hostname,type=hwthread,type-id=$id value=$((1 + RANDOM % 100)).0 $timestamp" echo "$metric,cluster=alex,hostname=$hostname,type=hwthread,type-id=$id value=$((1 + RANDOM % 100)).0 $timestamp"
@@ -109,7 +111,7 @@ while [ true ]; do
# 2. FRITZ: HWTHREAD # 2. FRITZ: HWTHREAD
echo "Generating Fritz: hwthread" echo "Generating Fritz: hwthread"
{ {
for metric in $METRICS_STD; do for metric in $FRITZ_METRICS_HWTHREAD; do
for hostname in $FRITZ_HOSTS; do for hostname in $FRITZ_HOSTS; do
for id in {0..71}; do for id in {0..71}; do
echo "$metric,cluster=fritz,hostname=$hostname,type=hwthread,type-id=$id value=$((1 + RANDOM % 100)).0 $timestamp" echo "$metric,cluster=fritz,hostname=$hostname,type=hwthread,type-id=$id value=$((1 + RANDOM % 100)).0 $timestamp"
@@ -122,7 +124,7 @@ while [ true ]; do
# 3. ALEX: ACCELERATOR # 3. ALEX: ACCELERATOR
echo "Generating Alex: accelerator" echo "Generating Alex: accelerator"
{ {
for metric in $METRICS_STD; do for metric in $ALEX_METRICS_ACC; do
for hostname in $ALEX_HOSTS; do for hostname in $ALEX_HOSTS; do
for id in $ACCEL_IDS; do for id in $ACCEL_IDS; do
echo "$metric,cluster=alex,hostname=$hostname,type=accelerator,type-id=$id value=$((1 + RANDOM % 100)).0 $timestamp" echo "$metric,cluster=alex,hostname=$hostname,type=accelerator,type-id=$id value=$((1 + RANDOM % 100)).0 $timestamp"
@@ -132,23 +134,10 @@ while [ true ]; do
} > sample_alex.txt } > sample_alex.txt
send_payload "sample_alex.txt" "alex" send_payload "sample_alex.txt" "alex"
# 4. ALEX: MEMORY DOMAIN
echo "Generating Alex: memoryDomain"
{
for metric in $METRICS_STD; do
for hostname in $ALEX_HOSTS; do
for id in {0..7}; do
echo "$metric,cluster=alex,hostname=$hostname,type=memoryDomain,type-id=$id value=$((1 + RANDOM % 100)).0 $timestamp"
done
done
done
} > sample_alex.txt
send_payload "sample_alex.txt" "alex"
# 5. ALEX: SOCKET # 5. ALEX: SOCKET
echo "Generating Alex: socket" echo "Generating Alex: socket"
{ {
for metric in $METRICS_STD; do for metric in $ALEX_METRICS_SOCKET; do
for hostname in $ALEX_HOSTS; do for hostname in $ALEX_HOSTS; do
for id in {0..1}; do for id in {0..1}; do
echo "$metric,cluster=alex,hostname=$hostname,type=socket,type-id=$id value=$((1 + RANDOM % 100)).0 $timestamp" echo "$metric,cluster=alex,hostname=$hostname,type=socket,type-id=$id value=$((1 + RANDOM % 100)).0 $timestamp"
@@ -161,7 +150,7 @@ while [ true ]; do
# 6. FRITZ: SOCKET # 6. FRITZ: SOCKET
echo "Generating Fritz: socket" echo "Generating Fritz: socket"
{ {
for metric in $METRICS_STD; do for metric in $FRITZ_METRICS_SOCKET; do
for hostname in $FRITZ_HOSTS; do for hostname in $FRITZ_HOSTS; do
for id in {0..1}; do for id in {0..1}; do
echo "$metric,cluster=fritz,hostname=$hostname,type=socket,type-id=$id value=$((1 + RANDOM % 100)).0 $timestamp" echo "$metric,cluster=fritz,hostname=$hostname,type=socket,type-id=$id value=$((1 + RANDOM % 100)).0 $timestamp"
@@ -174,7 +163,7 @@ while [ true ]; do
# 7. ALEX: NODE # 7. ALEX: NODE
echo "Generating Alex: node" echo "Generating Alex: node"
{ {
for metric in $METRICS_NODE; do for metric in $ALEX_METRICS_NODE; do
for hostname in $ALEX_HOSTS; do for hostname in $ALEX_HOSTS; do
echo "$metric,cluster=alex,hostname=$hostname,type=node value=$((1 + RANDOM % 100)).0 $timestamp" echo "$metric,cluster=alex,hostname=$hostname,type=node value=$((1 + RANDOM % 100)).0 $timestamp"
done done
@@ -185,7 +174,7 @@ while [ true ]; do
# 8. FRITZ: NODE # 8. FRITZ: NODE
echo "Generating Fritz: node" echo "Generating Fritz: node"
{ {
for metric in $METRICS_NODE; do for metric in $FRITZ_METRICS_NODE; do
for hostname in $FRITZ_HOSTS; do for hostname in $FRITZ_HOSTS; do
echo "$metric,cluster=fritz,hostname=$hostname,type=node value=$((1 + RANDOM % 100)).0 $timestamp" echo "$metric,cluster=fritz,hostname=$hostname,type=node value=$((1 + RANDOM % 100)).0 $timestamp"
done done

View File

@@ -68,12 +68,8 @@
energyFootprint { hardware, metric, value } energyFootprint { hardware, metric, value }
} }
`); `);
const client = getContextClient();
const ccconfig = getContext("cc-config");
const showRoofline = !!ccconfig[`jobView_showRoofline`];
const showStatsTable = !!ccconfig[`jobView_showStatTable`];
/* Note: Actual metric data queried in <Metric> Component, only require base infos here -> reduce backend load by requesting just stats */ /* Note: Actual metric data queried in <Metric> Component, only require base infos here -> reduce backend load by requesting just stats */
const client = getContextClient();
const query = gql` const query = gql`
query ($dbid: ID!, $selectedMetrics: [String!]!, $selectedScopes: [MetricScope!]!) { query ($dbid: ID!, $selectedMetrics: [String!]!, $selectedScopes: [MetricScope!]!) {
scopedJobStats(id: $dbid, metrics: $selectedMetrics, scopes: $selectedScopes) { scopedJobStats(id: $dbid, metrics: $selectedMetrics, scopes: $selectedScopes) {
@@ -89,12 +85,56 @@
/* State Init */ /* State Init */
let plots = $state({}); let plots = $state({});
let isMetricsSelectionOpen = $state(false); let isMetricsSelectionOpen = $state(false);
let selectedMetrics = $state([]);
let selectedScopes = $state([]);
let totalMetrics = $state(0); let totalMetrics = $state(0);
/* Derived */ /* Derived Init Return */
const showSummary = $derived((!!ccconfig[`jobView_showFootprint`] || !!ccconfig[`jobView_showPolarPlot`])) const thisJob = $derived($initq?.data ? $initq.data.job : null);
/* Derived Settings */
const globalMetrics = $derived(thisJob ? getContext("globalMetrics") : null);
const clusterInfo = $derived(thisJob ? getContext("clusters") : null);
const ccconfig = $derived(thisJob ? getContext("cc-config") : null);
const showRoofline = $derived(ccconfig ? !!ccconfig[`jobView_showRoofline`] : false);
const showStatsTable = $derived(ccconfig ? !!ccconfig[`jobView_showStatTable`] : false);
const showSummary = $derived(ccconfig ? (!!ccconfig[`jobView_showFootprint`] || !!ccconfig[`jobView_showPolarPlot`]) : false)
/* Derived Var Preprocessing*/
let selectedMetrics = $derived.by(() => {
if(thisJob && ccconfig) {
if (thisJob.cluster) {
if (thisJob.subCluster) {
return ccconfig[`metricConfig_jobViewPlotMetrics:${thisJob.cluster}:${thisJob.subCluster}`] ||
ccconfig[`metricConfig_jobViewPlotMetrics:${thisJob.cluster}`] ||
ccconfig.metricConfig_jobViewPlotMetrics
}
return ccconfig[`metricConfig_jobViewPlotMetrics:${thisJob.cluster}`] ||
ccconfig.metricConfig_jobViewPlotMetrics
}
return ccconfig.metricConfig_jobViewPlotMetrics
}
return [];
});
let selectedScopes = $derived.by(() => {
const pendingScopes = ["node"]
if (thisJob) {
const accScopeDefault = [...selectedMetrics].some(function (m) {
const thisCluster = clusterInfo.find((c) => c.name == thisJob.cluster);
const subCluster = thisCluster.subClusters.find((sc) => sc.name == thisJob.subCluster);
return subCluster.metricConfig.find((smc) => smc.name == m)?.scope === "accelerator";
});
if (accScopeDefault) pendingScopes.push("accelerator")
if (thisJob.numNodes === 1) {
pendingScopes.push("socket")
pendingScopes.push("core")
}
}
return[...new Set(pendingScopes)];
});
/* Derived Query and Postprocessing*/
const jobMetrics = $derived(queryStore({ const jobMetrics = $derived(queryStore({
client: client, client: client,
query: query, query: query,
@@ -103,11 +143,10 @@
); );
const missingMetrics = $derived.by(() => { const missingMetrics = $derived.by(() => {
if ($initq?.data && $jobMetrics?.data) { if (thisJob && $jobMetrics?.data) {
let job = $initq.data.job;
let metrics = $jobMetrics.data.scopedJobStats; let metrics = $jobMetrics.data.scopedJobStats;
let metricNames = $initq.data.globalMetrics.reduce((names, gm) => { let metricNames = globalMetrics.reduce((names, gm) => {
if (gm.availability.find((av) => av.cluster === job.cluster)) { if (gm.availability.find((av) => av.cluster === thisJob.cluster)) {
names.push(gm.name); names.push(gm.name);
} }
return names; return names;
@@ -118,9 +157,10 @@
!metrics.some((jm) => jm.name == metric) && !metrics.some((jm) => jm.name == metric) &&
selectedMetrics.includes(metric) && selectedMetrics.includes(metric) &&
!checkMetricDisabled( !checkMetricDisabled(
globalMetrics,
metric, metric,
$initq.data.job.cluster, thisJob.cluster,
$initq.data.job.subCluster, thisJob.subCluster,
), ),
); );
} else { } else {
@@ -129,17 +169,16 @@
}); });
const missingHosts = $derived.by(() => { const missingHosts = $derived.by(() => {
if ($initq?.data && $jobMetrics?.data) { if (thisJob && $jobMetrics?.data) {
let job = $initq.data.job;
let metrics = $jobMetrics.data.scopedJobStats; let metrics = $jobMetrics.data.scopedJobStats;
let metricNames = $initq.data.globalMetrics.reduce((names, gm) => { let metricNames = globalMetrics.reduce((names, gm) => {
if (gm.availability.find((av) => av.cluster === job.cluster)) { if (gm.availability.find((av) => av.cluster === thisJob.cluster)) {
names.push(gm.name); names.push(gm.name);
} }
return names; return names;
}, []); }, []);
return job.resources return thisJob.resources
.map(({ hostname }) => ({ .map(({ hostname }) => ({
hostname: hostname, hostname: hostname,
metrics: metricNames.filter( metrics: metricNames.filter(
@@ -165,51 +204,19 @@
? "Loading..." ? "Loading..."
: $initq?.error : $initq?.error
? "Error" ? "Error"
: `Job ${$initq.data.job.jobId} - ClusterCockpit`; : `Job ${thisJob.jobId} - ClusterCockpit`;
});
/* On Init */
getContext("on-init")(() => {
let job = $initq.data.job;
if (!job) return;
const pendingMetrics = (
ccconfig[`metricConfig_jobViewPlotMetrics:${job.cluster}:${job.subCluster}`] ||
ccconfig[`metricConfig_jobViewPlotMetrics:${job.cluster}`]
) ||
$initq.data.globalMetrics.reduce((names, gm) => {
if (gm.availability.find((av) => av.cluster === job.cluster && av.subClusters.includes(job.subCluster))) {
names.push(gm.name);
}
return names;
}, [])
// Select default Scopes to load: Check before if any metric has accelerator scope by default
const accScopeDefault = [...pendingMetrics].some(function (m) {
const cluster = $initq.data.clusters.find((c) => c.name == job.cluster);
const subCluster = cluster.subClusters.find((sc) => sc.name == job.subCluster);
return subCluster.metricConfig.find((smc) => smc.name == m)?.scope === "accelerator";
});
const pendingScopes = ["node"]
if (accScopeDefault) pendingScopes.push("accelerator")
if (job.numNodes === 1) {
pendingScopes.push("socket")
pendingScopes.push("core")
}
selectedMetrics = [...new Set(pendingMetrics)];
selectedScopes = [...new Set(pendingScopes)];
}); });
/* Functions */ /* Functions */
const orderAndMap = (grouped, selectedMetrics) => const orderAndMap = (grouped, inputMetrics) =>
selectedMetrics.map((metric) => ({ inputMetrics.map((metric) => ({
metric: metric, metric: metric,
data: grouped.find((group) => group[0].name == metric), data: grouped.find((group) => group[0].name == metric),
disabled: checkMetricDisabled( disabled: checkMetricDisabled(
globalMetrics,
metric, metric,
$initq.data.job.cluster, thisJob.cluster,
$initq.data.job.subCluster, thisJob.subCluster,
), ),
})); }));
</script> </script>
@@ -219,34 +226,34 @@
<Col xs={12} md={6} xl={3} class="mb-3 mb-xxl-0"> <Col xs={12} md={6} xl={3} class="mb-3 mb-xxl-0">
{#if $initq.error} {#if $initq.error}
<Card body color="danger">{$initq.error.message}</Card> <Card body color="danger">{$initq.error.message}</Card>
{:else if $initq?.data} {:else if thisJob}
<Card class="overflow-auto" style="height: auto;"> <Card class="overflow-auto" style="height: auto;">
<TabContent> <!-- on:tab={(e) => (status = e.detail)} --> <TabContent> <!-- on:tab={(e) => (status = e.detail)} -->
{#if $initq.data?.job?.metaData?.message} {#if thisJob?.metaData?.message}
<TabPane tabId="admin-msg" tab="Admin Note" active> <TabPane tabId="admin-msg" tab="Admin Note" active>
<CardBody> <CardBody>
<Card body class="mb-2" color="warning"> <Card body class="mb-2" color="warning">
<h5>Job {$initq.data?.job?.jobId} ({$initq.data?.job?.cluster})</h5> <h5>Job {thisJob?.jobId} ({thisJob?.cluster})</h5>
The following note was added by administrators: The following note was added by administrators:
</Card> </Card>
<Card body> <Card body>
{@html $initq.data.job.metaData.message} {@html thisJob.metaData.message}
</Card> </Card>
</CardBody> </CardBody>
</TabPane> </TabPane>
{/if} {/if}
<TabPane tabId="meta-info" tab="Job Info" active={$initq.data?.job?.metaData?.message?false:true}> <TabPane tabId="meta-info" tab="Job Info" active={thisJob?.metaData?.message?false:true}>
<CardBody class="pb-2"> <CardBody class="pb-2">
<JobInfo job={$initq.data.job} {username} {authlevel} {roles} showTagEdit/> <JobInfo job={thisJob} {username} {authlevel} {roles} showTagEdit/>
</CardBody> </CardBody>
</TabPane> </TabPane>
{#if $initq.data.job.concurrentJobs != null && $initq.data.job.concurrentJobs.items.length != 0} {#if thisJob.concurrentJobs != null && thisJob.concurrentJobs.items.length != 0}
<TabPane tabId="shared-jobs"> <TabPane tabId="shared-jobs">
<span slot="tab"> <span slot="tab">
{$initq.data.job.concurrentJobs.items.length} Concurrent Jobs {thisJob.concurrentJobs.items.length} Concurrent Jobs
</span> </span>
<CardBody> <CardBody>
<ConcurrentJobs cJobs={$initq.data.job.concurrentJobs} showLinks={(authlevel > roles.manager)}/> <ConcurrentJobs cJobs={thisJob.concurrentJobs} showLinks={(authlevel > roles.manager)}/>
</CardBody> </CardBody>
</TabPane> </TabPane>
{/if} {/if}
@@ -261,9 +268,9 @@
<Col xs={12} md={6} xl={4} xxl={3} class="mb-3 mb-xxl-0"> <Col xs={12} md={6} xl={4} xxl={3} class="mb-3 mb-xxl-0">
{#if $initq.error} {#if $initq.error}
<Card body color="danger">{$initq.error.message}</Card> <Card body color="danger">{$initq.error.message}</Card>
{:else if $initq?.data} {:else if thisJob}
{#if showSummary} {#if showSummary}
<JobSummary job={$initq.data.job}/> <JobSummary job={thisJob}/>
{/if} {/if}
{:else} {:else}
<Spinner secondary /> <Spinner secondary />
@@ -274,9 +281,9 @@
<Col xs={12} md={12} xl={5} xxl={6}> <Col xs={12} md={12} xl={5} xxl={6}>
{#if $initq.error} {#if $initq.error}
<Card body color="danger">{$initq.error.message}</Card> <Card body color="danger">{$initq.error.message}</Card>
{:else if $initq?.data} {:else if thisJob}
{#if showRoofline} {#if showRoofline}
<JobRoofline job={$initq.data.job} clusters={$initq.data.clusters}/> <JobRoofline job={thisJob} {clusterInfo}/>
{/if} {/if}
{:else} {:else}
<Spinner secondary /> <Spinner secondary />
@@ -285,10 +292,10 @@
</Row> </Row>
<!-- Row 2: Energy Information if available --> <!-- Row 2: Energy Information if available -->
{#if $initq?.data && $initq.data.job.energyFootprint.length != 0} {#if thisJob && thisJob?.energyFootprint?.length != 0}
<Row class="mb-3"> <Row class="mb-3">
<Col> <Col>
<EnergySummary jobId={$initq.data.job.jobId} jobEnergy={$initq.data.job.energy} jobEnergyFootprint={$initq.data.job.energyFootprint}/> <EnergySummary jobId={thisJob.jobId} jobEnergy={thisJob.energy} jobEnergyFootprint={thisJob.energyFootprint}/>
</Col> </Col>
</Row> </Row>
{/if} {/if}
@@ -297,7 +304,7 @@
<Card class="mb-3"> <Card class="mb-3">
<CardBody> <CardBody>
<Row class="mb-2"> <Row class="mb-2">
{#if $initq?.data} {#if thisJob}
<Col xs="auto"> <Col xs="auto">
<Button outline onclick={() => (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)
@@ -310,7 +317,7 @@
{#if $jobMetrics.error} {#if $jobMetrics.error}
<Row class="mt-2"> <Row class="mt-2">
<Col> <Col>
{#if $initq?.data && ($initq.data.job?.monitoringStatus == 0 || $initq.data.job?.monitoringStatus == 2)} {#if thisJob && (thisJob?.monitoringStatus == 0 || thisJob?.monitoringStatus == 2)}
<Card body color="warning">Not monitored or archiving failed</Card> <Card body color="warning">Not monitored or archiving failed</Card>
<br /> <br />
{/if} {/if}
@@ -323,18 +330,18 @@
<Spinner secondary /> <Spinner secondary />
</Col> </Col>
</Row> </Row>
{:else if $initq?.data && $jobMetrics?.data?.scopedJobStats} {:else if thisJob && $jobMetrics?.data?.scopedJobStats}
<!-- Note: Ignore '#snippet' Error in IDE --> <!-- Note: Ignore '#snippet' Error in IDE -->
{#snippet gridContent(item)} {#snippet gridContent(item)}
{#if item.data} {#if item.data}
<Metric <Metric
bind:this={plots[item.metric]} bind:this={plots[item.metric]}
job={$initq.data.job} job={thisJob}
metricName={item.metric} metricName={item.metric}
metricUnit={$initq.data.globalMetrics.find((gm) => gm.name == item.metric)?.unit} metricUnit={globalMetrics.find((gm) => gm.name == item.metric)?.unit}
nativeScope={$initq.data.globalMetrics.find((gm) => gm.name == item.metric)?.scope} nativeScope={globalMetrics.find((gm) => gm.name == item.metric)?.scope}
presetScopes={item.data.map((x) => x.scope)} presetScopes={item.data.map((x) => x.scope)}
isShared={$initq.data.job.shared != "none"} isShared={thisJob.shared != "none"}
/> />
{:else if item.disabled == true} {:else if item.disabled == true}
<Card color="info"> <Card color="info">
@@ -342,7 +349,7 @@
<b>Disabled Metric</b> <b>Disabled Metric</b>
</CardHeader> </CardHeader>
<CardBody> <CardBody>
<p>Metric <b>{item.metric}</b> is disabled for cluster <b>{$initq.data.job.cluster}:{$initq.data.job.subCluster}</b>.</p> <p>Metric <b>{item.metric}</b> is disabled for cluster <b>{thisJob.cluster}:{thisJob.subCluster}</b>.</p>
<p class="mb-1">To remove this card, open metric selection and press "Close and Apply".</p> <p class="mb-1">To remove this card, open metric selection and press "Close and Apply".</p>
</CardBody> </CardBody>
</Card> </Card>
@@ -353,7 +360,7 @@
</CardHeader> </CardHeader>
<CardBody> <CardBody>
<p>No dataset(s) returned for <b>{item.metric}</b>.</p> <p>No dataset(s) returned for <b>{item.metric}</b>.</p>
<p class="mb-1">Metric was not found in metric store for cluster <b>{$initq.data.job.cluster}</b>.</p> <p class="mb-1">Metric was not found in metric store for cluster <b>{thisJob.cluster}</b>.</p>
</CardBody> </CardBody>
</Card> </Card>
{/if} {/if}
@@ -374,7 +381,7 @@
<!-- Metadata && Statistcics Table --> <!-- Metadata && Statistcics Table -->
<Row class="mb-3"> <Row class="mb-3">
<Col> <Col>
{#if $initq?.data} {#if thisJob}
<Card> <Card>
<TabContent> <TabContent>
{#if somethingMissing} {#if somethingMissing}
@@ -409,12 +416,12 @@
{/if} {/if}
{#if showStatsTable} {#if showStatsTable}
<!-- Includes <TabPane> Statistics Table with Independent GQL Query --> <!-- Includes <TabPane> Statistics Table with Independent GQL Query -->
<StatsTab job={$initq.data.job} clusters={$initq.data.clusters} tabActive={!somethingMissing}/> <StatsTab job={thisJob} {clusterInfo} {globalMetrics} {ccconfig} tabActive={!somethingMissing}/>
{/if} {/if}
<TabPane tabId="job-script" tab="Job Script"> <TabPane tabId="job-script" tab="Job Script">
<div class="pre-wrapper"> <div class="pre-wrapper">
{#if $initq.data.job.metaData?.jobScript} {#if thisJob.metaData?.jobScript}
<pre><code>{$initq.data.job.metaData?.jobScript}</code></pre> <pre><code>{thisJob.metaData?.jobScript}</code></pre>
{:else} {:else}
<Card body color="warning">No job script available</Card> <Card body color="warning">No job script available</Card>
{/if} {/if}
@@ -422,8 +429,8 @@
</TabPane> </TabPane>
<TabPane tabId="slurm-info" tab="Slurm Info"> <TabPane tabId="slurm-info" tab="Slurm Info">
<div class="pre-wrapper"> <div class="pre-wrapper">
{#if $initq.data.job.metaData?.slurmInfo} {#if thisJob.metaData?.slurmInfo}
<pre><code>{$initq.data.job.metaData?.slurmInfo}</code></pre> <pre><code>{thisJob.metaData?.slurmInfo}</code></pre>
{:else} {:else}
<Card body color="warning" <Card body color="warning"
>No additional slurm information available</Card >No additional slurm information available</Card
@@ -437,15 +444,15 @@
</Col> </Col>
</Row> </Row>
{#if $initq?.data} {#if thisJob}
<MetricSelection <MetricSelection
bind:isOpen={isMetricsSelectionOpen} bind:isOpen={isMetricsSelectionOpen}
bind:totalMetrics bind:totalMetrics
presetMetrics={selectedMetrics} presetMetrics={selectedMetrics}
cluster={$initq.data.job.cluster} cluster={thisJob.cluster}
subCluster={$initq.data.job.subCluster} subCluster={thisJob.subCluster}
configName="metricConfig_jobViewPlotMetrics" configName="metricConfig_jobViewPlotMetrics"
preInitialized {globalMetrics}
applyMetrics={(newMetrics) => applyMetrics={(newMetrics) =>
selectedMetrics = [...newMetrics] selectedMetrics = [...newMetrics]
} }

View File

@@ -36,7 +36,6 @@
/* Const Init */ /* Const Init */
const { query: initq } = init(); const { query: initq } = init();
const ccconfig = getContext("cc-config");
const matchedJobCompareLimit = 500; const matchedJobCompareLimit = 500;
/* State Init */ /* State Init */
@@ -52,11 +51,17 @@
let isMetricsSelectionOpen = $state(false); let isMetricsSelectionOpen = $state(false);
let sorting = $state({ field: "startTime", type: "col", order: "DESC" }); let sorting = $state({ field: "startTime", type: "col", order: "DESC" });
/* Derived Init Return */
const thisInit = $derived($initq?.data ? true : false);
/* Derived */ /* Derived */
const ccconfig = $derived(thisInit ? getContext("cc-config") : null);
const globalMetrics = $derived(thisInit ? getContext("globalMetrics") : null);
let presetProject = $derived(filterPresets?.project ? filterPresets.project : ""); let presetProject = $derived(filterPresets?.project ? filterPresets.project : "");
let selectedCluster = $derived(filterPresets?.cluster ? filterPresets.cluster : null); let selectedCluster = $derived(filterPresets?.cluster ? filterPresets.cluster : null);
let selectedSubCluster = $derived(filterPresets?.partition ? filterPresets.partition : null); let selectedSubCluster = $derived(filterPresets?.partition ? filterPresets.partition : null);
let metrics = $derived.by(() => { let metrics = $derived.by(() => {
if (thisInit && ccconfig) {
if (selectedCluster) { if (selectedCluster) {
if (selectedSubCluster) { if (selectedSubCluster) {
return ccconfig[`metricConfig_jobListMetrics:${selectedCluster}:${selectedSubCluster}`] || return ccconfig[`metricConfig_jobListMetrics:${selectedCluster}:${selectedSubCluster}`] ||
@@ -67,11 +72,15 @@
ccconfig.metricConfig_jobListMetrics ccconfig.metricConfig_jobListMetrics
} }
return ccconfig.metricConfig_jobListMetrics return ccconfig.metricConfig_jobListMetrics
}
return [];
}); });
let showFootprint = $derived(selectedCluster let showFootprint = $derived((thisInit && ccconfig)
? !!ccconfig[`jobList_showFootprint:${selectedCluster}`] ? selectedCluster
: !!ccconfig.jobList_showFootprint ? ccconfig[`jobList_showFootprint:${selectedCluster}`]
: ccconfig.jobList_showFootprint
: {}
); );
/* Functions */ /* Functions */
@@ -219,6 +228,7 @@
<Sorting <Sorting
bind:isOpen={isSortingOpen} bind:isOpen={isSortingOpen}
presetSorting={sorting} presetSorting={sorting}
{globalMetrics}
applySorting={(newSort) => applySorting={(newSort) =>
sorting = {...newSort} sorting = {...newSort}
} }
@@ -232,6 +242,7 @@
subCluster={selectedSubCluster} subCluster={selectedSubCluster}
configName="metricConfig_jobListMetrics" configName="metricConfig_jobListMetrics"
footprintSelect footprintSelect
{globalMetrics}
applyMetrics={(newMetrics) => applyMetrics={(newMetrics) =>
metrics = [...newMetrics] metrics = [...newMetrics]
} }

View File

@@ -49,14 +49,10 @@
/* Const Init */ /* Const Init */
const { query: initq } = init(); const { query: initq } = init();
const initialized = getContext("initialized") const client = getContextClient();
const globalMetrics = getContext("globalMetrics")
const ccconfig = getContext("cc-config");
const clusters = getContext("clusters");
const nowEpoch = Date.now(); const nowEpoch = Date.now();
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" };
const client = getContextClient();
const nodeMetricsQuery = gql` const nodeMetricsQuery = gql`
query ($cluster: String!, $nodes: [String!], $from: Time!, $to: Time!) { query ($cluster: String!, $nodes: [String!], $from: Time!, $to: Time!) {
nodeMetrics(cluster: $cluster, nodes: $nodes, from: $from, to: $to) { nodeMetrics(cluster: $cluster, nodes: $nodes, from: $from, to: $to) {
@@ -112,14 +108,32 @@
let from = $state(presetFrom ? presetFrom : new Date(nowEpoch - (4 * 3600 * 1000))); let from = $state(presetFrom ? presetFrom : new Date(nowEpoch - (4 * 3600 * 1000)));
// svelte-ignore state_referenced_locally // svelte-ignore state_referenced_locally
let to = $state(presetTo ? presetTo : new Date(nowEpoch)); let to = $state(presetTo ? presetTo : new Date(nowEpoch));
let systemUnits = $state({});
/* Derived Init Return */
const thisInit = $derived($initq?.data ? true : false);
/* Derived */ /* Derived */
const ccconfig = $derived(thisInit ? getContext("cc-config") : null);
const globalMetrics = $derived(thisInit ? getContext("globalMetrics") : null);
const clusterInfos = $derived(thisInit ? getContext("clusters") : null);
const filter = $derived([ const filter = $derived([
{ cluster: { eq: cluster } }, { cluster: { eq: cluster } },
{ node: { contains: hostname } }, { node: { contains: hostname } },
{ state: ["running"] }, { state: ["running"] },
]); ]);
const systemUnits = $derived.by(() => {
const pendingUnits = {};
if (thisInit) {
const systemMetrics = [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster))]
for (let sm of systemMetrics) {
pendingUnits[sm.name] = (sm?.unit?.prefix ? sm.unit.prefix : "") + (sm?.unit?.base ? sm.unit.base : "")
}
}
return {...pendingUnits};
});
const nodeMetricsData = $derived(queryStore({ const nodeMetricsData = $derived(queryStore({
client: client, client: client,
query: nodeMetricsQuery, query: nodeMetricsQuery,
@@ -140,20 +154,6 @@
); );
const thisNodeState = $derived($nodeMetricsData?.data?.nodeMetrics[0]?.state ? $nodeMetricsData.data.nodeMetrics[0].state : 'notindb'); const thisNodeState = $derived($nodeMetricsData?.data?.nodeMetrics[0]?.state ? $nodeMetricsData.data.nodeMetrics[0].state : 'notindb');
/* Effect */
$effect(() => {
loadUnits($initialized);
});
/* Functions */
function loadUnits(isInitialized) {
if (!isInitialized) return
const systemMetrics = [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster))]
for (let sm of systemMetrics) {
systemUnits[sm.name] = (sm?.unit?.prefix ? sm.unit.prefix : "") + (sm?.unit?.base ? sm.unit.base : "")
}
}
</script> </script>
<Row cols={{ xs: 2, lg: 5 }}> <Row cols={{ xs: 2, lg: 5 }}>
@@ -246,7 +246,7 @@
<MetricPlot <MetricPlot
metric={item.name} metric={item.name}
timestep={item.metric.timestep} timestep={item.metric.timestep}
cluster={clusters.find((c) => c.name == cluster)} cluster={clusterInfos.find((c) => c.name == cluster)}
subCluster={$nodeMetricsData.data.nodeMetrics[0].subCluster} subCluster={$nodeMetricsData.data.nodeMetrics[0].subCluster}
series={item.metric.series} series={item.metric.series}
enableFlip enableFlip
@@ -277,6 +277,7 @@
.map((m) => ({ .map((m) => ({
...m, ...m,
disabled: checkMetricDisabled( disabled: checkMetricDisabled(
globalMetrics,
m.name, m.name,
cluster, cluster,
$nodeMetricsData.data.nodeMetrics[0].subCluster, $nodeMetricsData.data.nodeMetrics[0].subCluster,

View File

@@ -51,13 +51,6 @@
/* Const Init */ /* Const Init */
const { query: initq } = init(); const { query: initq } = init();
const client = getContextClient(); const client = getContextClient();
const ccconfig = getContext("cc-config");
const initialized = getContext("initialized");
const globalMetrics = getContext("globalMetrics");
const resampleConfig = getContext("resampling") || null;
const resampleResolutions = resampleConfig ? [...resampleConfig.resolutions] : [];
const resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0;
const stateOptions = ['all', 'allocated', 'idle', 'reserved', 'mixed', 'down', 'unknown', 'notindb']; const stateOptions = ['all', 'allocated', 'idle', 'reserved', 'mixed', 'down', 'unknown', 'notindb'];
const nowDate = new Date(Date.now()); const nowDate = new Date(Date.now());
@@ -65,35 +58,55 @@
let timeoutId = null; let timeoutId = null;
/* State Init */ /* State Init */
let selectedResolution = $state(resampleConfig ? resampleDefault : 0);
let hostnameFilter = $state(""); let hostnameFilter = $state("");
let hoststateFilter = $state("all"); let hoststateFilter = $state("all");
let pendingHostnameFilter = $state(""); let pendingHostnameFilter = $state("");
let isMetricsSelectionOpen = $state(false); let isMetricsSelectionOpen = $state(false);
/* Derived Init Return */
const thisInit = $derived($initq?.data ? true : false);
/* Derived States */ /* Derived States */
const ccconfig = $derived(thisInit ? getContext("cc-config") : null);
const globalMetrics = $derived(thisInit ? getContext("globalMetrics") : null);
const resampleConfig = $derived(thisInit ? getContext("resampling") : null);
const resampleResolutions = $derived(resampleConfig ? [...resampleConfig.resolutions] : []);
const resampleDefault = $derived(resampleConfig ? Math.max(...resampleConfig.resolutions) : 0);
const displayNodeOverview = $derived((displayType === 'OVERVIEW'));
const systemMetrics = $derived(globalMetrics ? [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster))] : []);
const systemUnits = $derived.by(() => {
const pendingUnits = {};
if (thisInit && systemMetrics.length > 0) {
for (let sm of systemMetrics) {
pendingUnits[sm.name] = (sm?.unit?.prefix ? sm.unit.prefix : "") + (sm?.unit?.base ? sm.unit.base : "")
};
}
return {...pendingUnits};
});
let selectedResolution = $derived(resampleDefault);
let to = $derived(presetTo ? presetTo : new Date(Date.now())); let to = $derived(presetTo ? presetTo : new Date(Date.now()));
let from = $derived(presetFrom ? presetFrom : new Date(nowDate.setHours(nowDate.getHours() - 4))); let from = $derived(presetFrom ? presetFrom : new Date(nowDate.setHours(nowDate.getHours() - 4)));
const displayNodeOverview = $derived((displayType === 'OVERVIEW'));
const systemMetrics = $derived($initialized ? [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster))] : []);
const presetSystemUnits = $derived(loadUnits(systemMetrics));
let selectedMetric = $derived.by(() => { let selectedMetric = $derived.by(() => {
let configKey = `nodeOverview_selectedMetric`; let configKey = `nodeOverview_selectedMetric`;
if (cluster) configKey += `:${cluster}`; if (cluster) configKey += `:${cluster}`;
if (subCluster) configKey += `:${subCluster}`; if (subCluster) configKey += `:${subCluster}`;
if ($initialized) { if (thisInit) {
if (ccconfig[configKey]) return ccconfig[configKey] if (ccconfig[configKey]) return ccconfig[configKey]
else if (systemMetrics.length !== 0) return systemMetrics[0].name else if (systemMetrics.length !== 0) return systemMetrics[0].name
} }
return "" return ""
}); });
let selectedMetrics = $derived.by(() => { let selectedMetrics = $derived.by(() => {
let configKey = `nodeList_selectedMetrics`; let configKey = `nodeList_selectedMetrics`;
if (cluster) configKey += `:${cluster}`; if (cluster) configKey += `:${cluster}`;
if (subCluster) configKey += `:${subCluster}`; if (subCluster) configKey += `:${subCluster}`;
if ($initialized) { if (thisInit) {
if (ccconfig[configKey]) return ccconfig[configKey] if (ccconfig[configKey]) return ccconfig[configKey]
else if (systemMetrics.length >= 3) return [systemMetrics[0].name, systemMetrics[1].name, systemMetrics[2].name] else if (systemMetrics.length >= 3) return [systemMetrics[0].name, systemMetrics[1].name, systemMetrics[2].name]
} }
@@ -108,16 +121,6 @@
}); });
/* Functions */ /* Functions */
function loadUnits(systemMetrics) {
let pendingUnits = {};
if (systemMetrics.length > 0) {
for (let sm of systemMetrics) {
pendingUnits[sm.name] = (sm?.unit?.prefix ? sm.unit.prefix : "") + (sm?.unit?.base ? sm.unit.base : "")
};
};
return {...pendingUnits};
};
// Wait after input for some time to prevent too many requests // Wait after input for some time to prevent too many requests
function updateHostnameFilter() { function updateHostnameFilter() {
if (timeoutId != null) clearTimeout(timeoutId); if (timeoutId != null) clearTimeout(timeoutId);
@@ -157,7 +160,7 @@
<!-- ROW1: Tools--> <!-- ROW1: Tools-->
<Row cols={{ xs: 2, lg: !displayNodeOverview ? (resampleConfig ? 6 : 5) : 5 }} class="mb-3"> <Row cols={{ xs: 2, lg: !displayNodeOverview ? (resampleConfig ? 6 : 5) : 5 }} class="mb-3">
{#if $initq?.data} {#if thisInit}
<!-- List Metric Select Col--> <!-- List Metric Select Col-->
{#if !displayNodeOverview} {#if !displayNodeOverview}
<Col> <Col>
@@ -234,7 +237,7 @@
<Input type="select" bind:value={selectedMetric}> <Input type="select" bind:value={selectedMetric}>
{#each systemMetrics as metric (metric.name)} {#each systemMetrics as metric (metric.name)}
<option value={metric.name} <option value={metric.name}
>{metric.name} {presetSystemUnits[metric.name] ? "("+presetSystemUnits[metric.name]+")" : ""}</option >{metric.name} {systemUnits[metric.name] ? "("+systemUnits[metric.name]+")" : ""}</option
> >
{:else} {:else}
<option disabled>No available options</option> <option disabled>No available options</option>
@@ -266,10 +269,11 @@
{:else} {:else}
{#if displayNodeOverview} {#if displayNodeOverview}
<!-- ROW2-1: Node Overview (Grid Included)--> <!-- ROW2-1: Node Overview (Grid Included)-->
<NodeOverview {cluster} {ccconfig} {selectedMetric} {from} {to} {hostnameFilter} {hoststateFilter}/> <NodeOverview {cluster} {ccconfig} {selectedMetric} {globalMetrics} {from} {to} {hostnameFilter} {hoststateFilter}/>
{:else} {:else}
<!-- ROW2-2: Node List (Grid Included)--> <!-- ROW2-2: Node List (Grid Included)-->
<NodeList {cluster} {subCluster} {ccconfig} pendingSelectedMetrics={selectedMetrics} {selectedResolution} {hostnameFilter} {hoststateFilter} {from} {to} {presetSystemUnits}/> <NodeList {cluster} {subCluster} {ccconfig} {globalMetrics}
pendingSelectedMetrics={selectedMetrics} {selectedResolution} {hostnameFilter} {hoststateFilter} {from} {to} {systemUnits}/>
{/if} {/if}
{/if} {/if}
@@ -279,6 +283,7 @@
presetMetrics={selectedMetrics} presetMetrics={selectedMetrics}
{cluster} {cluster}
{subCluster} {subCluster}
{globalMetrics}
configName="nodeList_selectedMetrics" configName="nodeList_selectedMetrics"
applyMetrics={(newMetrics) => applyMetrics={(newMetrics) =>
selectedMetrics = [...newMetrics] selectedMetrics = [...newMetrics]

View File

@@ -56,12 +56,10 @@
/* Const Init */ /* Const Init */
const { query: initq } = init(); const { query: initq } = init();
const ccconfig = getContext("cc-config");
const client = getContextClient(); const client = getContextClient();
const durationBinOptions = ["1m","10m","1h","6h","12h"]; const durationBinOptions = ["1m","10m","1h","6h","12h"];
const metricBinOptions = [10, 20, 50, 100]; const metricBinOptions = [10, 20, 50, 100];
const matchedJobCompareLimit = 500; const matchedJobCompareLimit = 500;
const shortDuration = ccconfig.jobList_hideShortRunningJobs; // Always configured
/* State Init */ /* State Init */
// List & Control Vars // List & Control Vars
@@ -73,7 +71,6 @@
let isSortingOpen = $state(false); let isSortingOpen = $state(false);
let isMetricsSelectionOpen = $state(false); let isMetricsSelectionOpen = $state(false);
let sorting = $state({ field: "startTime", type: "col", order: "DESC" }); let sorting = $state({ field: "startTime", type: "col", order: "DESC" });
let selectedHistogramsBuffer = $state({ all: (ccconfig['userView_histogramMetrics'] || []) })
let jobCompare = $state(null); let jobCompare = $state(null);
let matchedCompareJobs = $state(0); let matchedCompareJobs = $state(0);
let showCompare = $state(false); let showCompare = $state(false);
@@ -84,10 +81,17 @@
let numDurationBins = $state("1h"); let numDurationBins = $state("1h");
let numMetricBins = $state(10); let numMetricBins = $state(10);
/* Derived Init Return */
const thisInit = $derived($initq?.data ? true : false);
/* Derived */ /* Derived */
const ccconfig = $derived(thisInit ? getContext("cc-config") : null);
const globalMetrics = $derived(thisInit ? getContext("globalMetrics") : null);
const shortDuration = $derived(ccconfig?.jobList_hideShortRunningJobs);
let selectedCluster = $derived(filterPresets?.cluster ? filterPresets.cluster : null); let selectedCluster = $derived(filterPresets?.cluster ? filterPresets.cluster : null);
let selectedSubCluster = $derived(filterPresets?.partition ? filterPresets.partition : null); let selectedSubCluster = $derived(filterPresets?.partition ? filterPresets.partition : null);
let metrics = $derived.by(() => { let metrics = $derived.by(() => {
if (thisInit && ccconfig) {
if (selectedCluster) { if (selectedCluster) {
if (selectedSubCluster) { if (selectedSubCluster) {
return ccconfig[`metricConfig_jobListMetrics:${selectedCluster}:${selectedSubCluster}`] || return ccconfig[`metricConfig_jobListMetrics:${selectedCluster}:${selectedSubCluster}`] ||
@@ -98,12 +102,27 @@
ccconfig.metricConfig_jobListMetrics ccconfig.metricConfig_jobListMetrics
} }
return ccconfig.metricConfig_jobListMetrics return ccconfig.metricConfig_jobListMetrics
}
return [];
}); });
let showFootprint = $derived(filterPresets.cluster
? !!ccconfig[`jobList_showFootprint:${filterPresets.cluster}`] let showFootprint = $derived((thisInit && ccconfig)
: !!ccconfig.jobList_showFootprint ? filterPresets?.cluster
? ccconfig[`jobList_showFootprint:${filterPresets.cluster}`]
: ccconfig.jobList_showFootprint
: {}
); );
let selectedHistograms = $derived(selectedCluster ? selectedHistogramsBuffer[selectedCluster] : selectedHistogramsBuffer['all']);
let selectedHistograms = $derived.by(() => {
if (thisInit && ccconfig) {
if (selectedCluster) {
return ccconfig[`userView_histogramMetrics:${selectedCluster}`] // No Fallback; Unspecific lists an include unavailable metrics
}
return ccconfig.userView_histogramMetrics
}
return []
});
let stats = $derived( let stats = $derived(
queryStore({ queryStore({
client: client, client: client,
@@ -159,19 +178,9 @@
}); });
}); });
$effect(() => {
if (!selectedHistogramsBuffer[selectedCluster]) {
selectedHistogramsBuffer[selectedCluster] = ccconfig[`userView_histogramMetrics:${selectedCluster}`];
};
});
/* On Mount */ /* On Mount */
onMount(() => { onMount(() => {
filterComponent.updateFilters(); filterComponent.updateFilters();
// Why? -> `$derived(ccconfig[$cluster])` only loads array from last Backend-Query if $cluster changed reactively (without reload)
if (filterPresets?.cluster) {
selectedHistogramsBuffer[filterPresets.cluster] = ccconfig[`userView_histogramMetrics:${filterPresets.cluster}`];
};
}); });
</script> </script>
@@ -508,6 +517,7 @@
<Sorting <Sorting
bind:isOpen={isSortingOpen} bind:isOpen={isSortingOpen}
presetSorting={sorting} presetSorting={sorting}
{globalMetrics}
applySorting={(newSort) => applySorting={(newSort) =>
sorting = {...newSort} sorting = {...newSort}
} }
@@ -521,6 +531,7 @@
subCluster={selectedSubCluster} subCluster={selectedSubCluster}
configName="metricConfig_jobListMetrics" configName="metricConfig_jobListMetrics"
footprintSelect footprintSelect
{globalMetrics}
applyMetrics={(newMetrics) => applyMetrics={(newMetrics) =>
metrics = [...newMetrics] metrics = [...newMetrics]
} }
@@ -531,7 +542,8 @@
bind:isOpen={isHistogramSelectionOpen} bind:isOpen={isHistogramSelectionOpen}
presetSelectedHistograms={selectedHistograms} presetSelectedHistograms={selectedHistograms}
configName="userView_histogramMetrics" configName="userView_histogramMetrics"
{globalMetrics}
applyChange={(newSelection) => { applyChange={(newSelection) => {
selectedHistogramsBuffer[selectedCluster || 'all'] = [...newSelection]; selectedHistograms = [...newSelection];
}} }}
/> />

View File

@@ -39,10 +39,6 @@
} = $props(); } = $props();
/* Const Init */ /* Const Init */
const ccconfig = getContext("cc-config");
const initialized = getContext("initialized");
const globalMetrics = getContext("globalMetrics");
const usePaging = ccconfig?.jobList_usePaging || false;
const jobInfoColumnWidth = 250; const jobInfoColumnWidth = 250;
const client = getContextClient(); const client = getContextClient();
const query = gql` const query = gql`
@@ -100,11 +96,18 @@
let headerPaddingTop = $state(0); let headerPaddingTop = $state(0);
let jobs = $state([]); let jobs = $state([]);
let page = $state(1); let page = $state(1);
let itemsPerPage = $state(usePaging ? (ccconfig?.jobList_jobsPerPage || 10) : 10);
let triggerMetricRefresh = $state(false); let triggerMetricRefresh = $state(false);
let tableWidth = $state(0); let tableWidth = $state(0);
/* Derived */ /* Derived */
const initialized = $derived(getContext("initialized") || false);
const ccconfig = $derived(initialized ? getContext("cc-config") : null);
const globalMetrics = $derived(initialized ? getContext("globalMetrics") : null);
const clusterInfos = $derived(initialized ? getContext("clusters"): null);
const resampleConfig = $derived(initialized ? getContext("resampling") : null);
const usePaging = $derived(ccconfig?.jobList_usePaging || false);
let itemsPerPage = $derived(usePaging ? (ccconfig?.jobList_jobsPerPage || 10) : 10);
let filter = $derived([...filterBuffer]); let filter = $derived([...filterBuffer]);
let paging = $derived({ itemsPerPage, page }); let paging = $derived({ itemsPerPage, page });
const plotWidth = $derived.by(() => { const plotWidth = $derived.by(() => {
@@ -274,7 +277,7 @@
style="width: {plotWidth}px; padding-top: {headerPaddingTop}px" style="width: {plotWidth}px; padding-top: {headerPaddingTop}px"
> >
{metric} {metric}
{#if $initialized} {#if initialized}
({getUnit(metric)}) ({getUnit(metric)})
{/if} {/if}
</th> </th>
@@ -292,7 +295,8 @@
</tr> </tr>
{:else} {:else}
{#each jobs as job (job.id)} {#each jobs as job (job.id)}
<JobListRow {triggerMetricRefresh} {job} {metrics} {plotWidth} {showFootprint} previousSelect={selectedJobs.includes(job.id)} <JobListRow {triggerMetricRefresh} {job} {metrics} {plotWidth} {showFootprint} {globalMetrics} {clusterInfos} {resampleConfig}
previousSelect={selectedJobs.includes(job.id)}
selectJob={(detail) => selectedJobs = [...selectedJobs, detail]} selectJob={(detail) => selectedJobs = [...selectedJobs, detail]}
unselectJob={(detail) => selectedJobs = selectedJobs.filter(item => item !== detail)} unselectJob={(detail) => selectedJobs = selectedJobs.filter(item => item !== detail)}
/> />

View File

@@ -30,11 +30,10 @@
setFilter setFilter
} = $props(); } = $props();
/* Const Init */
const clusters = getContext("clusters");
const initialized = getContext("initialized");
/* Derived */ /* Derived */
const initialized = $derived(getContext("initialized") || false);
const clusterInfos = $derived($initialized ? getContext("clusters") : null);
let pendingCluster = $derived(presetCluster); let pendingCluster = $derived(presetCluster);
let pendingPartition = $derived(presetPartition); let pendingPartition = $derived(presetPartition);
</script> </script>
@@ -56,7 +55,7 @@
> >
Any Cluster Any Cluster
</ListGroupItem> </ListGroupItem>
{#each clusters as cluster} {#each clusterInfos as cluster}
<ListGroupItem <ListGroupItem
disabled={disableClusterSelection} disabled={disableClusterSelection}
active={pendingCluster == cluster.name} active={pendingCluster == cluster.name}
@@ -80,7 +79,7 @@
> >
Any Partition Any Partition
</ListGroupItem> </ListGroupItem>
{#each clusters?.find((c) => c.name == pendingCluster)?.partitions as partition} {#each clusterInfos?.find((c) => c.name == pendingCluster)?.partitions as partition}
<ListGroupItem <ListGroupItem
active={pendingPartition == partition} active={pendingPartition == partition}
onclick={() => (pendingPartition = partition)} onclick={() => (pendingPartition = partition)}

View File

@@ -42,8 +42,8 @@
contains: "Contains", contains: "Contains",
} }
const findMaxNumAccels = (clusters) => const findMaxNumAccels = (infos) =>
clusters.reduce( infos.reduce(
(max, cluster) => (max, cluster) =>
Math.max( Math.max(
max, max,
@@ -56,8 +56,8 @@
); );
// Limited to Single-Node Thread Count // Limited to Single-Node Thread Count
const findMaxNumHWThreadsPerNode = (clusters) => const findMaxNumHWThreadsPerNode = (infos) =>
clusters.reduce( infos.reduce(
(max, cluster) => (max, cluster) =>
Math.max( Math.max(
max, max,
@@ -92,8 +92,8 @@
let threadState = $derived(presetNumHWThreads); let threadState = $derived(presetNumHWThreads);
let accState = $derived(presetNumAccelerators); let accState = $derived(presetNumAccelerators);
const clusters = $derived(getContext("clusters")); const initialized = $derived(getContext("initialized") || false);
const initialized = $derived(getContext("initialized")); const clusterInfos = $derived($initialized ? getContext("clusters") : null);
// Is Selection Active // Is Selection Active
const nodesActive = $derived(!(JSON.stringify(nodesState) === JSON.stringify({ from: 1, to: maxNumNodes }))); const nodesActive = $derived(!(JSON.stringify(nodesState) === JSON.stringify({ from: 1, to: maxNumNodes })));
const threadActive = $derived(!(JSON.stringify(threadState) === JSON.stringify({ from: 1, to: maxNumHWThreads }))); const threadActive = $derived(!(JSON.stringify(threadState) === JSON.stringify({ from: 1, to: maxNumHWThreads })));
@@ -109,12 +109,12 @@
$effect(() => { $effect(() => {
if ($initialized) { if ($initialized) {
if (activeCluster != null) { if (activeCluster != null) {
const { subClusters } = clusters.find((c) => c.name == activeCluster); const { subClusters } = clusterInfos.find((c) => c.name == activeCluster);
maxNumAccelerators = findMaxNumAccels([{ subClusters }]); maxNumAccelerators = findMaxNumAccels([{ subClusters }]);
maxNumHWThreads = findMaxNumHWThreadsPerNode([{ subClusters }]); maxNumHWThreads = findMaxNumHWThreadsPerNode([{ subClusters }]);
} else if (clusters.length > 0) { } else if (clusterInfos.length > 0) {
maxNumAccelerators = findMaxNumAccels(clusters); maxNumAccelerators = findMaxNumAccels(clusterInfos);
maxNumHWThreads = findMaxNumHWThreadsPerNode(clusters); maxNumHWThreads = findMaxNumHWThreadsPerNode(clusterInfos);
} }
} }
}); });

View File

@@ -31,8 +31,8 @@
} = $props(); } = $props();
/* Derived */ /* Derived */
const allTags = $derived(getContext("tags")) const initialized = $derived(getContext("initialized") || false)
const initialized = $derived(getContext("initialized")) const allTags = $derived($initialized ? getContext("tags") : [])
/* State Init */ /* State Init */
let searchTerm = $state(""); let searchTerm = $state("");

View File

@@ -18,8 +18,8 @@
} = $props(); } = $props();
/* Derived */ /* Derived */
const allTags = $derived(getContext('tags')); const initialized = $derived(getContext('initialized') || false);
const initialized = $derived(getContext('initialized')); const allTags = $derived($initialized ? getContext('tags') : []);
/* Effects */ /* Effects */
$effect(() => { $effect(() => {

View File

@@ -48,8 +48,6 @@
const client = getContextClient(); const client = getContextClient();
/* State Init */ /* State Init */
let initialized = getContext("initialized")
let allTags = getContext("tags")
let newTagType = $state(""); let newTagType = $state("");
let newTagName = $state(""); let newTagName = $state("");
let filterTerm = $state(""); let filterTerm = $state("");
@@ -57,10 +55,13 @@
let isOpen = $state(false); let isOpen = $state(false);
/* Derived */ /* Derived */
const initialized = $derived(getContext("initialized") || false );
let allTags = $derived(initialized ? getContext("tags") : [])
let newTagScope = $derived(username); let newTagScope = $derived(username);
const isAdmin = $derived((roles && authlevel == roles.admin)); const isAdmin = $derived((roles && authlevel == roles.admin));
const isSupport = $derived((roles && authlevel == roles.support)); const isSupport = $derived((roles && authlevel == roles.support));
const allTagsFiltered = $derived(($initialized, jobTags, fuzzySearchTags(filterTerm, allTags))); // $init und JobTags only for triggering react const allTagsFiltered = $derived((initialized, jobTags, fuzzySearchTags(filterTerm, allTags))); // $init und JobTags only for triggering react
const usedTagsFiltered = $derived(matchJobTags(jobTags, allTagsFiltered, 'used', isAdmin, isSupport)); const usedTagsFiltered = $derived(matchJobTags(jobTags, allTagsFiltered, 'used', isAdmin, isSupport));
const unusedTagsFiltered = $derived(matchJobTags(jobTags, allTagsFiltered, 'unused', isAdmin, isSupport)); const unusedTagsFiltered = $derived(matchJobTags(jobTags, allTagsFiltered, 'unused', isAdmin, isSupport));

View File

@@ -11,11 +11,13 @@
- `triggerMetricRefresh Bool?`: If changed to true from upstream, will trigger metric query [Default: false] - `triggerMetricRefresh Bool?`: If changed to true from upstream, will trigger metric query [Default: false]
- `selectJob Func`: The callback function to select a job for comparison - `selectJob Func`: The callback function to select a job for comparison
- `unselectJob Func`: The callback function to unselect a job from comparison - `unselectJob Func`: The callback function to unselect a job from comparison
- `globalMetrics [Obj]`: Includes the backend supplied availabilities for cluster and subCluster
- `clusterInfos [Obj]`: Includes the backend supplied cluster topology
- `resampleConfig [Obj]`: Includes the backend supplied resampling info
--> -->
<script> <script>
import { queryStore, gql, getContextClient } from "@urql/svelte"; import { queryStore, gql, getContextClient } from "@urql/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";
@@ -33,13 +35,13 @@
triggerMetricRefresh = false, triggerMetricRefresh = false,
selectJob, selectJob,
unselectJob, unselectJob,
globalMetrics,
clusterInfos,
resampleConfig
} = $props(); } = $props();
/* Const Init */ /* Const Init */
const client = getContextClient(); const client = getContextClient();
const cluster = getContext("clusters");
const resampleConfig = getContext("resampling") || null;
const resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0;
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) {
@@ -73,11 +75,11 @@
`; `;
/* State Init */ /* State Init */
let selectedResolution = $state(resampleDefault);
let zoomStates = $state({}); let zoomStates = $state({});
let thresholdStates = $state({}); let thresholdStates = $state({});
/* Derived */ /* Derived */
const resampleDefault = $derived(resampleConfig ? Math.max(...resampleConfig.resolutions) : 0);
const jobId = $derived(job?.id); const jobId = $derived(job?.id);
const scopes = $derived.by(() => { const scopes = $derived.by(() => {
if (job.numNodes == 1) { if (job.numNodes == 1) {
@@ -87,6 +89,8 @@
return ["node"]; return ["node"];
}; };
}); });
let selectedResolution = $derived(resampleDefault);
let isSelected = $derived(previousSelect); let isSelected = $derived(previousSelect);
let metricsQuery = $derived(queryStore({ let metricsQuery = $derived(queryStore({
client: client, client: client,
@@ -94,6 +98,7 @@
variables: { id: jobId, metrics, scopes, selectedResolution }, variables: { id: jobId, metrics, scopes, selectedResolution },
}) })
); );
const refinedData = $derived($metricsQuery?.data?.jobMetrics ? sortAndSelectScope($metricsQuery.data.jobMetrics) : []); const refinedData = $derived($metricsQuery?.data?.jobMetrics ? sortAndSelectScope($metricsQuery.data.jobMetrics) : []);
/* Effects */ /* Effects */
@@ -160,6 +165,7 @@
return { return {
name: jobMetric.data.name, name: jobMetric.data.name,
disabled: checkMetricDisabled( disabled: checkMetricDisabled(
globalMetrics,
jobMetric.data.name, jobMetric.data.name,
job.cluster, job.cluster,
job.subCluster, job.subCluster,
@@ -220,7 +226,7 @@
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.find((c) => c.name == job.cluster)} cluster={clusterInfos.find((c) => c.name == job.cluster)}
subCluster={job.subCluster} subCluster={job.subCluster}
isShared={job.shared != "none"} isShared={job.shared != "none"}
numhwthreads={job.numHWThreads} numhwthreads={job.numHWThreads}

View File

@@ -18,7 +18,7 @@
import uPlot from "uplot"; import uPlot from "uplot";
import { formatNumber, formatDurationTime } from "../units.js"; import { formatNumber, formatDurationTime } from "../units.js";
import { getContext, onDestroy } from "svelte"; import { getContext, onDestroy } from "svelte";
import { Card } from "@sveltestrap/sveltestrap"; import { Card, CardHeader, CardBody } from "@sveltestrap/sveltestrap";
/* Svelte 5 Props */ /* Svelte 5 Props */
let { let {

View File

@@ -6,6 +6,7 @@
- `ìsOpen Bool`: Is selection opened [Bindable] - `ìsOpen Bool`: Is selection opened [Bindable]
- `configName String`: The config id string to be updated in database on selection change - `configName String`: The config id string to be updated in database on selection change
- `presetSelectedHistograms [String]`: The currently selected metrics to display as histogram - `presetSelectedHistograms [String]`: The currently selected metrics to display as histogram
- `globalMetrics [Obj]`: Includes the backend supplied availabilities for cluster and subCluster
- `applyChange Func`: The callback function to apply current selection - `applyChange Func`: The callback function to apply current selection
--> -->
@@ -24,10 +25,11 @@
/* Svelte 5 Props */ /* Svelte 5 Props */
let { let {
cluster, cluster = "",
isOpen = $bindable(), isOpen = $bindable(),
configName, configName,
presetSelectedHistograms, presetSelectedHistograms,
globalMetrics,
applyChange applyChange
} = $props(); } = $props();
@@ -42,11 +44,11 @@
function loadHistoMetrics(thisCluster) { function loadHistoMetrics(thisCluster) {
// isInit Check Removed: Parent Component has finished Init-Query: Globalmetrics available here. // isInit Check Removed: Parent Component has finished Init-Query: Globalmetrics available here.
if (!thisCluster) { if (!thisCluster) {
return getContext("globalMetrics") return globalMetrics
.filter((gm) => gm?.footprint) .filter((gm) => gm?.footprint)
.map((fgm) => { return fgm.name }) .map((fgm) => { return fgm.name })
} else { } else {
return getContext("globalMetrics") return globalMetrics
.filter((gm) => gm?.availability.find((av) => av.cluster == thisCluster)) .filter((gm) => gm?.availability.find((av) => av.cluster == thisCluster))
.filter((agm) => agm?.footprint) .filter((agm) => agm?.footprint)
.map((afgm) => { return afgm.name }) .map((afgm) => { return afgm.name })

View File

@@ -9,13 +9,12 @@
- `cluster String?`: The currently selected cluster [Default: null] - `cluster String?`: The currently selected cluster [Default: null]
- `subCluster String?`: The currently selected subCluster [Default: null] - `subCluster String?`: The currently selected subCluster [Default: null]
- `footprintSelect Bool?`: Render checkbox for footprint display in upstream component [Default: false] - `footprintSelect Bool?`: Render checkbox for footprint display in upstream component [Default: false]
- `preInitialized Bool?`: If the parent component has a dedicated call to init() [Default: false]
- `configName String`: The config key for the last saved selection (constant) - `configName String`: The config key for the last saved selection (constant)
- `globalMetrics [Obj]`: Includes the backend supplied availabilities for cluster and subCluster
- `applyMetrics Func`: The callback function to apply current selection - `applyMetrics Func`: The callback function to apply current selection
--> -->
<script> <script>
import { getContext } from "svelte";
import { import {
Modal, Modal,
ModalBody, ModalBody,
@@ -35,14 +34,12 @@
cluster = null, cluster = null,
subCluster = null, subCluster = null,
footprintSelect = false, footprintSelect = false,
preInitialized = false, // Job View is Pre-Init'd: $initialized "alone" store returns false
configName, configName,
globalMetrics,
applyMetrics applyMetrics
} = $props(); } = $props();
/* Const Init */ /* Const Init */
const globalMetrics = getContext("globalMetrics");
const initialized = getContext("initialized");
const client = getContextClient(); const client = getContextClient();
const updateConfigurationMutation = ({ name, value }) => { const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({ return mutationStore({
@@ -58,27 +55,23 @@
/* State Init */ /* State Init */
let pendingShowFootprint = $state(!!showFootprint); let pendingShowFootprint = $state(!!showFootprint);
let listedMetrics = $state([]);
let columnHovering = $state(null); let columnHovering = $state(null);
/* Derives States */ /* Derives States */
let pendingMetrics = $derived(presetMetrics); const allMetrics = $derived(loadAvailable(globalMetrics));
const allMetrics = $derived(loadAvailable(preInitialized || $initialized)); let pendingMetrics = $derived(presetMetrics || []);
let listedMetrics = $derived([...presetMetrics, ...allMetrics.difference(new Set(presetMetrics))]); // List (preset) active metrics first, then list inactives
/* Reactive Effects */ /* Reactive Effects */
$effect(() => { $effect(() => {
totalMetrics = allMetrics?.size || 0; totalMetrics = allMetrics?.size || 0;
}); });
$effect(() => {
listedMetrics = [...presetMetrics, ...allMetrics.difference(new Set(presetMetrics))]; // List (preset) active metrics first, then list inactives
});
/* Functions */ /* Functions */
function loadAvailable(init) { function loadAvailable(gms) {
const availableMetrics = new Set(); const availableMetrics = new Set();
if (init) { if (gms) {
for (let gm of globalMetrics) { for (let gm of gms) {
if (!cluster) { if (!cluster) {
availableMetrics.add(gm.name) availableMetrics.add(gm.name)
} else { } else {
@@ -90,7 +83,7 @@
} }
} }
} }
return availableMetrics return availableMetrics;
} }
function printAvailability(metric, cluster) { function printAvailability(metric, cluster) {

View File

@@ -5,11 +5,11 @@
- `presetSorting Object?`: The latest sort selection state - `presetSorting Object?`: The latest sort selection state
- Default { field: "startTime", type: "col", order: "DESC" } - Default { field: "startTime", type: "col", order: "DESC" }
- `isOpen Bool?`: Is modal opened [Bindable, Default: false] - `isOpen Bool?`: Is modal opened [Bindable, Default: false]
- `globalMetrics [Obj]`: Includes the backend supplied availabilities for cluster and subCluster
- `applySorting Func`: The callback function to apply current selection - `applySorting Func`: The callback function to apply current selection
--> -->
<script> <script>
import { getContext, onMount } from "svelte";
import { import {
Icon, Icon,
Button, Button,
@@ -25,12 +25,11 @@
let { let {
isOpen = $bindable(false), isOpen = $bindable(false),
presetSorting = { field: "startTime", type: "col", order: "DESC" }, presetSorting = { field: "startTime", type: "col", order: "DESC" },
globalMetrics,
applySorting applySorting
} = $props(); } = $props();
/* Const Init */ /* Const Init */
const initialized = getContext("initialized");
const globalMetrics = getContext("globalMetrics");
const fixedSortables = $state([ const fixedSortables = $state([
{ field: "startTime", type: "col", text: "Start Time (Default)", order: "DESC" }, { field: "startTime", type: "col", text: "Start Time (Default)", order: "DESC" },
{ field: "duration", type: "col", text: "Duration", order: "DESC" }, { field: "duration", type: "col", text: "Duration", order: "DESC" },
@@ -42,22 +41,11 @@
/* State Init */ /* State Init */
let activeColumnIdx = $state(0); let activeColumnIdx = $state(0);
let metricSortables = $state([]);
/* Derived */ /* Derived */
let sorting = $derived({...presetSorting}) let sorting = $derived({...presetSorting})
let sortableColumns = $derived([...fixedSortables, ...metricSortables]); let metricSortables = $derived.by(() => {
return globalMetrics.map((gm) => {
/* Effect */
$effect(() => {
if ($initialized) {
loadMetricSortables();
};
});
/* Functions */
function loadMetricSortables() {
metricSortables = globalMetrics.map((gm) => {
if (gm?.footprint) { if (gm?.footprint) {
return { return {
field: gm.name + '_' + gm.footprint, field: gm.name + '_' + gm.footprint,
@@ -68,8 +56,10 @@
} }
return null return null
}).filter((r) => r != null) }).filter((r) => r != null)
}; });
let sortableColumns = $derived([...fixedSortables, ...metricSortables]);
/* Functions */
function loadActiveIndex() { function loadActiveIndex() {
activeColumnIdx = sortableColumns.findIndex( activeColumnIdx = sortableColumns.findIndex(
(col) => col.field == sorting.field, (col) => col.field == sorting.field,

View File

@@ -302,19 +302,17 @@ export function stickyHeader(datatableHeaderSelector, updatePading) {
onDestroy(() => document.removeEventListener("scroll", onscroll)); onDestroy(() => document.removeEventListener("scroll", onscroll));
} }
export function checkMetricDisabled(m, c, s) { // [m]etric, [c]luster, [s]ubcluster export function checkMetricDisabled(gm, m, c, s) { // [g]lobal[m]etrics, [m]etric, [c]luster, [s]ubcluster
const metrics = getContext("globalMetrics"); const available = gm?.find((gm) => gm.name === m)?.availability?.find((av) => av.cluster === c)?.subClusters?.includes(s)
const available = metrics?.find((gm) => gm.name === m)?.availability?.find((av) => av.cluster === c)?.subClusters?.includes(s)
// Return inverse logic // Return inverse logic
return !available return !available
} }
export function checkMetricsDisabled(ma, c, s) { // [m]etric[a]rray, [c]luster, [s]ubcluster export function checkMetricsDisabled(gm, ma, c, s) { // [g]lobal[m]etrics, [m]etric[a]rray, [c]luster, [s]ubcluster
let result = {}; let result = {};
const metrics = getContext("globalMetrics");
ma.forEach((m) => { ma.forEach((m) => {
// Return named inverse logic: !available // Return named inverse logic: !available
result[m] = !(metrics?.find((gm) => gm.name === m)?.availability?.find((av) => av.cluster === c)?.subClusters?.includes(s)) result[m] = !(gm?.find((gm) => gm.name === m)?.availability?.find((av) => av.cluster === c)?.subClusters?.includes(s))
}); });
return result return result
} }

View File

@@ -3,7 +3,7 @@
Properties: Properties:
- `job Object`: The GQL job object - `job Object`: The GQL job object
- `clusters Array`: The GQL clusters array - `clusterInfo Array`: The GQL clusters array
--> -->
<script> <script>
@@ -24,7 +24,7 @@
/* Svelte 5 Props */ /* Svelte 5 Props */
let { let {
job, job,
clusters, clusterInfo,
} = $props(); } = $props();
/* Const Init */ /* Const Init */
@@ -62,7 +62,7 @@
<div bind:clientWidth={roofWidth}> <div bind:clientWidth={roofWidth}>
<Roofline <Roofline
width={roofWidth} width={roofWidth}
subCluster={clusters subCluster={clusterInfo
.find((c) => c.name == job.cluster) .find((c) => c.name == job.cluster)
.subClusters.find((sc) => sc.name == job.subCluster)} .subClusters.find((sc) => sc.name == job.subCluster)}
data={transformDataForRoofline( data={transformDataForRoofline(

View File

@@ -3,8 +3,10 @@
Properties: Properties:
- `job Object`: The job object - `job Object`: The job object
- `clusters Object`: The clusters object - `clusterInfo Object`: The clusters object
- `tabActive bool`: Boolean if StatsTabe Tab is Active on Creation - `tabActive bool`: Boolean if StatsTabe Tab is Active on Creation
- `globalMetrics [Obj]`: Includes the backend supplied availabilities for cluster and subCluster
- `ccconfig Object?`: The ClusterCockpit Config Context
--> -->
<script> <script>
@@ -13,7 +15,6 @@
gql, gql,
getContextClient getContextClient
} from "@urql/svelte"; } from "@urql/svelte";
import { getContext } from "svelte";
import { import {
Card, Card,
Button, Button,
@@ -29,8 +30,10 @@
/* Svelte 5 Props */ /* Svelte 5 Props */
let { let {
job, job,
clusters, clusterInfo,
tabActive, tabActive,
globalMetrics,
ccconfig
} = $props(); } = $props();
/* Const Init */ /* Const Init */
@@ -55,65 +58,73 @@
/* State Init */ /* State Init */
let moreScopes = $state(false); let moreScopes = $state(false);
let selectedScopes = $state([]);
let selectedMetrics = $state([]);
let totalMetrics = $state(0); // For Info Only, filled by MetricSelection Component let totalMetrics = $state(0); // For Info Only, filled by MetricSelection Component
let isMetricSelectionOpen = $state(false); let isMetricSelectionOpen = $state(false);
/* Derived */ /* Derived Var Preprocessing*/
const scopedStats = $derived(queryStore({ let selectedTableMetrics = $derived.by(() => {
client: client, if(job && ccconfig) {
query: query, if (job.cluster) {
variables: { dbid: job.id, selectedMetrics, selectedScopes }, if (job.subCluster) {
}) return ccconfig[`metricConfig_jobViewTableMetrics:${job.cluster}:${job.subCluster}`] ||
); ccconfig[`metricConfig_jobViewTableMetrics:${job.cluster}`] ||
ccconfig.metricConfig_jobViewTableMetrics
/* Functions */ }
function loadScopes() { return ccconfig[`metricConfig_jobViewTableMetrics:${job.cluster}`] ||
// Archived Jobs Load All Scopes By Default (See Backend) ccconfig.metricConfig_jobViewTableMetrics
moreScopes = true; }
selectedScopes = ["node", "socket", "core", "hwthread", "accelerator"]; return ccconfig.metricConfig_jobViewTableMetrics
}; }
return [];
/* On Init */ });
// Handle Job Query on Init -> is not executed anymore
getContext("on-init")(() => {
if (!job) return;
const pendingMetrics = (
getContext("cc-config")[`metricConfig_jobViewTableMetrics:${job.cluster}:${job.subCluster}`] ||
getContext("cc-config")[`metricConfig_jobViewTableMetrics:${job.cluster}`]
) || getContext("cc-config")["metricConfig_jobViewTableMetrics"];
let selectedTableScopes = $derived.by(() => {
if (job) {
if (!moreScopes) {
// Select default Scopes to load: Check before if any metric has accelerator scope by default // Select default Scopes to load: Check before if any metric has accelerator scope by default
const accScopeDefault = [...pendingMetrics].some(function (m) { const pendingScopes = ["node"]
const cluster = clusters.find((c) => c.name == job.cluster); const accScopeDefault = [...selectedTableMetrics].some(function (m) {
const cluster = clusterInfo.find((c) => c.name == job.cluster);
const subCluster = cluster.subClusters.find((sc) => sc.name == job.subCluster); const subCluster = cluster.subClusters.find((sc) => sc.name == job.subCluster);
return subCluster.metricConfig.find((smc) => smc.name == m)?.scope === "accelerator"; return subCluster.metricConfig.find((smc) => smc.name == m)?.scope === "accelerator";
}); });
const pendingScopes = ["node"]
if (job.numNodes === 1) { if (job.numNodes === 1) {
pendingScopes.push("socket") pendingScopes.push("socket")
pendingScopes.push("core") pendingScopes.push("core")
pendingScopes.push("hwthread") pendingScopes.push("hwthread")
if (accScopeDefault) { pendingScopes.push("accelerator") } if (accScopeDefault) { pendingScopes.push("accelerator") }
} }
return[...new Set(pendingScopes)];
selectedMetrics = [...pendingMetrics]; } else {
selectedScopes = [...pendingScopes]; // If flag set: Always load all scopes
return ["node", "socket", "core", "hwthread", "accelerator"];
}
} // Fallback
return ["node"]
}); });
/* Derived Query */
const scopedStats = $derived(queryStore({
client: client,
query: query,
variables: {
dbid: job.id,
selectedMetrics: selectedTableMetrics,
selectedScopes: selectedTableScopes
},
})
);
</script> </script>
<TabPane tabId="stats" tab="Statistics Table" class="overflow-x-auto" active={tabActive}> <TabPane tabId="stats" tab="Statistics Table" class="overflow-x-auto" active={tabActive}>
<Row> <Row>
<Col class="m-2"> <Col class="m-2">
<Button outline onclick={() => (isMetricSelectionOpen = true)} class="px-2" color="primary" style="margin-right:0.5rem"> <Button outline onclick={() => (isMetricSelectionOpen = true)} class="px-2" color="primary" style="margin-right:0.5rem">
Select Metrics (Selected {selectedMetrics.length} of {totalMetrics} available) Select Metrics (Selected {selectedTableMetrics.length} of {totalMetrics} available)
</Button> </Button>
{#if job.numNodes > 1 && job.state === "running"} {#if job.numNodes > 1 && job.state === "running"}
<Button class="px-2 ml-auto" color="success" outline onclick={loadScopes} disabled={moreScopes}> <Button class="px-2 ml-auto" color="success" outline onclick={() => (moreScopes = !moreScopes)} disabled={moreScopes}>
{#if !moreScopes} {#if !moreScopes}
<Icon name="plus-square-fill" style="margin-right:0.25rem"/> Add More Scopes <Icon name="plus-square-fill" style="margin-right:0.25rem"/> Add More Scopes
{:else} {:else}
@@ -141,7 +152,7 @@
<StatsTable <StatsTable
hosts={job.resources.map((r) => r.hostname).sort()} hosts={job.resources.map((r) => r.hostname).sort()}
jobStats={$scopedStats?.data?.scopedJobStats} jobStats={$scopedStats?.data?.scopedJobStats}
{selectedMetrics} selectedMetrics={selectedTableMetrics}
/> />
{/if} {/if}
</TabPane> </TabPane>
@@ -149,12 +160,12 @@
<MetricSelection <MetricSelection
bind:isOpen={isMetricSelectionOpen} bind:isOpen={isMetricSelectionOpen}
bind:totalMetrics bind:totalMetrics
presetMetrics={selectedMetrics} presetMetrics={selectedTableMetrics}
cluster={job.cluster} cluster={job.cluster}
subCluster={job.subCluster} subCluster={job.subCluster}
configName="metricConfig_jobViewTableMetrics" configName="metricConfig_jobViewTableMetrics"
preInitialized {globalMetrics}
applyMetrics={(newMetrics) => applyMetrics={(newMetrics) =>
selectedMetrics = [...newMetrics] selectedTableMetrics = [...newMetrics]
} }
/> />

View File

@@ -5,11 +5,12 @@
- `cluster String`: The nodes' cluster - `cluster String`: The nodes' cluster
- `subCluster String`: The nodes' subCluster [Default: ""] - `subCluster String`: The nodes' subCluster [Default: ""]
- `ccconfig Object?`: The ClusterCockpit Config Context [Default: null] - `ccconfig Object?`: The ClusterCockpit Config Context [Default: null]
- `globalMetrics [Obj]`: Includes the backend supplied availabilities for cluster and subCluster
- `pendingSelectedMetrics [String]`: The array of selected metrics [Default []] - `pendingSelectedMetrics [String]`: The array of selected metrics [Default []]
- `selectedResolution Number?`: The selected data resolution [Default: 0] - `selectedResolution Number?`: The selected data resolution [Default: 0]
- `hostnameFilter String?`: The active hostnamefilter [Default: ""] - `hostnameFilter String?`: The active hostnamefilter [Default: ""]
- `hoststateFilter String?`: The active hoststatefilter [Default: ""] - `hoststateFilter String?`: The active hoststatefilter [Default: ""]
- `presetSystemUnits Object`: The object of metric units [Default: null] - `systemUnits Object`: The object of metric units [Default: null]
- `from Date?`: The selected "from" date [Default: null] - `from Date?`: The selected "from" date [Default: null]
- `to Date?`: The selected "to" date [Default: null] - `to Date?`: The selected "to" date [Default: null]
--> -->
@@ -27,11 +28,12 @@
cluster, cluster,
subCluster = "", subCluster = "",
ccconfig = null, ccconfig = null,
globalMetrics = null,
pendingSelectedMetrics = [], pendingSelectedMetrics = [],
selectedResolution = 0, selectedResolution = 0,
hostnameFilter = "", hostnameFilter = "",
hoststateFilter = "", hoststateFilter = "",
presetSystemUnits = null, systemUnits = null,
from = null, from = null,
to = null to = null
} = $props(); } = $props();
@@ -236,7 +238,7 @@
scope="col" scope="col"
style="padding-top: {headerPaddingTop}px" style="padding-top: {headerPaddingTop}px"
> >
{metric} ({presetSystemUnits[metric]}) {metric} ({systemUnits[metric]})
</th> </th>
{/each} {/each}
</tr> </tr>
@@ -250,7 +252,7 @@
</Row> </Row>
{:else} {:else}
{#each nodes as nodeData (nodeData.host)} {#each nodes as nodeData (nodeData.host)}
<NodeListRow {nodeData} {cluster} {selectedMetrics}/> <NodeListRow {nodeData} {cluster} {selectedMetrics} {globalMetrics}/>
{:else} {:else}
<tr> <tr>
<td colspan={selectedMetrics.length + 1}> No nodes found </td> <td colspan={selectedMetrics.length + 1}> No nodes found </td>

View File

@@ -9,10 +9,10 @@
- `hostnameFilter String?`: The active hoststatefilter [Default: ""] - `hostnameFilter String?`: The active hoststatefilter [Default: ""]
- `from Date?`: The selected "from" date [Default: null] - `from Date?`: The selected "from" date [Default: null]
- `to Date?`: The selected "to" date [Default: null] - `to Date?`: The selected "to" date [Default: null]
- `globalMetrics [Obj]`: Includes the backend supplied availabilities for cluster and subCluster
--> -->
<script> <script>
import { getContext } from "svelte";
import { queryStore, gql, getContextClient } from "@urql/svelte"; import { queryStore, gql, getContextClient } from "@urql/svelte";
import { Row, Col, Card, CardHeader, CardBody, Spinner, Badge } from "@sveltestrap/sveltestrap"; import { Row, Col, Card, CardHeader, CardBody, Spinner, Badge } from "@sveltestrap/sveltestrap";
import { checkMetricDisabled } from "../generic/utils.js"; import { checkMetricDisabled } from "../generic/utils.js";
@@ -26,11 +26,11 @@
hostnameFilter = "", hostnameFilter = "",
hoststateFilter = "", hoststateFilter = "",
from = null, from = null,
to = null to = null,
globalMetrics
} = $props(); } = $props();
/* Const Init */ /* Const Init */
const initialized = getContext("initialized");
const client = getContextClient(); const client = getContextClient();
// Node State Colors // Node State Colors
const stateColors = { const stateColors = {
@@ -87,7 +87,7 @@
}, },
})); }));
const mappedData = $derived(handleQueryData($initialized, $nodesQuery?.data)); const mappedData = $derived(handleQueryData($nodesQuery?.data));
const filteredData = $derived(mappedData.filter((h) => { const filteredData = $derived(mappedData.filter((h) => {
if (hostnameFilter) { if (hostnameFilter) {
if (hoststateFilter == 'all') return h.host.includes(hostnameFilter) if (hoststateFilter == 'all') return h.host.includes(hostnameFilter)
@@ -99,7 +99,7 @@
})); }));
/* Functions */ /* Functions */
function handleQueryData(isInitialized, queryData) { function handleQueryData(queryData) {
let rawData = [] let rawData = []
if (queryData) { if (queryData) {
rawData = queryData.nodeMetrics.filter((h) => { rawData = queryData.nodeMetrics.filter((h) => {
@@ -120,7 +120,8 @@
data: h.metrics.filter( data: h.metrics.filter(
(m) => m?.name == selectedMetric && m.scope == "node", (m) => m?.name == selectedMetric && m.scope == "node",
), ),
disabled: isInitialized ? checkMetricDisabled(selectedMetric, cluster, h.subCluster) : null, // TODO: Move To New Func Variant With Disabled Check on WHole Cluster Level: This never Triggers!
disabled: checkMetricDisabled(globalMetrics, selectedMetric, cluster, h.subCluster),
})) }))
.sort((a, b) => a.host.localeCompare(b.host)) .sort((a, b) => a.host.localeCompare(b.host))
} }
@@ -163,6 +164,7 @@
</div> </div>
{#if item?.data} {#if item?.data}
{#if item.disabled === true} {#if item.disabled === true}
<!-- TODO: Will never be Shown: Overview Single Metric Return Will be Null, see Else Case-->
<Card body class="mx-3" color="info" <Card body class="mx-3" color="info"
>Metric disabled for subcluster <code >Metric disabled for subcluster <code
>{selectedMetric}:{item.subCluster}</code >{selectedMetric}:{item.subCluster}</code
@@ -182,7 +184,7 @@
enableFlip enableFlip
/> />
{/key} {/key}
{:else if item.disabled === null} {:else}
<Card body class="mx-3" color="info"> <Card body class="mx-3" color="info">
Global Metric List Not Initialized Global Metric List Not Initialized
Can not determine {selectedMetric} availability: Please Reload Page Can not determine {selectedMetric} availability: Please Reload Page

View File

@@ -5,6 +5,7 @@
- `cluster String`: The nodes' cluster - `cluster String`: The nodes' cluster
- `nodeData Object`: The node data object including metric data - `nodeData Object`: The node data object including metric data
- `selectedMetrics [String]`: The array of selected metrics - `selectedMetrics [String]`: The array of selected metrics
- `globalMetrics [Obj]`: Includes the backend supplied availabilities for cluster and subCluster
--> -->
<script> <script>
@@ -24,6 +25,7 @@
cluster, cluster,
nodeData, nodeData,
selectedMetrics, selectedMetrics,
globalMetrics
} = $props(); } = $props();
/* Var Init*/ /* Var Init*/
@@ -92,6 +94,7 @@
if (scopedNodeMetric?.data) { if (scopedNodeMetric?.data) {
return { return {
disabled: checkMetricDisabled( disabled: checkMetricDisabled(
globalMetrics,
scopedNodeMetric.data.name, scopedNodeMetric.data.name,
cluster, cluster,
nodeData.subCluster, nodeData.subCluster,