Merge pull request #148 from ClusterCockpit/hotfix

Hotfix
This commit is contained in:
Jan Eitzinger 2023-06-16 14:37:10 +02:00 committed by GitHub
commit 07f8950838
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 228 additions and 91 deletions

2
.gitignore vendored
View File

@ -14,3 +14,5 @@
/archive-manager
var/job.db-shm
var/job.db-wal
dist/

70
.goreleaser.yaml Normal file
View File

@ -0,0 +1,70 @@
# This is an example .goreleaser.yml file with some sensible defaults.
before:
hooks:
- go mod tidy
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- darwin
goarch:
- amd64
- arm64
goamd64:
- v2
- v3
goarm:
- "7"
id: "cc-backend"
main: ./cmd/cc-backend
tags:
- static_build
archives:
- format: tar.gz
# this name template makes the OS and Arch compatible with the results of uname.
name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
filters:
exclude:
- "^test:"
- "^chore"
- "merge conflict"
- Merge pull request
- Merge remote-tracking branch
- Merge branch
groups:
- title: "Dependency updates"
regexp: '^.*?(feat|fix)\(deps\)!?:.+$'
order: 300
- title: "New Features"
regexp: '^.*?feat(\([[:word:]]+\))??!?:.+$'
order: 100
- title: "Security updates"
regexp: '^.*?sec(\([[:word:]]+\))??!?:.+$'
order: 150
- title: "Bug fixes"
regexp: '^.*?fix(\([[:word:]]+\))??!?:.+$'
order: 200
- title: "Documentation updates"
regexp: ^.*?doc(\([[:word:]]+\))??!?:.+$
order: 400
- title: Other work
order: 9999
release:
draft: true
footer: |
Supports job archive version 1 and database version 4.
# vim: set ts=2 sw=2 tw=0 fo=cnqoj

View File

