cc-backend/web/frontend/src/utils.js
Christoph Kluge 0c1b66aad9 Adapt svelte to new schema, add removed metric box
- Moved 'scope' field to parent jobMetric
- Implemented unit { prefix, base } where necessary
- SubCluster Metric Config 'remove' option implemented in Joblists
2023-03-30 15:21:35 +02:00

288 lines
8.2 KiB
JavaScript

import { expiringCacheExchange } from './cache-exchange.js'
import { initClient } from '@urql/svelte'
import { setContext, getContext, hasContext, onDestroy, tick } from 'svelte'
import { dedupExchange, fetchExchange } from '@urql/core'
import { readable } from 'svelte/store'
/*
* Call this function only at component initialization time!
*
* It does several things:
* - Initialize the GraphQL client
* - Creates a readable store 'initialization' which indicates when the values below can be used.
* - Adds 'tags' to the context (list of all tags)
* - Adds 'clusters' to the context (object with cluster names as keys)
* - Adds 'metrics' to the context, a function that takes a cluster and metric name and returns the MetricConfig (or undefined)
*/
export function init(extraInitQuery = '') {
const jwt = hasContext('jwt')
? getContext('jwt')
: getContext('cc-config')['jwt']
const client = initClient({
url: `${window.location.origin}/query`,
fetchOptions: jwt != null
? { headers: { 'Authorization': `Bearer ${jwt}` } } : {},
exchanges: [
dedupExchange,
expiringCacheExchange({
ttl: 5 * 60 * 1000,
maxSize: 150,
}),
fetchExchange
]
})
const query = client.query(`query {
clusters {
name,
metricConfig {
name, unit { base, prefix }, peak,
normal, caution, alert,
timestep, scope,
aggregation,
subClusters { name, peak, normal, caution, alert, remove }
}
partitions
subClusters {
name, processorType
socketsPerNode
coresPerSocket
threadsPerCore
flopRateScalar { unit { base, prefix }, value }
flopRateSimd { unit { base, prefix }, value }
memoryBandwidth { unit { base, prefix }, value }
numberOfNodes
topology {
node, socket, core
accelerators { id }
}
}
}
tags { id, name, type }
${extraInitQuery}
}`).toPromise()
let state = { fetching: true, error: null, data: null }
let subscribers = []
const subscribe = (callback) => {
callback(state)
subscribers.push(callback)
return () => {
subscribers = subscribers.filter(cb => cb != callback)
}
};
const tags = [], clusters = []
setContext('tags', tags)
setContext('clusters', clusters)
setContext('metrics', (cluster, metric) => {
if (typeof cluster !== 'object')
cluster = clusters.find(c => c.name == cluster)
return cluster.metricConfig.find(m => m.name == metric)
})
setContext('on-init', callback => state.fetching
? subscribers.push(callback)
: callback(state))
setContext('initialized', readable(false, (set) =>
subscribers.push(() => set(true))))
query.then(({ error, data }) => {
state.fetching = false
if (error != null) {
console.error(error)
state.error = error
tick().then(() => subscribers.forEach(cb => cb(state)))
return
}
for (let tag of data.tags)
tags.push(tag)
for (let cluster of data.clusters)
clusters.push(cluster)
state.data = data
tick().then(() => subscribers.forEach(cb => cb(state)))
})
return {
query: { subscribe },
tags,
clusters,
}
}
export function formatNumber(x) {
let suffix = ''
if (x >= 1000000000) {
x /= 1000000
suffix = 'G'
} else if (x >= 1000000) {
x /= 1000000
suffix = 'M'
} else if (x >= 1000) {
x /= 1000
suffix = 'k'
}
return `${(Math.round(x * 100) / 100)} ${suffix}`
}
// Use https://developer.mozilla.org/en-US/docs/Web/API/structuredClone instead?
export function deepCopy(x) {
return JSON.parse(JSON.stringify(x))
}
function fuzzyMatch(term, string) {
return string.toLowerCase().includes(term)
}
export function fuzzySearchTags(term, tags) {
if (!tags)
return []
let results = []
let termparts = term.split(':').map(s => s.trim()).filter(s => s.length > 0)
if (termparts.length == 0) {
results = tags.slice()
} else if (termparts.length == 1) {
for (let tag of tags)
if (fuzzyMatch(termparts[0], tag.type)
|| fuzzyMatch(termparts[0], tag.name))
results.push(tag)
} else if (termparts.length == 2) {
for (let tag of tags)
if (fuzzyMatch(termparts[0], tag.type)
&& fuzzyMatch(termparts[1], tag.name))
results.push(tag)
}
return results.sort((a, b) => {
if (a.type < b.type) return -1
if (a.type > b.type) return 1
if (a.name < b.name) return -1
if (a.name > b.name) return 1
return 0
})
}
export function groupByScope(jobMetrics) {
let metrics = new Map()
for (let metric of jobMetrics) {
if (metrics.has(metric.name))
metrics.get(metric.name).push(metric)
else
metrics.set(metric.name, [metric])
}
return [...metrics.values()].sort((a, b) => a[0].name.localeCompare(b[0].name))
}
const scopeGranularity = {
"node": 10,
"socket": 5,
"accelerator": 5,
"core": 2,
"hwthread": 1
};
export function maxScope(scopes) {
console.assert(scopes.length > 0 && scopes.every(x => scopeGranularity[x] != null))
let sm = scopes[0], gran = scopeGranularity[scopes[0]]
for (let scope of scopes) {
let otherGran = scopeGranularity[scope]
if (otherGran > gran) {
sm = scope
gran = otherGran
}
}
return sm
}
export function minScope(scopes) {
console.assert(scopes.length > 0 && scopes.every(x => scopeGranularity[x] != null))
let sm = scopes[0], gran = scopeGranularity[scopes[0]]
for (let scope of scopes) {
let otherGran = scopeGranularity[scope]
if (otherGran < gran) {
sm = scope
gran = otherGran
}
}
return sm
}
export async function fetchMetrics(job, metrics, scopes) {
if (job.monitoringStatus == 0)
return null
let query = []
if (metrics != null) {
for (let metric of metrics) {
query.push(`metric=${metric}`)
}
}
if (scopes != null) {
for (let scope of scopes) {
query.push(`scope=${scope}`)
}
}
try {
let res = await fetch(`/api/jobs/metrics/${job.id}${(query.length > 0) ? '?' : ''}${query.join('&')}`)
if (res.status != 200) {
return { error: { status: res.status, message: await res.text() } }
}
return await res.json()
} catch (e) {
return { error: e }
}
}
export function fetchMetricsStore() {
let set = null
let prev = { fetching: true, error: null, data: null }
return [
readable(prev, (_set) => { set = _set }),
(job, metrics, scopes) => fetchMetrics(job, metrics, scopes).then(res => {
let next = { fetching: false, error: res.error, data: res.data }
if (prev.data && next.data)
next.data.jobMetrics.push(...prev.data.jobMetrics)
prev = next
set(next)
})
]
}
export function stickyHeader(datatableHeaderSelector, updatePading) {
const header = document.querySelector('header > nav.navbar')
if (!header)
return
let ticking = false, datatableHeader = null
const onscroll = event => {
if (ticking)
return
ticking = true
window.requestAnimationFrame(() => {
ticking = false
if (!datatableHeader)
datatableHeader = document.querySelector(datatableHeaderSelector)
const top = datatableHeader.getBoundingClientRect().top
updatePading(top < header.clientHeight
? (header.clientHeight - top) + 10
: 10)
})
}
document.addEventListener('scroll', onscroll)
onDestroy(() => document.removeEventListener('scroll', onscroll))
}