@ -5,7 +5,7 @@ FRONTEND = ./web/frontend
VERSION = 1.0.0
GIT_HASH := $(shell git rev-parse --short HEAD || echo 'development')
CURRENT_TIME = $(shell date +"%Y-%m-%d:T%H:%M:%S")
LD_FLAGS = '-s -X main.buildTime=${CURRENT_TIME} -X main.version=${VERSION} -X main.hash=${GIT_HASH}'
LD_FLAGS = '-s -X main.date=${CURRENT_TIME} -X main.version=${VERSION} -X main.commit=${GIT_HASH}'
EXECUTABLES = go npm
K := $(foreach exec,$(EXECUTABLES),\

View File

@ -8,7 +8,7 @@ implementation of ClusterCockpit.
**Breaking changes**
The aggregate job statistic core hours is now computed using the job table
column `num_hwthreads`. In a the future release this column will be renamed to
column `num_hwthreads`. In a future release this column will be renamed to
`num_cores`. For correct display of core hours `num_hwthreads` must be correctly
filled on job start. If your existing jobs do not provide the correct value in
this column then you can set this with one SQL INSERT statement. This only applies
@ -16,7 +16,7 @@ if you have exclusive jobs, only. Please be aware that we treat this column as
it is the number of cores. In case you have SMT enabled and `num_hwthreads`
is not the number of cores the core hours will be too high by a factor!
**Features**
**Notable changes**
* Supports user roles admin, support, manager, user, and api.
* Unified search bar supports job id, job name, project id, user name, and name
* Performance improvements for sqlite db backend
@ -24,4 +24,3 @@ is not the number of cores the core hours will be too high by a factor!
* Better support for shared jobs
* More flexible metric list configuration
* Versioning and migration for database and job archive

View File

@ -59,8 +59,8 @@ const logoString = `
`
var (
buildTime string
hash string
date string
commit string
version string
)
@ -86,8 +86,8 @@ func main() {
if flagVersion {
fmt.Print(logoString)
fmt.Printf("Version:\t%s\n", version)
fmt.Printf("Git hash:\t%s\n", hash)
fmt.Printf("Build time:\t%s\n", buildTime)
fmt.Printf("Git hash:\t%s\n", commit)
fmt.Printf("Build time:\t%s\n", date)
fmt.Printf("SQL db version:\t%d\n", repository.Version)
fmt.Printf("Job archive version:\t%d\n", archive.Version)
os.Exit(0)
@ -245,7 +245,7 @@ func main() {
}
r := mux.NewRouter()
buildInfo := web.Build{Version: version, Hash: hash, Buildtime: buildTime}
buildInfo := web.Build{Version: version, Hash: commit, Buildtime: date}
r.HandleFunc("/login", func(rw http.ResponseWriter, r *http.Request) {
rw.Header().Add("Content-Type", "text/html; charset=utf-8")
@ -320,7 +320,7 @@ func main() {
})
// Mount all /monitoring/... and /api/... routes.
routerConfig.SetupRoutes(secured, version, hash, buildTime)
routerConfig.SetupRoutes(secured, version, commit, date)
api.MountRoutes(secured)
if config.Keys.EmbedStaticFiles {

7
go.mod
View File

@ -6,6 +6,7 @@ require (
github.com/99designs/gqlgen v0.17.24
github.com/ClusterCockpit/cc-units v0.4.0
github.com/Masterminds/squirrel v1.5.3
github.com/go-co-op/gocron v1.25.0
github.com/go-ldap/ldap/v3 v3.4.4
github.com/go-sql-driver/mysql v1.7.0
github.com/golang-jwt/jwt/v4 v4.5.0
@ -25,6 +26,7 @@ require (
github.com/swaggo/swag v1.8.10
github.com/vektah/gqlparser/v2 v2.5.1
golang.org/x/crypto v0.6.0
golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea
)
require (
@ -40,7 +42,6 @@ require (
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
github.com/go-co-op/gocron v1.25.0 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/spec v0.20.8 // indirect
@ -56,7 +57,6 @@ require (
github.com/josharian/intern v1.0.0 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
@ -70,14 +70,11 @@ require (
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/rogpeppe/go-internal v1.8.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/stretchr/testify v1.8.2 // indirect
github.com/swaggo/files v1.0.0 // indirect
github.com/urfave/cli/v2 v2.24.4 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
go.uber.org/atomic v1.10.0 // indirect
golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/oauth2 v0.5.0 // indirect

5
go.sum
View File

@ -828,7 +828,6 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
@ -1010,7 +1009,6 @@ github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi
github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -1073,9 +1071,7 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
@ -1155,7 +1151,6 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/swaggo/files v1.0.0 h1:1gGXVIeUFCS/dta17rnP0iOpr6CXFwKD7EO5ID233e4=
github.com/swaggo/files v1.0.0/go.mod h1:N59U6URJLyU1PQgFqPM7wXLMhJx7QAolnvfQkqO13kc=

View File

@ -6,6 +6,7 @@ package auth
import (
"errors"
"fmt"
"net/http"
"os"
"strings"
@ -85,8 +86,8 @@ func (la *LdapAuthenticator) Login(
userDn := strings.Replace(la.config.UserBind, "{username}", user.Username, -1)
if err := l.Bind(userDn, r.FormValue("password")); err != nil {
log.Error("Error while binding to ldap connection")
return nil, err
log.Errorf("AUTH/LOCAL > Authentication for user %s failed: %v", user.Username, err)
return nil, fmt.Errorf("AUTH/LDAP > Authentication failed")
}
return user, nil

View File

@ -8,6 +8,7 @@ import (
"fmt"
"net/http"
"github.com/ClusterCockpit/cc-backend/pkg/log"
"golang.org/x/crypto/bcrypt"
)
@ -39,7 +40,8 @@ func (la *LocalAuthenticator) Login(
r *http.Request) (*User, error) {
if e := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(r.FormValue("password"))); e != nil {
return nil, fmt.Errorf("AUTH/LOCAL > user '%s' provided the wrong password (%w)", user.Username, e)
log.Errorf("AUTH/LOCAL > Authentication for user %s failed!", user.Username)
return nil, fmt.Errorf("AUTH/LOCAL > Authentication failed")
}
return user, nil

View File

@ -4,6 +4,7 @@
import Histogram from './plots/Histogram.svelte'
import { Row, Col, Spinner, Card, CardHeader, CardTitle, CardBody, Table, Progress, Icon } from 'sveltestrap'
import { init } from './utils.js'
import { scaleNumbers } from './units.js'
import { queryStore, gql, getContextClient } from '@urql/svelte'
const { query: initq } = init()
@ -50,15 +51,17 @@
? sum + (node.metrics.find(m => m.name == metric)?.metric.series.reduce((sum, series) => sum + series.data[series.data.length - 1], 0) || 0)
: sum, 0)
let allocatedNodes = {}, flopRate = {}, flopRateUnit = {}, memBwRate = {}, memBwRateUnit = {}
let allocatedNodes = {}, flopRate = {}, flopRateUnitPrefix = {}, flopRateUnitBase = {}, memBwRate = {}, memBwRateUnitPrefix = {}, memBwRateUnitBase = {}
$: if ($initq.data && $mainQuery.data) {
let subClusters = $initq.data.clusters.find(c => c.name == cluster).subClusters
for (let subCluster of subClusters) {
allocatedNodes[subCluster.name] = $mainQuery.data.allocatedNodes.find(({ name }) => name == subCluster.name)?.count || 0
flopRate[subCluster.name] = Math.floor(sumUp($mainQuery.data.nodeMetrics, subCluster.name, 'flops_any') * 100) / 100
flopRateUnit[subCluster.name] = subCluster.flopRateSimd.unit.prefix + subCluster.flopRateSimd.unit.base
flopRateUnitPrefix[subCluster.name] = subCluster.flopRateSimd.unit.prefix
flopRateUnitBase[subCluster.name] = subCluster.flopRateSimd.unit.base
memBwRate[subCluster.name] = Math.floor(sumUp($mainQuery.data.nodeMetrics, subCluster.name, 'mem_bw') * 100) / 100
memBwRateUnit[subCluster.name] = subCluster.memoryBandwidth.unit.prefix + subCluster.memoryBandwidth.unit.base
memBwRateUnitPrefix[subCluster.name] = subCluster.memoryBandwidth.unit.prefix
memBwRateUnitBase[subCluster.name] = subCluster.memoryBandwidth.unit.base
}
}
@ -111,17 +114,27 @@
<tr class="py-2">
<th scope="col">Allocated Nodes</th>
<td style="min-width: 100px;"><div class="col"><Progress value={allocatedNodes[subCluster.name]} max={subCluster.numberOfNodes}/></div></td>
<td>({allocatedNodes[subCluster.name]} Nodes / {subCluster.numberOfNodes} Total Nodes)</td>
<td>{allocatedNodes[subCluster.name]} / {subCluster.numberOfNodes} Nodes</td>
</tr>
<tr class="py-2">
<th scope="col">Flop Rate (Any) <Icon name="info-circle" class="p-1" style="cursor: help;" title="Flops[Any] = (Flops[Double] x 2) + Flops[Single]"/></th>
<td style="min-width: 100px;"><div class="col"><Progress value={flopRate[subCluster.name]} max={subCluster.flopRateSimd.value * subCluster.numberOfNodes}/></div></td>
<td>({flopRate[subCluster.name]} {flopRateUnit[subCluster.name]} / {(subCluster.flopRateSimd.value * subCluster.numberOfNodes)} {flopRateUnit[subCluster.name]} [Max])</td>
<td>
{scaleNumbers(flopRate[subCluster.name],
(subCluster.flopRateSimd.value * subCluster.numberOfNodes),
flopRateUnitPrefix[subCluster.name])
}{flopRateUnitBase[subCluster.name]} [Max]
</td>
</tr>
<tr class="py-2">
<th scope="col">MemBw Rate</th>
<td style="min-width: 100px;"><div class="col"><Progress value={memBwRate[subCluster.name]} max={subCluster.memoryBandwidth.value * subCluster.numberOfNodes}/></div></td>
<td>({memBwRate[subCluster.name]} {memBwRateUnit[subCluster.name]} / {(subCluster.memoryBandwidth.value * subCluster.numberOfNodes)} {memBwRateUnit[subCluster.name]} [Max])</td>
<td>
{scaleNumbers(memBwRate[subCluster.name],
(subCluster.memoryBandwidth.value * subCluster.numberOfNodes),
memBwRateUnitPrefix[subCluster.name])
}{memBwRateUnitBase[subCluster.name]} [Max]
</td>
</tr>
</Table>
</CardBody>

View File

@ -21,6 +21,8 @@ Changes: remove dependency, text inputs, configurable value ranges, on:change ev
export let max;
export let firstSlider;
export let secondSlider;
export let inputFieldFrom = 0;
export let inputFieldTo = 0;
const dispatch = createEventDispatcher();
@ -33,7 +35,6 @@ Changes: remove dependency, text inputs, configurable value ranges, on:change ev
let leftHandle;
let body;
let slider;
let inputFieldFrom, inputFieldTo;
let timeoutId = null;
function queueChangeEvent() {
@ -45,10 +46,10 @@ Changes: remove dependency, text inputs, configurable value ranges, on:change ev
timeoutId = null;
// Show selection but avoid feedback loop
if (values[0] != null && inputFieldFrom.value != values[0].toString())
inputFieldFrom.value = values[0].toString();
if (values[1] != null && inputFieldTo.value != values[1].toString())
inputFieldTo.value = values[1].toString();
if (values[0] != null && inputFieldFrom != values[0].toString())
inputFieldFrom = values[0].toString();
if (values[1] != null && inputFieldTo != values[1].toString())
inputFieldTo = values[1].toString();
dispatch('change', values);
}, 250);
@ -176,7 +177,7 @@ Changes: remove dependency, text inputs, configurable value ranges, on:change ev
const leftHandleLeft = leftHandle.getBoundingClientRect().left;
const pxStart = clamp((leftHandleLeft + event.detail.dx) - left, 0, parentWidth - width);
const pxStart = clamp((leftHandleLeft + evt.detail.dx) - left, 0, parentWidth - width);
const pxEnd = clamp(pxStart + width, width, parentWidth);
const pStart = pxStart / parentWidth;
@ -190,12 +191,12 @@ Changes: remove dependency, text inputs, configurable value ranges, on:change ev
<div class="double-range-container">
<div class="header">
<input class="form-control" type="text" placeholder="from..." bind:this={inputFieldFrom}
<input class="form-control" type="text" placeholder="from..." bind:value={inputFieldFrom}
on:input={(e) => inputChanged(0, e)} />
<span>Full Range: <b> {min} </b> - <b> {max} </b></span>
<input class="form-control" type="text" placeholder="to..." bind:this={inputFieldTo}
<input class="form-control" type="text" placeholder="to..." bind:value={inputFieldTo}
on:input={(e) => inputChanged(1, e)} />
</div>
<div class="slider" bind:this={slider}>

View File

@ -60,7 +60,10 @@
isTagsOpen = false,
isDurationOpen = false,
isResourcesOpen = false,
isStatsOpen = false
isStatsOpen = false,
isNodesModified = false,
isHwthreadsModified = false,
isAccsModified = false
// Can be called from the outside to trigger a 'update' event from this component.
export function update(additionalFilters = null) {
@ -181,7 +184,7 @@
<Icon name="tags"/> Tags
</DropdownItem>
<DropdownItem on:click={() => (isResourcesOpen = true)}>
<Icon name="hdd-stack"/> Nodes/Accelerators
<Icon name="hdd-stack"/> Resources
</DropdownItem>
<DropdownItem on:click={() => (isStatsOpen = true)}>
<Icon name="bar-chart" on:click={() => (isStatsOpen = true)}/> Statistics
@ -268,9 +271,15 @@
</Info>
{/if}
{#if filters.numNodes.from != null || filters.numNodes.to != null}
{#if filters.numNodes.from != null || filters.numNodes.to != null ||
filters.numHWThreads.from != null || filters.numHWThreads.to != null ||
filters.numAccelerators.from != null || filters.numAccelerators.to != null }
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}>
Nodes: {filters.numNodes.from} - {filters.numNodes.to}
{#if isNodesModified } Nodes: {filters.numNodes.from} - {filters.numNodes.to} {/if}
{#if isNodesModified && isHwthreadsModified }, {/if}
{#if isHwthreadsModified } HWThreads: {filters.numHWThreads.from} - {filters.numHWThreads.to} {/if}
{#if (isNodesModified || isHwthreadsModified) && isAccsModified }, {/if}
{#if isAccsModified } Accelerators: {filters.numAccelerators.from} - {filters.numAccelerators.to} {/if}
</Info>
{/if}
@ -316,6 +325,9 @@
bind:numNodes={filters.numNodes}
bind:numHWThreads={filters.numHWThreads}
bind:numAccelerators={filters.numAccelerators}
bind:isNodesModified={isNodesModified}
bind:isHwthreadsModified={isHwthreadsModified}
bind:isAccsModified={isAccsModified}
on:update={() => update()} />
<Statistics cluster={filters.cluster}

View File

@ -9,20 +9,23 @@
dispatch = createEventDispatcher()
export let cluster = null
export let isModified = false
export let isOpen = false
export let numNodes = { from: null, to: null }
export let numHWThreads = { from: null, to: null }
export let numAccelerators = { from: null, to: null }
export let isNodesModified = false
export let isHwthreadsModified = false
export let isAccsModified = false
let pendingNumNodes = numNodes, pendingNumHWThreads = numHWThreads, pendingNumAccelerators = numAccelerators
$: isModified = pendingNumNodes.from != numNodes.from || pendingNumNodes.to != numNodes.to
|| pendingNumHWThreads.from != numHWThreads.from || pendingNumHWThreads.to != numHWThreads.to
|| pendingNumAccelerators.from != numAccelerators.from || pendingNumAccelerators.to != numAccelerators.to
const findMaxNumAccels = clusters => clusters.reduce((max, cluster) => Math.max(max,
cluster.subClusters.reduce((max, sc) => Math.max(max, sc.topology.accelerators?.length || 0), 0)), 0)
// Limited to Single-Node Thread Count
const findMaxNumHWTreadsPerNode = clusters => clusters.reduce((max, cluster) => Math.max(max,
cluster.subClusters.reduce((max, sc) => Math.max(max, (sc.threadsPerCore * sc.coresPerSocket * sc.socketsPerNode) || 0), 0)), 0)
// console.log(header)
let minNumNodes = 1, maxNumNodes = 0, minNumHWThreads = 1, maxNumHWThreads = 0, minNumAccelerators = 0, maxNumAccelerators = 0
$: {
@ -33,11 +36,13 @@
minNumNodes = filterRanges.numNodes.from
maxNumNodes = filterRanges.numNodes.to
maxNumAccelerators = findMaxNumAccels([{ subClusters }])
maxNumHWThreads = findMaxNumHWTreadsPerNode([{ subClusters }])
} else if (clusters.length > 0) {
const { filterRanges } = header.clusters[0]
minNumNodes = filterRanges.numNodes.from
maxNumNodes = filterRanges.numNodes.to
maxNumAccelerators = findMaxNumAccels(clusters)
maxNumHWThreads = findMaxNumHWTreadsPerNode(clusters)
for (let cluster of header.clusters) {
const { filterRanges } = cluster
minNumNodes = Math.min(minNumNodes, filterRanges.numNodes.from)
@ -52,27 +57,53 @@
pendingNumNodes = { from: 0, to: maxNumNodes }
}
}
$: {
if (isOpen && $initialized && ((pendingNumHWThreads.from == null && pendingNumHWThreads.to == null) || (isHwthreadsModified == false))) {
pendingNumHWThreads = { from: 0, to: maxNumHWThreads }
}
}
$: if ( maxNumAccelerators != null && maxNumAccelerators > 1 ) {
if (isOpen && $initialized && pendingNumAccelerators.from == null && pendingNumAccelerators.to == null) {
pendingNumAccelerators = { from: 0, to: maxNumAccelerators }
}
}
</script>
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>
Select Number of Nodes, HWThreads and Accelerators
Select number of utilized Resources
</ModalHeader>
<ModalBody>
<h4>Number of Nodes</h4>
<h6>Number of Nodes</h6>
<DoubleRangeSlider
on:change={({ detail }) => (pendingNumNodes = { from: detail[0], to: detail[1] })}
on:change={({ detail }) => {
pendingNumNodes = { from: detail[0], to: detail[1] }
isNodesModified = true
}}
min={minNumNodes} max={maxNumNodes}
firstSlider={pendingNumNodes.from} secondSlider={pendingNumNodes.to} />
<!-- <DoubleRangeSlider
on:change={({ detail }) => (pendingNumHWThreads = { from: detail[0], to: detail[1] })}
min={minNumHWThreads} max={maxNumHWThreads}
firstSlider={pendingNumHWThreads.from} secondSlider={pendingNumHWThreads.to} /> -->
{#if maxNumAccelerators != null && maxNumAccelerators > 1}
firstSlider={pendingNumNodes.from} secondSlider={pendingNumNodes.to}
inputFieldFrom={pendingNumNodes.from} inputFieldTo={pendingNumNodes.to}/>
<h6 style="margin-top: 1rem;">Number of HWThreads (Use for Single-Node Jobs)</h6>
<DoubleRangeSlider
on:change={({ detail }) => (pendingNumAccelerators = { from: detail[0], to: detail[1] })}
on:change={({ detail }) => {
pendingNumHWThreads = { from: detail[0], to: detail[1] }
isHwthreadsModified = true
}}
min={minNumHWThreads} max={maxNumHWThreads}
firstSlider={pendingNumHWThreads.from} secondSlider={pendingNumHWThreads.to}
inputFieldFrom={pendingNumHWThreads.from} inputFieldTo={pendingNumHWThreads.to}/>
{#if maxNumAccelerators != null && maxNumAccelerators > 1}
<h6 style="margin-top: 1rem;">Number of Accelerators</h6>
<DoubleRangeSlider
on:change={({ detail }) => {
pendingNumAccelerators = { from: detail[0], to: detail[1] }
isAccsModified = true
}}
min={minNumAccelerators} max={maxNumAccelerators}
firstSlider={pendingNumAccelerators.from} secondSlider={pendingNumAccelerators.to} />
firstSlider={pendingNumAccelerators.from} secondSlider={pendingNumAccelerators.to}
inputFieldFrom={pendingNumAccelerators.from} inputFieldTo={pendingNumAccelerators.to}/>
{/if}
</ModalBody>
<ModalFooter>
@ -80,7 +111,10 @@
disabled={pendingNumNodes.from == null || pendingNumNodes.to == null}
on:click={() => {
isOpen = false
numNodes = { from: pendingNumNodes.from, to: pendingNumNodes.to }
pendingNumNodes = isNodesModified ? pendingNumNodes : { from: null, to: null }
pendingNumHWThreads = isHwthreadsModified ? pendingNumHWThreads : { from: null, to: null }
pendingNumAccelerators = isAccsModified ? pendingNumAccelerators : { from: null, to: null }
numNodes ={ from: pendingNumNodes.from, to: pendingNumNodes.to }
numHWThreads = { from: pendingNumHWThreads.from, to: pendingNumHWThreads.to }
numAccelerators = { from: pendingNumAccelerators.from, to: pendingNumAccelerators.to }
dispatch('update', { numNodes, numHWThreads, numAccelerators })
@ -95,6 +129,9 @@
numNodes = { from: pendingNumNodes.from, to: pendingNumNodes.to }
numHWThreads = { from: pendingNumHWThreads.from, to: pendingNumHWThreads.to }
numAccelerators = { from: pendingNumAccelerators.from, to: pendingNumAccelerators.to }
isNodesModified = false
isHwthreadsModified = false
isAccsModified = false
dispatch('update', { numNodes, numHWThreads, numAccelerators })
}}>Reset</Button>
<Button on:click={() => (isOpen = false)}>Close</Button>

View File

@ -93,7 +93,8 @@
<DoubleRangeSlider
on:change={({ detail }) => (stat.from = detail[0], stat.to = detail[1], stat.enabled = true)}
min={0} max={stat.peak}
firstSlider={stat.from} secondSlider={stat.to} />
firstSlider={stat.from} secondSlider={stat.to}
inputFieldFrom={stat.from} inputFieldTo={stat.to}/>
{/each}
</ModalBody>
<ModalFooter>
@ -104,7 +105,8 @@
}}>Close & Apply</Button>
<Button color="danger" on:click={() => {
isOpen = false
statistics.forEach(stat => stat.enabled = false)
resetRange($initialized, cluster)
statistics.forEach(stat => (stat.enabled = false))
stats = []
dispatch('update', { stats })
}}>Reset</Button>

View File

@ -196,7 +196,7 @@
</style>
<script context="module">
import { formatNumber } from '../utils.js'
import { formatNumber } from '../units.js'
export function binsFromFootprint(weights, values, numBins) {
let min = 0, max = 0

View File

@ -22,7 +22,7 @@
-->
<script>
import uPlot from 'uplot'
import { formatNumber } from '../utils.js'
import { formatNumber } from '../units.js'
import { getContext, onMount, onDestroy } from 'svelte'
export let width
@ -312,7 +312,9 @@
</script>
<!--Add empty series warning card-->
<div bind:this={plotWrapper} class="cc-plot"></div>
<style>
.cc-plot {
border-radius: 5px;

View File

@ -46,16 +46,6 @@
}
}
const power = [1, 1e3, 1e6, 1e9, 1e12]
const suffix = ['', 'k', 'm', 'g']
function formatNumber(x) {
for (let i = 0; i < suffix.length; i++)
if (power[i] <= x && x < power[i+1])
return `${x / power[i]}${suffix[i]}`
return Math.abs(x) >= 1000 ? x.toExponential() : x.toString()
}
function axisStepFactor(i, size) {
if (size && size < 500)
return 10
@ -307,6 +297,7 @@
<script>
import { onMount, tick } from 'svelte'
import { formatNumber } from '../units.js'
export let flopsAny = null
export let memBw = null

View File

@ -3,7 +3,7 @@
</div>
<script context="module">
import { formatNumber } from '../utils.js'
import { formatNumber } from '../units.js'
const axesColor = '#aaaaaa'
const fontSize = 12

29
web/frontend/src/units.js Normal file
View File

@ -0,0 +1,29 @@
/*
Collect Functions for Unit Handling and Scaling Here
*/
const power = [1, 1e3, 1e6, 1e9, 1e12, 1e15, 1e18, 1e21]
const prefix = ['', 'K', 'M', 'G', 'T', 'P', 'E']
export function formatNumber(x) {
for (let i = 0; i < prefix.length; i++)
if (power[i] <= x && x < power[i+1])
return `${Math.round((x / power[i]) * 100) / 100} ${prefix[i]}`
return Math.abs(x) >= 1000 ? x.toExponential() : x.toString()
}
export function scaleNumbers(x, y , p = '') {
const oldPower = power[prefix.indexOf(p)]
const rawXValue = x * oldPower
const rawYValue = y * oldPower
for (let i = 0; i < prefix.length; i++) {
if (power[i] <= rawYValue && rawYValue < power[i+1]) {
return `${Math.round((rawXValue / power[i]) * 100) / 100} / ${Math.round((rawYValue / power[i]) * 100) / 100} ${prefix[i]}`
}
}
return Math.abs(rawYValue) >= 1000 ? `${rawXValue.toExponential()} / ${rawYValue.toExponential()}` : `${rawYValue.toString()} / ${rawYValue.toString()}`
}

View File

@ -123,22 +123,6 @@ export function init(extraInitQuery = "") {
};
}
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));