Merge branch 'master' into 37-provide-a-s3-compatible-storage-backend-for-the-job-archive

This commit is contained in:
Jan Eitzinger 2024-02-15 11:01:38 +01:00
commit 03a496d477
32 changed files with 2247 additions and 231 deletions

View File

@ -28,6 +28,11 @@ type Job {
resources: [Resource!]! resources: [Resource!]!
concurrentJobs: JobLinkResultList concurrentJobs: JobLinkResultList
memUsedMax: Float
flopsAnyAvg: Float
memBwAvg: Float
loadAvg: Float
metaData: Any metaData: Any
userData: User userData: User
} }
@ -198,7 +203,7 @@ type Query {
jobsFootprints(filter: [JobFilter!], metrics: [String!]!): Footprints jobsFootprints(filter: [JobFilter!], metrics: [String!]!): Footprints
jobs(filter: [JobFilter!], page: PageRequest, order: OrderByInput): JobResultList! jobs(filter: [JobFilter!], page: PageRequest, order: OrderByInput): JobResultList!
jobsStatistics(filter: [JobFilter!], page: PageRequest, sortBy: SortByAggregate, groupBy: Aggregate): [JobsStatistics!]! jobsStatistics(filter: [JobFilter!], metrics: [String!], page: PageRequest, sortBy: SortByAggregate, groupBy: Aggregate): [JobsStatistics!]!
rooflineHeatmap(filter: [JobFilter!]!, rows: Int!, cols: Int!, minX: Float!, minY: Float!, maxX: Float!, maxY: Float!): [[Float!]!]! rooflineHeatmap(filter: [JobFilter!]!, rows: Int!, cols: Int!, minX: Float!, minY: Float!, maxX: Float!, maxY: Float!): [[Float!]!]!
@ -286,6 +291,19 @@ type HistoPoint {
value: Int! value: Int!
} }
type MetricHistoPoints {
metric: String!
unit: String!
data: [MetricHistoPoint!]
}
type MetricHistoPoint {
bin: Int
count: Int!
min: Int
max: Int
}
type JobsStatistics { type JobsStatistics {
id: ID! # If `groupBy` was used, ID of the user/project/cluster id: ID! # If `groupBy` was used, ID of the user/project/cluster
name: String! # if User-Statistics: Given Name of Account (ID) Owner name: String! # if User-Statistics: Given Name of Account (ID) Owner
@ -303,6 +321,7 @@ type JobsStatistics {
histNumNodes: [HistoPoint!]! # value: number of nodes, count: number of jobs with that number of nodes histNumNodes: [HistoPoint!]! # value: number of nodes, count: number of jobs with that number of nodes
histNumCores: [HistoPoint!]! # value: number of cores, count: number of jobs with that number of cores histNumCores: [HistoPoint!]! # value: number of cores, count: number of jobs with that number of cores
histNumAccs: [HistoPoint!]! # value: number of accs, count: number of jobs with that number of accs histNumAccs: [HistoPoint!]! # value: number of accs, count: number of jobs with that number of accs
histMetrics: [MetricHistoPoints!]! # metric: metricname, data array of histopoints: value: metric average bin, count: number of jobs with that metric average
} }
input PageRequest { input PageRequest {

8
go.mod
View File

@ -26,7 +26,7 @@ require (
github.com/swaggo/http-swagger v1.3.3 github.com/swaggo/http-swagger v1.3.3
github.com/swaggo/swag v1.16.2 github.com/swaggo/swag v1.16.2
github.com/vektah/gqlparser/v2 v2.5.10 github.com/vektah/gqlparser/v2 v2.5.10
golang.org/x/crypto v0.15.0 golang.org/x/crypto v0.16.0
golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea
) )
@ -84,11 +84,11 @@ require (
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
go.uber.org/atomic v1.10.0 // indirect go.uber.org/atomic v1.10.0 // indirect
golang.org/x/mod v0.14.0 // indirect golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.18.0 // indirect golang.org/x/net v0.19.0 // indirect
golang.org/x/oauth2 v0.5.0 // indirect golang.org/x/oauth2 v0.5.0 // indirect
golang.org/x/sys v0.14.0 // indirect golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.15.0 // indirect golang.org/x/tools v0.16.0 // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.30.0 // indirect google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect

17
go.sum
View File

@ -1302,8 +1302,8 @@ golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -1425,8 +1425,8 @@ golang.org/x/net v0.0.0-20220111093109-d55c255bac03/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@ -1585,9 +1585,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@ -1694,8 +1693,8 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8= golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM=
golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

File diff suppressed because it is too large Load Diff

View File

@ -101,6 +101,7 @@ type JobsStatistics struct {
HistNumNodes []*HistoPoint `json:"histNumNodes"` HistNumNodes []*HistoPoint `json:"histNumNodes"`
HistNumCores []*HistoPoint `json:"histNumCores"` HistNumCores []*HistoPoint `json:"histNumCores"`
HistNumAccs []*HistoPoint `json:"histNumAccs"` HistNumAccs []*HistoPoint `json:"histNumAccs"`
HistMetrics []*MetricHistoPoints `json:"histMetrics"`
} }
type MetricFootprints struct { type MetricFootprints struct {
@ -108,6 +109,19 @@ type MetricFootprints struct {
Data []schema.Float `json:"data"` Data []schema.Float `json:"data"`
} }
type MetricHistoPoint struct {
Bin *int `json:"bin,omitempty"`
Count int `json:"count"`
Min *int `json:"min,omitempty"`
Max *int `json:"max,omitempty"`
}
type MetricHistoPoints struct {
Metric string `json:"metric"`
Unit string `json:"unit"`
Data []*MetricHistoPoint `json:"data,omitempty"`
}
type NodeMetrics struct { type NodeMetrics struct {
Host string `json:"host"` Host string `json:"host"`
SubCluster string `json:"subCluster"` SubCluster string `json:"subCluster"`

View File

@ -2,7 +2,7 @@ package graph
// This file will be automatically regenerated based on the schema, any resolver implementations // This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end. // will be copied through when generating and any unknown code will be moved to the end.
// Code generated by github.com/99designs/gqlgen version v0.17.36 // Code generated by github.com/99designs/gqlgen version v0.17.40
import ( import (
"context" "context"
@ -244,7 +244,7 @@ func (r *queryResolver) Jobs(ctx context.Context, filter []*model.JobFilter, pag
} }
// JobsStatistics is the resolver for the jobsStatistics field. // JobsStatistics is the resolver for the jobsStatistics field.
func (r *queryResolver) JobsStatistics(ctx context.Context, filter []*model.JobFilter, page *model.PageRequest, sortBy *model.SortByAggregate, groupBy *model.Aggregate) ([]*model.JobsStatistics, error) { func (r *queryResolver) JobsStatistics(ctx context.Context, filter []*model.JobFilter, metrics []string, page *model.PageRequest, sortBy *model.SortByAggregate, groupBy *model.Aggregate) ([]*model.JobsStatistics, error) {
var err error var err error
var stats []*model.JobsStatistics var stats []*model.JobsStatistics
@ -291,6 +291,17 @@ func (r *queryResolver) JobsStatistics(ctx context.Context, filter []*model.JobF
} }
} }
if requireField(ctx, "histMetrics") {
if groupBy == nil {
stats[0], err = r.Repo.AddMetricHistograms(ctx, filter, metrics, stats[0])
if err != nil {
return nil, err
}
} else {
return nil, errors.New("metric histograms only implemented without groupBy argument")
}
}
return stats, nil return stats, nil
} }

View File

@ -60,7 +60,7 @@ func GetJobRepository() *JobRepository {
var jobColumns []string = []string{ var jobColumns []string = []string{
"job.id", "job.job_id", "job.user", "job.project", "job.cluster", "job.subcluster", "job.start_time", "job.partition", "job.array_job_id", "job.id", "job.job_id", "job.user", "job.project", "job.cluster", "job.subcluster", "job.start_time", "job.partition", "job.array_job_id",
"job.num_nodes", "job.num_hwthreads", "job.num_acc", "job.exclusive", "job.monitoring_status", "job.smt", "job.job_state", "job.num_nodes", "job.num_hwthreads", "job.num_acc", "job.exclusive", "job.monitoring_status", "job.smt", "job.job_state",
"job.duration", "job.walltime", "job.resources", // "job.meta_data", "job.duration", "job.walltime", "job.resources", "job.mem_used_max", "job.flops_any_avg", "job.mem_bw_avg", "job.load_avg", // "job.meta_data",
} }
func scanJob(row interface{ Scan(...interface{}) error }) (*schema.Job, error) { func scanJob(row interface{ Scan(...interface{}) error }) (*schema.Job, error) {
@ -68,7 +68,7 @@ func scanJob(row interface{ Scan(...interface{}) error }) (*schema.Job, error) {
if err := row.Scan( if err := row.Scan(
&job.ID, &job.JobID, &job.User, &job.Project, &job.Cluster, &job.SubCluster, &job.StartTimeUnix, &job.Partition, &job.ArrayJobId, &job.ID, &job.JobID, &job.User, &job.Project, &job.Cluster, &job.SubCluster, &job.StartTimeUnix, &job.Partition, &job.ArrayJobId,
&job.NumNodes, &job.NumHWThreads, &job.NumAcc, &job.Exclusive, &job.MonitoringStatus, &job.SMT, &job.State, &job.NumNodes, &job.NumHWThreads, &job.NumAcc, &job.Exclusive, &job.MonitoringStatus, &job.SMT, &job.State,
&job.Duration, &job.Walltime, &job.RawResources /*&job.RawMetaData*/); err != nil { &job.Duration, &job.Walltime, &job.RawResources, &job.MemUsedMax, &job.FlopsAnyAvg, &job.MemBwAvg, &job.LoadAvg /*&job.RawMetaData*/); err != nil {
log.Warnf("Error while scanning rows (Job): %v", err) log.Warnf("Error while scanning rows (Job): %v", err)
return nil, err return nil, err
} }
@ -483,6 +483,7 @@ func (r *JobRepository) MarkArchived(
case "mem_bw": case "mem_bw":
stmt = stmt.Set("mem_bw_avg", stats.Avg) stmt = stmt.Set("mem_bw_avg", stats.Avg)
case "load": case "load":
stmt = stmt.Set("load_avg", stats.Avg)
case "cpu_load": case "cpu_load":
stmt = stmt.Set("load_avg", stats.Avg) stmt = stmt.Set("load_avg", stats.Avg)
case "net_bw": case "net_bw":

View File

@ -96,7 +96,7 @@ func SecurityCheck(ctx context.Context, query sq.SelectBuilder) (sq.SelectBuilde
user := GetUserFromContext(ctx) user := GetUserFromContext(ctx)
if user == nil { if user == nil {
var qnil sq.SelectBuilder var qnil sq.SelectBuilder
return qnil, fmt.Errorf("user context is nil!") return qnil, fmt.Errorf("user context is nil")
} else if user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleApi}) { // Admin & Co. : All jobs } else if user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleApi}) { // Admin & Co. : All jobs
return query, nil return query, nil
} else if user.HasRole(schema.RoleManager) { // Manager : Add filter for managed projects' jobs only + personal jobs } else if user.HasRole(schema.RoleManager) { // Manager : Add filter for managed projects' jobs only + personal jobs

View File

@ -8,11 +8,15 @@ import (
"context" "context"
"database/sql" "database/sql"
"fmt" "fmt"
"math"
"time" "time"
"github.com/ClusterCockpit/cc-backend/internal/config" "github.com/ClusterCockpit/cc-backend/internal/config"
"github.com/ClusterCockpit/cc-backend/internal/graph/model" "github.com/ClusterCockpit/cc-backend/internal/graph/model"
"github.com/ClusterCockpit/cc-backend/internal/metricdata"
"github.com/ClusterCockpit/cc-backend/pkg/archive"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema"
sq "github.com/Masterminds/squirrel" sq "github.com/Masterminds/squirrel"
) )
@ -450,6 +454,39 @@ func (r *JobRepository) AddHistograms(
return stat, nil return stat, nil
} }
// Requires thresholds for metric from config for cluster? Of all clusters and use largest? split to 10 + 1 for artifacts?
func (r *JobRepository) AddMetricHistograms(
ctx context.Context,
filter []*model.JobFilter,
metrics []string,
stat *model.JobsStatistics) (*model.JobsStatistics, error) {
start := time.Now()
// Running Jobs Only: First query jobdata from sqlite, then query data and make bins
for _, f := range filter {
if f.State != nil {
if len(f.State) == 1 && f.State[0] == "running" {
stat.HistMetrics = r.runningJobsMetricStatisticsHistogram(ctx, metrics, filter)
log.Debugf("Timer AddMetricHistograms %s", time.Since(start))
return stat, nil
}
}
}
// All other cases: Query and make bins in sqlite directly
for _, m := range metrics {
metricHisto, err := r.jobsMetricStatisticsHistogram(ctx, m, filter)
if err != nil {
log.Warnf("Error while loading job metric statistics histogram: %s", m)
continue
}
stat.HistMetrics = append(stat.HistMetrics, metricHisto)
}
log.Debugf("Timer AddMetricHistograms %s", time.Since(start))
return stat, nil
}
// `value` must be the column grouped by, but renamed to "value" // `value` must be the column grouped by, but renamed to "value"
func (r *JobRepository) jobsStatisticsHistogram( func (r *JobRepository) jobsStatisticsHistogram(
ctx context.Context, ctx context.Context,
@ -487,3 +524,231 @@ func (r *JobRepository) jobsStatisticsHistogram(
log.Debugf("Timer jobsStatisticsHistogram %s", time.Since(start)) log.Debugf("Timer jobsStatisticsHistogram %s", time.Since(start))
return points, nil return points, nil
} }
func (r *JobRepository) jobsMetricStatisticsHistogram(
ctx context.Context,
metric string,
filters []*model.JobFilter) (*model.MetricHistoPoints, error) {
var dbMetric string
switch metric {
case "cpu_load":
dbMetric = "load_avg"
case "flops_any":
dbMetric = "flops_any_avg"
case "mem_bw":
dbMetric = "mem_bw_avg"
case "mem_used":
dbMetric = "mem_used_max"
case "net_bw":
dbMetric = "net_bw_avg"
case "file_bw":
dbMetric = "file_bw_avg"
default:
return nil, fmt.Errorf("%s not implemented", metric)
}
// Get specific Peak or largest Peak
var metricConfig *schema.MetricConfig
var peak float64 = 0.0
var unit string = ""
for _, f := range filters {
if f.Cluster != nil {
metricConfig = archive.GetMetricConfig(*f.Cluster.Eq, metric)
peak = metricConfig.Peak
unit = metricConfig.Unit.Prefix + metricConfig.Unit.Base
log.Debugf("Cluster %s filter found with peak %f for %s", *f.Cluster.Eq, peak, metric)
}
}
if peak == 0.0 {
for _, c := range archive.Clusters {
for _, m := range c.MetricConfig {
if m.Name == metric {
if m.Peak > peak {
peak = m.Peak
}
if unit == "" {
unit = m.Unit.Prefix + m.Unit.Base
}
}
}
}
}
// log.Debugf("Metric %s: DB %s, Peak %f, Unit %s", metric, dbMetric, peak, unit)
// Make bins, see https://jereze.com/code/sql-histogram/
start := time.Now()
crossJoinQuery := sq.Select(
fmt.Sprintf(`max(%s) as max`, dbMetric),
fmt.Sprintf(`min(%s) as min`, dbMetric),
).From("job").Where(
fmt.Sprintf(`%s is not null`, dbMetric),
).Where(
fmt.Sprintf(`%s <= %f`, dbMetric, peak),
)
crossJoinQuery, cjqerr := SecurityCheck(ctx, crossJoinQuery)
if cjqerr != nil {
return nil, cjqerr
}
for _, f := range filters {
crossJoinQuery = BuildWhereClause(f, crossJoinQuery)
}
crossJoinQuerySql, crossJoinQueryArgs, sqlerr := crossJoinQuery.ToSql()
if sqlerr != nil {
return nil, sqlerr
}
bins := 10
binQuery := fmt.Sprintf(`CAST( (case when job.%s = value.max then value.max*0.999999999 else job.%s end - value.min) / (value.max - value.min) * %d as INTEGER )`, dbMetric, dbMetric, bins)
mainQuery := sq.Select(
fmt.Sprintf(`%s + 1 as bin`, binQuery),
fmt.Sprintf(`count(job.%s) as count`, dbMetric),
fmt.Sprintf(`CAST(((value.max / %d) * (%s )) as INTEGER ) as min`, bins, binQuery),
fmt.Sprintf(`CAST(((value.max / %d) * (%s + 1 )) as INTEGER ) as max`, bins, binQuery),
).From("job").CrossJoin(
fmt.Sprintf(`(%s) as value`, crossJoinQuerySql), crossJoinQueryArgs...,
).Where(fmt.Sprintf(`job.%s is not null and job.%s <= %f`, dbMetric, dbMetric, peak))
mainQuery, qerr := SecurityCheck(ctx, mainQuery)
if qerr != nil {
return nil, qerr
}
for _, f := range filters {
mainQuery = BuildWhereClause(f, mainQuery)
}
// Finalize query with Grouping and Ordering
mainQuery = mainQuery.GroupBy("bin").OrderBy("bin")
rows, err := mainQuery.RunWith(r.DB).Query()
if err != nil {
log.Errorf("Error while running mainQuery: %s", err)
return nil, err
}
points := make([]*model.MetricHistoPoint, 0)
for rows.Next() {
point := model.MetricHistoPoint{}
if err := rows.Scan(&point.Bin, &point.Count, &point.Min, &point.Max); err != nil {
log.Warnf("Error while scanning rows for %s", metric)
return nil, err // Totally bricks cc-backend if returned and if all metrics requested?
}
points = append(points, &point)
}
result := model.MetricHistoPoints{Metric: metric, Unit: unit, Data: points}
log.Debugf("Timer jobsStatisticsHistogram %s", time.Since(start))
return &result, nil
}
func (r *JobRepository) runningJobsMetricStatisticsHistogram(
ctx context.Context,
metrics []string,
filters []*model.JobFilter) []*model.MetricHistoPoints {
// Get Jobs
jobs, err := r.QueryJobs(ctx, filters, &model.PageRequest{Page: 1, ItemsPerPage: 500 + 1}, nil)
if err != nil {
log.Errorf("Error while querying jobs for footprint: %s", err)
return nil
}
if len(jobs) > 500 {
log.Errorf("too many jobs matched (max: %d)", 500)
return nil
}
// Get AVGs from metric repo
avgs := make([][]schema.Float, len(metrics))
for i := range avgs {
avgs[i] = make([]schema.Float, 0, len(jobs))
}
for _, job := range jobs {
if job.MonitoringStatus == schema.MonitoringStatusDisabled || job.MonitoringStatus == schema.MonitoringStatusArchivingFailed {
continue
}
if err := metricdata.LoadAverages(job, metrics, avgs, ctx); err != nil {
log.Errorf("Error while loading averages for histogram: %s", err)
return nil
}
}
// Iterate metrics to fill endresult
data := make([]*model.MetricHistoPoints, 0)
for idx, metric := range metrics {
// Get specific Peak or largest Peak
var metricConfig *schema.MetricConfig
var peak float64 = 0.0
var unit string = ""
for _, f := range filters {
if f.Cluster != nil {
metricConfig = archive.GetMetricConfig(*f.Cluster.Eq, metric)
peak = metricConfig.Peak
unit = metricConfig.Unit.Prefix + metricConfig.Unit.Base
log.Debugf("Cluster %s filter found with peak %f for %s", *f.Cluster.Eq, peak, metric)
}
}
if peak == 0.0 {
for _, c := range archive.Clusters {
for _, m := range c.MetricConfig {
if m.Name == metric {
if m.Peak > peak {
peak = m.Peak
}
if unit == "" {
unit = m.Unit.Prefix + m.Unit.Base
}
}
}
}
}
// Make and fill bins
bins := 10.0
peakBin := peak / bins
points := make([]*model.MetricHistoPoint, 0)
for b := 0; b < 10; b++ {
count := 0
bindex := b + 1
bmin := math.Round(peakBin * float64(b))
bmax := math.Round(peakBin * (float64(b) + 1.0))
// Iterate AVG values for indexed metric and count for bins
for _, val := range avgs[idx] {
if float64(val) >= bmin && float64(val) < bmax {
count += 1
}
}
bminint := int(bmin)
bmaxint := int(bmax)
// Append Bin to Metric Result Array
point := model.MetricHistoPoint{Bin: &bindex, Count: count, Min: &bminint, Max: &bmaxint}
points = append(points, &point)
}
// Append Metric Result Array to final results array
result := model.MetricHistoPoints{Metric: metric, Unit: unit, Data: points}
data = append(data, &result)
}
return data
}

View File

@ -8,8 +8,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/ClusterCockpit/cc-backend/pkg/schema"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema"
) )
var Clusters []*schema.Cluster var Clusters []*schema.Cluster

View File

@ -54,10 +54,10 @@ type Job struct {
BaseJob BaseJob
StartTimeUnix int64 `json:"-" db:"start_time" example:"1649723812"` // Start epoch time stamp in seconds StartTimeUnix int64 `json:"-" db:"start_time" example:"1649723812"` // Start epoch time stamp in seconds
StartTime time.Time `json:"startTime"` // Start time as 'time.Time' data type StartTime time.Time `json:"startTime"` // Start time as 'time.Time' data type
MemUsedMax float64 `json:"-" db:"mem_used_max"` // MemUsedMax as Float64 MemUsedMax float64 `json:"memUsedMax" db:"mem_used_max"` // MemUsedMax as Float64
FlopsAnyAvg float64 `json:"-" db:"flops_any_avg"` // FlopsAnyAvg as Float64 FlopsAnyAvg float64 `json:"flopsAnyAvg" db:"flops_any_avg"` // FlopsAnyAvg as Float64
MemBwAvg float64 `json:"-" db:"mem_bw_avg"` // MemBwAvg as Float64 MemBwAvg float64 `json:"memBwAvg" db:"mem_bw_avg"` // MemBwAvg as Float64
LoadAvg float64 `json:"-" db:"load_avg"` // LoadAvg as Float64 LoadAvg float64 `json:"loadAvg" db:"load_avg"` // LoadAvg as Float64
NetBwAvg float64 `json:"-" db:"net_bw_avg"` // NetBwAvg as Float64 NetBwAvg float64 `json:"-" db:"net_bw_avg"` // NetBwAvg as Float64
NetDataVolTotal float64 `json:"-" db:"net_data_vol_total"` // NetDataVolTotal as Float64 NetDataVolTotal float64 `json:"-" db:"net_data_vol_total"` // NetDataVolTotal as Float64
FileBwAvg float64 `json:"-" db:"file_bw_avg"` // FileBwAvg as Float64 FileBwAvg float64 `json:"-" db:"file_bw_avg"` // FileBwAvg as Float64

View File

@ -12,7 +12,10 @@
"@rollup/plugin-replace": "^5.0.2", "@rollup/plugin-replace": "^5.0.2",
"@urql/svelte": "^4.0.1", "@urql/svelte": "^4.0.1",
"chart.js": "^4.3.3", "chart.js": "^4.3.3",
"date-fns": "^2.30.0",
"date-fns": "^2.30.0",
"graphql": "^16.6.0", "graphql": "^16.6.0",
"mathjs": "^12.0.0",
"svelte-chartjs": "^3.1.2", "svelte-chartjs": "^3.1.2",
"sveltestrap": "^5.11.1", "sveltestrap": "^5.11.1",
"uplot": "^1.6.24", "uplot": "^1.6.24",
@ -42,6 +45,17 @@
} }
} }
}, },
"node_modules/@babel/runtime": {
"version": "7.23.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.5.tgz",
"integrity": "sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.3", "version": "0.3.3",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
@ -90,9 +104,12 @@
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
}, },
"node_modules/@jridgewell/trace-mapping": { "node_modules/@jridgewell/trace-mapping": {
"version": "0.3.19", "version": "0.3.20",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz",
"integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==",
"version": "0.3.20",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz",
"integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/resolve-uri": "^3.1.0",
@ -139,9 +156,12 @@
} }
}, },
"node_modules/@rollup/plugin-node-resolve": { "node_modules/@rollup/plugin-node-resolve": {
"version": "15.2.1", "version": "15.2.3",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz",
"integrity": "sha512-nsbUg588+GDSu8/NS8T4UAshO6xeaOfINNuXeVHcKV02LJtoRaM1SiOacClw4kws1SFiNhdLGxlbMY9ga/zs/w==", "integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==",
"version": "15.2.3",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz",
"integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@rollup/pluginutils": "^5.0.1", "@rollup/pluginutils": "^5.0.1",
@ -155,7 +175,8 @@
"node": ">=14.0.0" "node": ">=14.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"rollup": "^2.78.0||^3.0.0" "rollup": "^2.78.0||^3.0.0||^4.0.0"
"rollup": "^2.78.0||^3.0.0||^4.0.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"rollup": { "rollup": {
@ -164,18 +185,23 @@
} }
}, },
"node_modules/@rollup/plugin-replace": { "node_modules/@rollup/plugin-replace": {
"version": "5.0.2", "version": "5.0.5",
"resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.2.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.5.tgz",
"integrity": "sha512-M9YXNekv/C/iHHK+cvORzfRYfPbq0RDD8r0G+bMiTXjNGKulPnCT9O3Ss46WfhI6ZOCgApOP7xAdmCQJ+U2LAA==", "integrity": "sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.5.tgz",
"integrity": "sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==",
"dependencies": { "dependencies": {
"@rollup/pluginutils": "^5.0.1", "@rollup/pluginutils": "^5.0.1",
"magic-string": "^0.27.0" "magic-string": "^0.30.3"
"magic-string": "^0.30.3"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0" "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"rollup": { "rollup": {
@ -183,10 +209,35 @@
} }
} }
}, },
"node_modules/@rollup/plugin-replace/node_modules/magic-string": {
"version": "0.30.5",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
"integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@rollup/plugin-replace/node_modules/magic-string": {
"version": "0.30.5",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
"integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@rollup/plugin-terser": { "node_modules/@rollup/plugin-terser": {
"version": "0.4.3", "version": "0.4.4",
"resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz",
"integrity": "sha512-EF0oejTMtkyhrkwCdg0HJ0IpkcaVg1MMSf2olHb2Jp+1mnLM04OhjpJWGma4HobiDTF0WCyViWuvadyE9ch2XA==", "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==",
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz",
"integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"serialize-javascript": "^6.0.1", "serialize-javascript": "^6.0.1",
@ -197,7 +248,8 @@
"node": ">=14.0.0" "node": ">=14.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"rollup": "^2.x || ^3.x" "rollup": "^2.0.0||^3.0.0||^4.0.0"
"rollup": "^2.0.0||^3.0.0||^4.0.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"rollup": { "rollup": {
@ -206,9 +258,9 @@
} }
}, },
"node_modules/@rollup/pluginutils": { "node_modules/@rollup/pluginutils": {
"version": "5.0.3", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz",
"integrity": "sha512-hfllNN4a80rwNQ9QCxhxuHCGHMAvabXqxNdaChUSSadMre7t4iEUI6fFAhBOn/eIYTgYVhBv7vCLsAJ4u3lf3g==", "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==",
"dependencies": { "dependencies": {
"@types/estree": "^1.0.0", "@types/estree": "^1.0.0",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
@ -218,7 +270,8 @@
"node": ">=14.0.0" "node": ">=14.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0" "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"rollup": { "rollup": {
@ -227,15 +280,18 @@
} }
}, },
"node_modules/@timohausmann/quadtree-js": { "node_modules/@timohausmann/quadtree-js": {
"version": "1.2.5", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/@timohausmann/quadtree-js/-/quadtree-js-1.2.5.tgz", "resolved": "https://registry.npmjs.org/@timohausmann/quadtree-js/-/quadtree-js-1.2.6.tgz",
"integrity": "sha512-WcH3pouYtpyLjTCRvNP0WuSV4m7mRyYhLzW44egveFryT7pJhpDsdIJASEe37iCFNA0vmEpqTYGoG0siyXEthA==", "integrity": "sha512-EoAoLMFV2JfSG8+8XD9xWJQdyvfEB5xNpiQWGD7rTDSbDQQV8IVpkm0uOIxwJZ+1uC9hHKri9GmJ5wBSUO4jfg==",
"dev": true "dev": true
}, },
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.1", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==" "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
}, },
"node_modules/@types/resolve": { "node_modules/@types/resolve": {
"version": "1.20.2", "version": "1.20.2",
@ -244,9 +300,9 @@
"dev": true "dev": true
}, },
"node_modules/@urql/core": { "node_modules/@urql/core": {
"version": "4.1.1", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/@urql/core/-/core-4.1.1.tgz", "resolved": "https://registry.npmjs.org/@urql/core/-/core-4.2.0.tgz",
"integrity": "sha512-iIoAy6BY+BUZZ7KIpnMT7C9q+ULf5ZCVxGe3/i7WZSJBrQa2h1QkIMhL+8fAKmOn9gt83jSIv5drWWnhZ9izEA==", "integrity": "sha512-GRkZ4kECR9UohWAjiSk2UYUetco6/PqSrvyC4AH6g16tyqEShA63M232cfbE1J9XJPaGNjia14Gi+oOqzp144w==",
"dependencies": { "dependencies": {
"@0no-co/graphql.web": "^1.0.1", "@0no-co/graphql.web": "^1.0.1",
"wonka": "^6.3.2" "wonka": "^6.3.2"
@ -265,9 +321,12 @@
} }
}, },
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.10.0", "version": "8.11.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
"integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
"version": "8.11.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
"integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
"dev": true, "dev": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
@ -310,9 +369,9 @@
} }
}, },
"node_modules/chart.js": { "node_modules/chart.js": {
"version": "4.3.3", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.3.3.tgz", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.1.tgz",
"integrity": "sha512-aTk7pBw+x6sQYhon/NR3ikfUJuym/LdgpTlgZRe2PaEhjUMKBKyNaFCMVRAyTEWYFNO7qRu7iQVqOw/OqzxZxQ==", "integrity": "sha512-C74QN1bxwV1v2PEujhmKjOZ7iUM4w6BWs23Md/6aOZZSlwMzeCIDGuZay++rBgChYru7/+QFeoQW0fQoP534Dg==",
"dependencies": { "dependencies": {
"@kurkle/color": "^0.3.0" "@kurkle/color": "^0.3.0"
}, },
@ -332,6 +391,38 @@
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
"dev": true "dev": true
}, },
"node_modules/complex.js": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.1.1.tgz",
"integrity": "sha512-8njCHOTtFFLtegk6zQo0kkVX1rngygb/KQI6z1qZxlFI3scluC+LVTCFbrkWjBv4vvLlbQ9t88IPMC6k95VTTg==",
"engines": {
"node": "*"
},
"funding": {
"type": "patreon",
"url": "https://www.patreon.com/infusion"
}
},
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"dependencies": {
"@babel/runtime": "^7.21.0"
},
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/decimal.js": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz",
"integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA=="
},
"node_modules/deepmerge": { "node_modules/deepmerge": {
"version": "4.3.1", "version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
@ -341,11 +432,28 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/escape-latex": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz",
"integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw=="
},
"node_modules/estree-walker": { "node_modules/estree-walker": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
}, },
"node_modules/fraction.js": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.4.tgz",
"integrity": "sha512-pwiTgt0Q7t+GHZA4yaLjObx4vXmmdcS0iSJ19o8d/goUGgItX9UZWKWNnLHehxviD8wU2IWRsnR8cD5+yOJP2Q==",
"engines": {
"node": "*"
},
"funding": {
"type": "patreon",
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/fs.realpath": { "node_modules/fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -367,10 +475,20 @@
} }
}, },
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.1", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true "dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
}, },
"node_modules/glob": { "node_modules/glob": {
"version": "8.1.0", "version": "8.1.0",
@ -392,23 +510,32 @@
} }
}, },
"node_modules/graphql": { "node_modules/graphql": {
"version": "16.8.0", "version": "16.8.1",
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.0.tgz", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz",
"integrity": "sha512-0oKGaR+y3qcS5mCu1vb7KG+a89vjn06C7Ihq/dDl3jA+A8B3TKomvi3CiEcVLJQGalbu8F52LxkOym7U5sSfbg==", "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==",
"version": "16.8.1",
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz",
"integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==",
"engines": { "engines": {
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
} }
}, },
"node_modules/has": { "node_modules/hasown": {
"version": "1.0.3", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
"node_modules/hasown": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
"integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"function-bind": "^1.1.1" "function-bind": "^1.1.2"
"function-bind": "^1.1.2"
}, },
"engines": { "engines": {
"node": ">= 0.4.0" "node": ">= 0.4"
"node": ">= 0.4"
} }
}, },
"node_modules/inflight": { "node_modules/inflight": {
@ -443,12 +570,16 @@
} }
}, },
"node_modules/is-core-module": { "node_modules/is-core-module": {
"version": "2.13.0", "version": "2.13.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
"integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
"integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"has": "^1.0.3" "hasown": "^2.0.0"
"hasown": "^2.0.0"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
@ -469,10 +600,17 @@
"@types/estree": "*" "@types/estree": "*"
} }
}, },
"node_modules/javascript-natural-sort": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
"integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw=="
},
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.27.0", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz",
"integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==",
"dev": true,
"dev": true,
"dependencies": { "dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.13" "@jridgewell/sourcemap-codec": "^1.4.13"
}, },
@ -480,6 +618,28 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/mathjs": {
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/mathjs/-/mathjs-12.0.0.tgz",
"integrity": "sha512-Oz3swPplNPe7taoP6WrkKhQzhDE2SwvOgLzu8H3EN+hEadw2GjEJUm6Xl+hrioHoB8g2BYb3gfw1glSzhdBKYw==",
"dependencies": {
"@babel/runtime": "^7.23.2",
"complex.js": "^2.1.1",
"decimal.js": "^10.4.3",
"escape-latex": "^1.2.0",
"fraction.js": "4.3.4",
"javascript-natural-sort": "^0.7.1",
"seedrandom": "^3.0.5",
"tiny-emitter": "^2.1.0",
"typed-function": "^4.1.1"
},
"bin": {
"mathjs": "bin/cli.js"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "5.1.6", "version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
@ -527,10 +687,23 @@
"safe-buffer": "^5.1.0" "safe-buffer": "^5.1.0"
} }
}, },
"node_modules/regenerator-runtime": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
},
"node_modules/regenerator-runtime": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
},
"node_modules/resolve": { "node_modules/resolve": {
"version": "1.22.4", "version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
"integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"is-core-module": "^2.13.0", "is-core-module": "^2.13.0",
@ -554,9 +727,12 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "3.28.1", "version": "3.29.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.28.1.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
"integrity": "sha512-R9OMQmIHJm9znrU3m3cpE8uhN0fGdXiawME7aZIpQqvpS/85+Vt1Hq1/yVIcYfOmaQiHjvXkQAoJukvLpau6Yw==", "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
"version": "3.29.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
"integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
"devOptional": true, "devOptional": true,
"bin": { "bin": {
"rollup": "dist/bin/rollup" "rollup": "dist/bin/rollup"
@ -570,9 +746,12 @@
} }
}, },
"node_modules/rollup-plugin-css-only": { "node_modules/rollup-plugin-css-only": {
"version": "4.3.0", "version": "4.5.2",
"resolved": "https://registry.npmjs.org/rollup-plugin-css-only/-/rollup-plugin-css-only-4.3.0.tgz", "resolved": "https://registry.npmjs.org/rollup-plugin-css-only/-/rollup-plugin-css-only-4.5.2.tgz",
"integrity": "sha512-BsiCqJJQzZh2lQiHY5irejRoJ3I1EUFHEi5PjVqsr+EmOh54YrWVwd3YZEXnQJ2+fzlhif0YM/Kf0GuH90GAdQ==", "integrity": "sha512-7rj9+jB17Pz8LNcPgtMUb16JcgD8lxQMK9HcGfAVhMK3na/WXes3oGIo5QsrQQVqtgAU6q6KnQNXJrYunaUIQQ==",
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/rollup-plugin-css-only/-/rollup-plugin-css-only-4.5.2.tgz",
"integrity": "sha512-7rj9+jB17Pz8LNcPgtMUb16JcgD8lxQMK9HcGfAVhMK3na/WXes3oGIo5QsrQQVqtgAU6q6KnQNXJrYunaUIQQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@rollup/pluginutils": "5" "@rollup/pluginutils": "5"
@ -581,7 +760,8 @@
"node": ">=14" "node": ">=14"
}, },
"peerDependencies": { "peerDependencies": {
"rollup": "<4" "rollup": "<5"
"rollup": "<5"
} }
}, },
"node_modules/rollup-plugin-svelte": { "node_modules/rollup-plugin-svelte": {
@ -634,6 +814,11 @@
} }
] ]
}, },
"node_modules/seedrandom": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
"integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg=="
},
"node_modules/serialize-javascript": { "node_modules/serialize-javascript": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz",
@ -644,9 +829,12 @@
} }
}, },
"node_modules/smob": { "node_modules/smob": {
"version": "1.4.0", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/smob/-/smob-1.4.0.tgz", "resolved": "https://registry.npmjs.org/smob/-/smob-1.4.1.tgz",
"integrity": "sha512-MqR3fVulhjWuRNSMydnTlweu38UhQ0HXM4buStD/S3mc/BzX3CuM9OmhyQpmtYCvoYdl5ris6TI0ZqH355Ymqg==", "integrity": "sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==",
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/smob/-/smob-1.4.1.tgz",
"integrity": "sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==",
"dev": true "dev": true
}, },
"node_modules/source-map": { "node_modules/source-map": {
@ -698,9 +886,12 @@
} }
}, },
"node_modules/sveltestrap": { "node_modules/sveltestrap": {
"version": "5.11.1", "version": "5.11.2",
"resolved": "https://registry.npmjs.org/sveltestrap/-/sveltestrap-5.11.1.tgz", "resolved": "https://registry.npmjs.org/sveltestrap/-/sveltestrap-5.11.2.tgz",
"integrity": "sha512-FIvPIEU1VolqMN1wi2XrC8aehWVbIJEST7zPfPbOUUfPimyx9giN4nA3We5wkXrBUaifXA8CSIwuHFvf3CmYQw==", "integrity": "sha512-fkLqIUh2QHBoom7v6kHI85grLeOqplmvtnTiA5Ck2gchzpVmwXWaWpf8qWhCFxfDuMhJBPlWbJvtSmwpDEowrg==",
"version": "5.11.2",
"resolved": "https://registry.npmjs.org/sveltestrap/-/sveltestrap-5.11.2.tgz",
"integrity": "sha512-fkLqIUh2QHBoom7v6kHI85grLeOqplmvtnTiA5Ck2gchzpVmwXWaWpf8qWhCFxfDuMhJBPlWbJvtSmwpDEowrg==",
"dependencies": { "dependencies": {
"@popperjs/core": "^2.11.8" "@popperjs/core": "^2.11.8"
}, },
@ -709,9 +900,9 @@
} }
}, },
"node_modules/terser": { "node_modules/terser": {
"version": "5.19.2", "version": "5.25.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.19.2.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-5.25.0.tgz",
"integrity": "sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA==", "integrity": "sha512-we0I9SIsfvNUMP77zC9HG+MylwYYsGFSBG8qm+13oud2Yh+O104y614FRbyjpxys16jZwot72Fpi827YvGzuqg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@jridgewell/source-map": "^0.3.3", "@jridgewell/source-map": "^0.3.3",
@ -726,10 +917,26 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/tiny-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
},
"node_modules/typed-function": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.1.1.tgz",
"integrity": "sha512-Pq1DVubcvibmm8bYcMowjVnnMwPVMeh0DIdA8ad8NZY2sJgapANJmiigSUwlt+EgXxpfIv8MWrQXTIzkfYZLYQ==",
"engines": {
"node": ">= 14"
}
},
"node_modules/uplot": { "node_modules/uplot": {
"version": "1.6.25", "version": "1.6.27",
"resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.25.tgz", "resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.27.tgz",
"integrity": "sha512-eWLAhEaGtIcVBiS67mC2UC0yV+G6eYLS2rU67N4F2JVWjt7uBMg4xKXUYGW0dEz9G+m7fNatjCVXHts4gjyuMQ==" "integrity": "sha512-78U4ss5YeU65kQkOC/QAKiyII+4uo+TYUJJKvuxRzeSpk/s5sjpY1TL0agkmhHBBShpvLtmbHIEiM7+C5lBULg=="
"version": "1.6.27",
"resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.27.tgz",
"integrity": "sha512-78U4ss5YeU65kQkOC/QAKiyII+4uo+TYUJJKvuxRzeSpk/s5sjpY1TL0agkmhHBBShpvLtmbHIEiM7+C5lBULg=="
}, },
"node_modules/wonka": { "node_modules/wonka": {
"version": "6.3.4", "version": "6.3.4",

View File

@ -20,7 +20,9 @@
"@rollup/plugin-replace": "^5.0.2", "@rollup/plugin-replace": "^5.0.2",
"@urql/svelte": "^4.0.1", "@urql/svelte": "^4.0.1",
"chart.js": "^4.3.3", "chart.js": "^4.3.3",
"date-fns": "^2.30.0",
"graphql": "^16.6.0", "graphql": "^16.6.0",
"mathjs": "^12.0.0",
"svelte-chartjs": "^3.1.2", "svelte-chartjs": "^3.1.2",
"sveltestrap": "^5.11.1", "sveltestrap": "^5.11.1",
"uplot": "^1.6.24", "uplot": "^1.6.24",

View File

@ -389,9 +389,10 @@
<Histogram <Histogram
data={convert2uplot(item.bins)} data={convert2uplot(item.bins)}
width={width} height={250} width={width} height={250}
usesBins={true}
title="Average Distribution of '{item.metric}'" title="Average Distribution of '{item.metric}'"
xlabel={`${item.metric} bin maximum [${(metricConfig(cluster.name, item.metric)?.unit?.prefix ? metricConfig(cluster.name, item.metric)?.unit?.prefix : '') + xlabel={`${item.metric} bin maximum ${(metricConfig(cluster.name, item.metric)?.unit?.prefix ? '[' + metricConfig(cluster.name, item.metric)?.unit?.prefix : '') +
(metricConfig(cluster.name, item.metric)?.unit?.base ? metricConfig(cluster.name, item.metric)?.unit?.base : '')}]`} (metricConfig(cluster.name, item.metric)?.unit?.base ? metricConfig(cluster.name, item.metric)?.unit?.base + ']' : '')}`}
xunit={`${(metricConfig(cluster.name, item.metric)?.unit?.prefix ? metricConfig(cluster.name, item.metric)?.unit?.prefix : '') + xunit={`${(metricConfig(cluster.name, item.metric)?.unit?.prefix ? metricConfig(cluster.name, item.metric)?.unit?.prefix : '') +
(metricConfig(cluster.name, item.metric)?.unit?.base ? metricConfig(cluster.name, item.metric)?.unit?.base : '')}`} (metricConfig(cluster.name, item.metric)?.unit?.base ? metricConfig(cluster.name, item.metric)?.unit?.base : '')}`}
ylabel="Normalized Hours" ylabel="Normalized Hours"

View File

@ -0,0 +1,65 @@
<script>
import { Modal, ModalBody, ModalHeader, ModalFooter,
Button, ListGroup, ListGroupItem } from 'sveltestrap'
import { gql, getContextClient , mutationStore } from '@urql/svelte'
export let cluster
export let metricsInHistograms
export let isOpen
let availableMetrics = ['cpu_load', 'flops_any', 'mem_used', 'mem_bw'] // 'net_bw', 'file_bw'
let pendingMetrics = [...metricsInHistograms] // Copy
const client = getContextClient()
const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({
client: client,
query: gql`mutation($name: String!, $value: String!) {
updateConfiguration(name: $name, value: $value)
}`,
variables: { name, value }
})
}
function updateConfiguration(data) {
updateConfigurationMutation({
name: data.name,
value: JSON.stringify(data.value)
}).subscribe(res => {
if (res.fetching === false && res.error) {
throw res.error
// console.log('Error on subscription: ' + res.error)
}
})
}
function closeAndApply() {
metricsInHistograms = [...pendingMetrics] // Set for parent
isOpen = !isOpen
updateConfiguration({
name: cluster ? `user_view_histogramMetrics:${cluster}` : 'user_view_histogramMetrics',
value: metricsInHistograms
})
}
</script>
<Modal {isOpen}
toggle={() => (isOpen = !isOpen)}>
<ModalHeader>
Select metrics presented in histograms
</ModalHeader>
<ModalBody>
<ListGroup>
{#each availableMetrics as metric (metric)}
<ListGroupItem>
<input type="checkbox" bind:group={pendingMetrics} value={metric}>
{metric}
</ListGroupItem>
{/each}
</ListGroup>
</ModalBody>
<ModalFooter>
<Button color="primary" on:click={closeAndApply}> Close & Apply </Button>
<Button color="secondary" on:click={() => (isOpen = !isOpen)}> Close </Button>
</ModalFooter>
</Modal>

View File

@ -27,6 +27,7 @@
import TagManagement from "./TagManagement.svelte"; import TagManagement from "./TagManagement.svelte";
import MetricSelection from "./MetricSelection.svelte"; import MetricSelection from "./MetricSelection.svelte";
import StatsTable from "./StatsTable.svelte"; import StatsTable from "./StatsTable.svelte";
import JobFootprint from "./JobFootprint.svelte";
import { getContext } from "svelte"; import { getContext } from "svelte";
export let dbid; export let dbid;
@ -46,7 +47,8 @@
resources { hostname, hwthreads, accelerators }, resources { hostname, hwthreads, accelerators },
metaData, metaData,
userData { name, email }, userData { name, email },
concurrentJobs { items { id, jobId }, count, listQuery } concurrentJobs { items { id, jobId }, count, listQuery },
flopsAnyAvg, memBwAvg, loadAvg
} }
`); `);
@ -93,7 +95,9 @@
startFetching( startFetching(
job, job,
[...toFetch], [...toFetch],
job.numNodes > 2 ? ["node"] : ["node", "core"] job.numNodes > 2
? ["node"]
: ["node", "socket", "core"]
); );
} else { } else {
// Accels and not on node scope // Accels and not on node scope
@ -102,7 +106,7 @@
[...toFetch], [...toFetch],
job.numNodes > 2 job.numNodes > 2
? ["node", "accelerator"] ? ["node", "accelerator"]
: ["node", "accelerator", "core"] : ["node", "accelerator", "socket", "core"]
); );
} }
@ -132,7 +136,9 @@
let plots = {}, let plots = {},
jobTags, jobTags,
statsTable; statsTable,
jobFootprint;
$: document.title = $initq.fetching $: document.title = $initq.fetching
? "Loading..." ? "Loading..."
: $initq.error : $initq.error
@ -200,6 +206,17 @@
<Spinner secondary /> <Spinner secondary />
{/if} {/if}
</Col> </Col>
{#if $jobMetrics.data}
{#key $jobMetrics.data}
<Col>
<JobFootprint
bind:this={jobFootprint}
job={$initq.data.job}
jobMetrics={$jobMetrics.data.jobMetrics}
/>
</Col>
{/key}
{/if}
{#if $jobMetrics.data && $initq.data} {#if $jobMetrics.data && $initq.data}
{#if $initq.data.job.concurrentJobs != null && $initq.data.job.concurrentJobs.items.length != 0} {#if $initq.data.job.concurrentJobs != null && $initq.data.job.concurrentJobs.items.length != 0}
{#if authlevel > roles.manager} {#if authlevel > roles.manager}
@ -390,8 +407,6 @@
bind:this={statsTable} bind:this={statsTable}
job={$initq.data.job} job={$initq.data.job}
jobMetrics={$jobMetrics.data.jobMetrics} jobMetrics={$jobMetrics.data.jobMetrics}
accMetrics={accMetrics}
accNodeOnly={accNodeOnly}
/> />
{/key} {/key}
{/if} {/if}

View File

@ -0,0 +1,232 @@
<script>
import { getContext } from 'svelte'
import {
Card,
CardHeader,
CardTitle,
CardBody,
Progress,
Icon,
Tooltip
} from "sveltestrap";
import { mean, round } from 'mathjs'
export let job
export let jobMetrics
export let view = 'job'
export let width = 'auto'
const clusters = getContext('clusters')
const subclusterConfig = clusters.find((c) => c.name == job.cluster).subClusters.find((sc) => sc.name == job.subCluster)
const footprintMetrics = (job.numAcc !== 0)
? (job.exclusive !== 1)
? ['cpu_load', 'flops_any', 'acc_utilization']
: ['cpu_load', 'flops_any', 'acc_utilization', 'mem_bw']
: (job.exclusive !== 1)
? ['cpu_load', 'flops_any', 'mem_used']
: ['cpu_load', 'flops_any', 'mem_used', 'mem_bw']
const footprintData = footprintMetrics.map((fm) => {
// Mean: Primarily use backend sourced avgs from job.*, secondarily calculate/read from metricdata
let mv = null
if (fm === 'cpu_load' && job.loadAvg !== 0) {
mv = round(job.loadAvg, 2)
} else if (fm === 'flops_any' && job.flopsAnyAvg !== 0) {
mv = round(job.flopsAnyAvg, 2)
} else if (fm === 'mem_bw' && job.memBwAvg !== 0) {
mv = round(job.memBwAvg, 2)
} else { // Calculate from jobMetrics
const jm = jobMetrics.find((jm) => jm.name === fm && jm.scope === 'node')
if (jm?.metric?.statisticsSeries) {
mv = round(mean(jm.metric.statisticsSeries.mean), 2)
} else if (jm?.metric?.series?.length > 1) {
const avgs = jm.metric.series.map(jms => jms.statistics.avg)
mv = round(mean(avgs), 2)
} else if (jm?.metric?.series) {
mv = round(jm.metric.series[0].statistics.avg, 2)
} else {
mv = 0.0
}
}
// Unit
const fmc = getContext('metrics')(job.cluster, fm)
let unit = ''
if (fmc?.unit?.base) unit = fmc.unit.prefix + fmc.unit.base
// Threshold / -Differences
const fmt = findJobThresholds(job, fmc, subclusterConfig)
if (fm === 'flops_any') fmt.peak = round((fmt.peak * 0.85), 0)
// Define basic data
const fmBase = {
name: fm,
unit: unit,
avg: mv,
max: fmt.peak
}
if (evalFootprint(fm, mv, fmt, 'alert')) {
return {
...fmBase,
color: 'danger',
message:`Metric average way ${fm === 'mem_used' ? 'above' : 'below' } expected normal thresholds.`,
impact: 3
}
} else if (evalFootprint(fm, mv, fmt, 'caution')) {
return {
...fmBase,
color: 'warning',
message: `Metric average ${fm === 'mem_used' ? 'above' : 'below' } expected normal thresholds.`,
impact: 2
}
} else if (evalFootprint(fm, mv, fmt, 'normal')) {
return {
...fmBase,
color: 'success',
message: 'Metric average within expected thresholds.',
impact: 1
}
} else if (evalFootprint(fm, mv, fmt, 'peak')) {
return {
...fmBase,
color: 'info',
message: 'Metric average above expected normal thresholds: Check for artifacts recommended.',
impact: 0
}
} else {
return {
...fmBase,
color: 'secondary',
message: 'Metric average above expected peak threshold: Check for artifacts!',
impact: -1
}
}
})
function evalFootprint(metric, mean, thresholds, level) {
// mem_used has inverse logic regarding threshold levels
switch (level) {
case 'peak':
if (metric === 'mem_used') return (mean <= thresholds.peak && mean > thresholds.alert)
else return (mean <= thresholds.peak && mean > thresholds.normal)
case 'alert':
if (metric === 'mem_used') return (mean <= thresholds.alert && mean > thresholds.caution)
else return (mean <= thresholds.alert && mean >= 0)
case 'caution':
if (metric === 'mem_used') return (mean <= thresholds.caution && mean > thresholds.normal)
else return (mean <= thresholds.caution && mean > thresholds.alert)
case 'normal':
if (metric === 'mem_used') return (mean <= thresholds.normal && mean >= 0)
else return (mean <= thresholds.normal && mean > thresholds.caution)
default:
return false
}
}
</script>
<script context="module">
export function findJobThresholds(job, metricConfig, subClusterConfig) {
if (!job || !metricConfig || !subClusterConfig) {
console.warn('Argument missing for findJobThresholds!')
return null
}
const subclusterThresholds = metricConfig.subClusters.find(sc => sc.name == subClusterConfig.name)
const defaultThresholds = {
peak: subclusterThresholds ? subclusterThresholds.peak : metricConfig.peak,
normal: subclusterThresholds ? subclusterThresholds.normal : metricConfig.normal,
caution: subclusterThresholds ? subclusterThresholds.caution : metricConfig.caution,
alert: subclusterThresholds ? subclusterThresholds.alert : metricConfig.alert
}
if (job.exclusive === 1) { // Exclusive: Use as defined
return defaultThresholds
} else { // Shared: Handle specifically
if (metricConfig.name === 'cpu_load') { // Special: Avg Aggregation BUT scaled based on #hwthreads
return {
peak: job.numHWThreads,
normal: job.numHWThreads,
caution: defaultThresholds.caution,
alert: defaultThresholds.alert
}
} else if (metricConfig.aggregation === 'avg' ){
return defaultThresholds
} else if (metricConfig.aggregation === 'sum' ){
const jobFraction = job.numHWThreads / subClusterConfig.topology.node.length
return {
peak: round((defaultThresholds.peak * jobFraction), 0),
normal: round((defaultThresholds.normal * jobFraction), 0),
caution: round((defaultThresholds.caution * jobFraction), 0),
alert: round((defaultThresholds.alert * jobFraction), 0)
}
} else {
console.warn('Missing or unkown aggregation mode (sum/avg) for metric:', metricConfig)
return null
}
} // Other job.exclusive cases?
}
</script>
<Card class="h-auto mt-1" style="width: {width}px;">
{#if view === 'job'}
<CardHeader>
<CardTitle class="mb-0 d-flex justify-content-center">
Core Metrics Footprint
</CardTitle>
</CardHeader>
{/if}
<CardBody>
{#each footprintData as fpd, index}
<div class="mb-1 d-flex justify-content-between">
<div>&nbsp;<b>{fpd.name}</b></div> <!-- For symmetry, see below ...-->
<div class="cursor-help d-inline-flex" id={`footprint-${job.jobId}-${index}`}>
<div class="mx-1">
<!-- Alerts Only -->
{#if fpd.impact === 3 || fpd.impact === -1}
<Icon name="exclamation-triangle-fill" class="text-danger"/>
{:else if fpd.impact === 2}
<Icon name="exclamation-triangle" class="text-warning"/>
{/if}
<!-- Emoji for all states-->
{#if fpd.impact === 3}
<Icon name="emoji-frown" class="text-danger"/>
{:else if fpd.impact === 2}
<Icon name="emoji-neutral" class="text-warning"/>
{:else if fpd.impact === 1}
<Icon name="emoji-smile" class="text-success"/>
{:else if fpd.impact === 0}
<Icon name="emoji-laughing" class="text-info"/>
{:else if fpd.impact === -1}
<Icon name="emoji-dizzy" class="text-danger"/>
{/if}
</div>
<div>
<!-- Print Values -->
{fpd.avg} / {fpd.max} {fpd.unit} &nbsp; <!-- To increase margin to tooltip: No other way manageable ... -->
</div>
</div>
<Tooltip target={`footprint-${job.jobId}-${index}`} placement="right" offset={[0, 20]}>{fpd.message}</Tooltip>
</div>
<div class="mb-2">
<Progress
value={fpd.avg}
max={fpd.max}
color={fpd.color}
/>
</div>
{/each}
{#if job?.metaData?.message}
<hr class="mt-1 mb-2"/>
{@html job.metaData.message}
{/if}
</CardBody>
</Card>
<style>
.cursor-help {
cursor: help;
}
</style>

View File

@ -23,6 +23,9 @@
let metrics = filterPresets.cluster let metrics = filterPresets.cluster
? ccconfig[`plot_list_selectedMetrics:${filterPresets.cluster}`] || ccconfig.plot_list_selectedMetrics ? ccconfig[`plot_list_selectedMetrics:${filterPresets.cluster}`] || ccconfig.plot_list_selectedMetrics
: ccconfig.plot_list_selectedMetrics : ccconfig.plot_list_selectedMetrics
let showFootprint = filterPresets.cluster
? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
: !!ccconfig.plot_list_showFootprint
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null
// The filterPresets are handled by the Filters component, // The filterPresets are handled by the Filters component,
@ -81,7 +84,8 @@
bind:metrics={metrics} bind:metrics={metrics}
bind:sorting={sorting} bind:sorting={sorting}
bind:matchedJobs={matchedJobs} bind:matchedJobs={matchedJobs}
bind:this={jobList} /> bind:this={jobList}
bind:showFootprint={showFootprint} />
</Col> </Col>
</Row> </Row>
@ -93,4 +97,6 @@
bind:cluster={selectedCluster} bind:cluster={selectedCluster}
configName="plot_list_selectedMetrics" configName="plot_list_selectedMetrics"
bind:metrics={metrics} bind:metrics={metrics}
bind:isOpen={isMetricsSelectionOpen} /> bind:isOpen={isMetricsSelectionOpen}
bind:showFootprint={showFootprint}
view='list'/>

View File

@ -17,12 +17,15 @@
export let configName export let configName
export let allMetrics = null export let allMetrics = null
export let cluster = null export let cluster = null
export let showFootprint
export let view = 'job'
const clusters = getContext('clusters'), const clusters = getContext('clusters'),
onInit = getContext('on-init') onInit = getContext('on-init')
let newMetricsOrder = [] let newMetricsOrder = []
let unorderedMetrics = [...metrics] let unorderedMetrics = [...metrics]
let pendingShowFootprint = !!showFootprint
onInit(() => { onInit(() => {
if (allMetrics == null) allMetrics = new Set() if (allMetrics == null) allMetrics = new Set()
@ -90,6 +93,8 @@
metrics = newMetricsOrder.filter(m => unorderedMetrics.includes(m)) metrics = newMetricsOrder.filter(m => unorderedMetrics.includes(m))
isOpen = false isOpen = false
showFootprint = !!pendingShowFootprint
updateConfigurationMutation({ updateConfigurationMutation({
name: cluster == null ? configName : `${configName}:${cluster}`, name: cluster == null ? configName : `${configName}:${cluster}`,
value: JSON.stringify(metrics) value: JSON.stringify(metrics)
@ -99,6 +104,16 @@
// console.log('Error on subscription: ' + res.error) // console.log('Error on subscription: ' + res.error)
} }
}) })
updateConfigurationMutation({
name: cluster == null ? 'plot_list_showFootprint' : `plot_list_showFootprint:${cluster}`,
value: JSON.stringify(showFootprint)
}).subscribe(res => {
if (res.fetching === false && res.error) {
console.log('Error on footprint subscription: ' + res.error)
throw res.error
}
})
} }
</script> </script>
@ -121,6 +136,12 @@
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
<ListGroup> <ListGroup>
{#if view === 'list'}
<li class="list-group-item">
<input type="checkbox" bind:checked={pendingShowFootprint}> Show Footprint
</li>
<hr/>
{/if}
{#each newMetricsOrder as metric, index (metric)} {#each newMetricsOrder as metric, index (metric)}
<li class="cc-config-column list-group-item" <li class="cc-config-column list-group-item"
draggable={true} ondragover="return false" draggable={true} ondragover="return false"

View File

@ -7,8 +7,6 @@
export let job export let job
export let jobMetrics export let jobMetrics
export let accMetrics
export let accNodeOnly
const allMetrics = [...new Set(jobMetrics.map(m => m.name))].sort(), const allMetrics = [...new Set(jobMetrics.map(m => m.name))].sort(),
scopesForMetric = (metric) => jobMetrics scopesForMetric = (metric) => jobMetrics
@ -23,17 +21,23 @@
|| getContext('cc-config')['job_view_nodestats_selectedMetrics'] || getContext('cc-config')['job_view_nodestats_selectedMetrics']
for (let metric of allMetrics) { for (let metric of allMetrics) {
// Not Exclusive or Single Node: Get maxScope() // Not Exclusive or Multi-Node: get maxScope directly (mostly: node)
// No Accelerators in Job and not Acc-Metric: Use 'core' // -> Else: Load smallest available granularity as default as per availability
// Accelerator Metric available on accelerator scope: Use 'accelerator' const availableScopes = scopesForMetric(metric)
// Accelerator Metric only on node scope: Fallback to 'node' if (job.exclusive != 1 || job.numNodes == 1) {
selectedScopes[metric] = (job.exclusive != 1 || job.numNodes == 1) ? if (availableScopes.includes('accelerator')) {
(job.numAccs != 0 && accMetrics.includes(metric)) ? selectedScopes[metric] = 'accelerator'
accNodeOnly ? } else if (availableScopes.includes('core')) {
'node' selectedScopes[metric] = 'core'
: 'accelerator' } else if (availableScopes.includes('socket')) {
: 'core' selectedScopes[metric] = 'socket'
: maxScope(scopesForMetric(metric)) } else {
selectedScopes[metric] = 'node'
}
} else {
selectedScopes[metric] = maxScope(availableScopes)
}
sorting[metric] = { sorting[metric] = {
min: { dir: 'up', active: false }, min: { dir: 'up', active: false },
avg: { dir: 'up', active: false }, avg: { dir: 'up', active: false },
@ -84,8 +88,7 @@
{metric} {metric}
</InputGroupText> </InputGroupText>
<select class="form-select" <select class="form-select"
bind:value={selectedScopes[metric]} bind:value={selectedScopes[metric]}>
disabled={scopesForMetric(metric, jobMetrics).length == 1}>
{#each scopesForMetric(metric, jobMetrics) as scope} {#each scopesForMetric(metric, jobMetrics) as scope}
<option value={scope}>{scope}</option> <option value={scope}>{scope}</option>
{/each} {/each}

View File

@ -15,6 +15,7 @@
Table, Table,
Progress, Progress,
Icon, Icon,
Button
} from "sveltestrap"; } from "sveltestrap";
import { init, convert2uplot, transformPerNodeDataForRoofline } from "./utils.js"; import { init, convert2uplot, transformPerNodeDataForRoofline } from "./utils.js";
import { scaleNumbers } from "./units.js"; import { scaleNumbers } from "./units.js";
@ -24,6 +25,8 @@
getContextClient, getContextClient,
mutationStore, mutationStore,
} from "@urql/svelte"; } from "@urql/svelte";
import PlotTable from './PlotTable.svelte'
import HistogramSelection from './HistogramSelection.svelte'
const { query: initq } = init(); const { query: initq } = init();
const ccconfig = getContext("cc-config"); const ccconfig = getContext("cc-config");
@ -63,6 +66,9 @@
option.key == ccconfig.status_view_selectedTopUserCategory option.key == ccconfig.status_view_selectedTopUserCategory
); );
let isHistogramSelectionOpen = false
$: metricsInHistograms = cluster ? (ccconfig[`user_view_histogramMetrics:${cluster}`] || []) : (ccconfig.user_view_histogramMetrics || [])
const client = getContextClient(); const client = getContextClient();
$: mainQuery = queryStore({ $: mainQuery = queryStore({
client: client, client: client,
@ -73,6 +79,7 @@
$metrics: [String!] $metrics: [String!]
$from: Time! $from: Time!
$to: Time! $to: Time!
$metricsInHistograms: [String!]
) { ) {
nodeMetrics( nodeMetrics(
cluster: $cluster cluster: $cluster
@ -98,7 +105,7 @@
} }
} }
stats: jobsStatistics(filter: $filter) { stats: jobsStatistics(filter: $filter, metrics: $metricsInHistograms) {
histDuration { histDuration {
count count
value value
@ -115,6 +122,16 @@
count count
value value
} }
histMetrics {
metric
unit
data {
min
max
count
bin
}
}
} }
allocatedNodes(cluster: $cluster) { allocatedNodes(cluster: $cluster) {
@ -129,6 +146,7 @@
from: from.toISOString(), from: from.toISOString(),
to: to.toISOString(), to: to.toISOString(),
filter: [{ state: ["running"] }, { cluster: { eq: cluster } }], filter: [{ state: ["running"] }, { cluster: { eq: cluster } }],
metricsInHistograms: metricsInHistograms
}, },
}); });
@ -311,7 +329,7 @@
<Col xs="auto" style="align-self: flex-end;"> <Col xs="auto" style="align-self: flex-end;">
<h4 class="mb-0">Current utilization of cluster "{cluster}"</h4> <h4 class="mb-0">Current utilization of cluster "{cluster}"</h4>
</Col> </Col>
<Col xs="auto"> <Col xs="auto" style="margin-left: 0.25rem;">
{#if $initq.fetching || $mainQuery.fetching} {#if $initq.fetching || $mainQuery.fetching}
<Spinner /> <Spinner />
{:else if $initq.error} {:else if $initq.error}
@ -321,6 +339,13 @@
{/if} {/if}
</Col> </Col>
<Col xs="auto" style="margin-left: auto;"> <Col xs="auto" style="margin-left: auto;">
<Button
outline color="secondary"
on:click={() => (isHistogramSelectionOpen = true)}>
<Icon name="bar-chart-line"/> Select Histograms
</Button>
</Col>
<Col xs="auto" style="margin-left: 0.25rem;">
<Refresher <Refresher
initially={120} initially={120}
on:reload={() => { on:reload={() => {
@ -666,4 +691,35 @@
{/key} {/key}
</Col> </Col>
</Row> </Row>
<hr class="my-2" />
{#if metricsInHistograms}
<Row>
<Col>
{#key $mainQuery.data.stats[0].histMetrics}
<PlotTable
let:item
let:width
renderFor="user"
items={$mainQuery.data.stats[0].histMetrics}
itemsPerRow={3}>
<Histogram
data={convert2uplot(item.data)}
usesBins={true}
width={width} height={250}
title="Distribution of '{item.metric}' averages"
xlabel={`${item.metric} bin maximum ${item?.unit ? `[${item.unit}]` : ``}`}
xunit={item.unit}
ylabel="Number of Jobs"
yunit="Jobs"/>
</PlotTable>
{/key}
</Col>
</Row>
{/if}
{/if} {/if}
<HistogramSelection
bind:cluster={cluster}
bind:metricsInHistograms={metricsInHistograms}
bind:isOpen={isHistogramSelectionOpen} />

View File

@ -1,7 +1,7 @@
<script> <script>
import { onMount, getContext } from 'svelte' import { onMount, getContext } from 'svelte'
import { init, convert2uplot } from './utils.js' import { init, convert2uplot } from './utils.js'
import { Table, Row, Col, Button, Icon, Card, Spinner, Input } from 'sveltestrap' import { Table, Row, Col, Button, Icon, Card, Spinner } from 'sveltestrap'
import { queryStore, gql, getContextClient } from '@urql/svelte' import { queryStore, gql, getContextClient } from '@urql/svelte'
import Filters from './filters/Filters.svelte' import Filters from './filters/Filters.svelte'
import JobList from './joblist/JobList.svelte' import JobList from './joblist/JobList.svelte'
@ -9,6 +9,8 @@
import Refresher from './joblist/Refresher.svelte' import Refresher from './joblist/Refresher.svelte'
import Histogram from './plots/Histogram.svelte' import Histogram from './plots/Histogram.svelte'
import MetricSelection from './MetricSelection.svelte' import MetricSelection from './MetricSelection.svelte'
import HistogramSelection from './HistogramSelection.svelte'
import PlotTable from './PlotTable.svelte'
import { scramble, scrambleNames } from './joblist/JobInfo.svelte' import { scramble, scrambleNames } from './joblist/JobInfo.svelte'
const { query: initq } = init() const { query: initq } = init()
@ -23,23 +25,29 @@
let jobFilters = []; let jobFilters = [];
let sorting = { field: 'startTime', order: 'DESC' }, isSortingOpen = false let sorting = { field: 'startTime', order: 'DESC' }, isSortingOpen = false
let metrics = ccconfig.plot_list_selectedMetrics, isMetricsSelectionOpen = false let metrics = ccconfig.plot_list_selectedMetrics, isMetricsSelectionOpen = false
let w1, w2, histogramHeight = 250 let w1, w2, histogramHeight = 250, isHistogramSelectionOpen = false
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null
let showFootprint = filterPresets.cluster
? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
: !!ccconfig.plot_list_showFootprint
$: metricsInHistograms = selectedCluster ? (ccconfig[`user_view_histogramMetrics:${selectedCluster}`] || []) : (ccconfig.user_view_histogramMetrics || [])
const client = getContextClient(); const client = getContextClient();
$: stats = queryStore({ $: stats = queryStore({
client: client, client: client,
query: gql` query: gql`
query($jobFilters: [JobFilter!]!) { query($jobFilters: [JobFilter!]!, $metricsInHistograms: [String!]) {
jobsStatistics(filter: $jobFilters) { jobsStatistics(filter: $jobFilters, metrics: $metricsInHistograms) {
totalJobs totalJobs
shortJobs shortJobs
totalWalltime totalWalltime
totalCoreHours totalCoreHours
histDuration { count, value } histDuration { count, value }
histNumNodes { count, value } histNumNodes { count, value }
histMetrics { metric, unit, data { min, max, count, bin } }
}}`, }}`,
variables: { jobFilters } variables: { jobFilters, metricsInHistograms }
}) })
onMount(() => filterComponent.update()) onMount(() => filterComponent.update())
@ -68,6 +76,12 @@
on:click={() => (isMetricsSelectionOpen = true)}> on:click={() => (isMetricsSelectionOpen = true)}>
<Icon name="graph-up"/> Metrics <Icon name="graph-up"/> Metrics
</Button> </Button>
<Button
outline color="secondary"
on:click={() => (isHistogramSelectionOpen = true)}>
<Icon name="bar-chart-line"/> Select Histograms
</Button>
</Col> </Col>
<Col xs="auto"> <Col xs="auto">
<Filters <Filters
@ -159,13 +173,49 @@
</div> </div>
{/if} {/if}
</Row> </Row>
{#if metricsInHistograms}
<Row>
{#if $stats.error}
<Col>
<Card body color="danger">{$stats.error.message}</Card>
</Col>
{:else if !$stats.data}
<Col>
<Spinner secondary />
</Col>
{:else}
<Col>
{#key $stats.data.jobsStatistics[0].histMetrics}
<PlotTable
let:item
let:width
renderFor="user"
items={$stats.data.jobsStatistics[0].histMetrics}
itemsPerRow={3}>
<Histogram
data={convert2uplot(item.data)}
usesBins={true}
width={width} height={250}
title="Distribution of '{item.metric}' averages"
xlabel={`${item.metric} bin maximum ${item?.unit ? `[${item.unit}]` : ``}`}
xunit={item.unit}
ylabel="Number of Jobs"
yunit="Jobs"/>
</PlotTable>
{/key}
</Col>
{/if}
</Row>
{/if}
<br/> <br/>
<Row> <Row>
<Col> <Col>
<JobList <JobList
bind:metrics={metrics} bind:metrics={metrics}
bind:sorting={sorting} bind:sorting={sorting}
bind:this={jobList} /> bind:this={jobList}
bind:showFootprint={showFootprint} />
</Col> </Col>
</Row> </Row>
@ -177,4 +227,11 @@
bind:cluster={selectedCluster} bind:cluster={selectedCluster}
configName="plot_list_selectedMetrics" configName="plot_list_selectedMetrics"
bind:metrics={metrics} bind:metrics={metrics}
bind:isOpen={isMetricsSelectionOpen} /> bind:isOpen={isMetricsSelectionOpen}
bind:showFootprint={showFootprint}
view='list'/>
<HistogramSelection
bind:cluster={selectedCluster}
bind:metricsInHistograms={metricsInHistograms}
bind:isOpen={isHistogramSelectionOpen} />

View File

@ -1,16 +1,21 @@
<script> <script>
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { Row, Col, Button, Modal, ModalBody, ModalHeader, ModalFooter, FormGroup } from 'sveltestrap' import { Row, Col, Button, Modal, ModalBody, ModalHeader, ModalFooter } from 'sveltestrap'
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export let isOpen = false export let isOpen = false
export let lessThan = null
export let moreThan = null
export let from = null export let from = null
export let to = null export let to = null
let pendingFrom, pendingTo let pendingLessThan, pendingMoreThan, pendingFrom, pendingTo
let lessDisabled = false, moreDisabled = false, betweenDisabled = false
function reset() { function reset() {
pendingLessThan = lessThan == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(lessThan)
pendingMoreThan = moreThan == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(moreThan)
pendingFrom = from == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(from) pendingFrom = from == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(from)
pendingTo = to == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(to) pendingTo = to == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(to)
} }
@ -27,18 +32,23 @@
function hoursAndMinsToSecs({ hours, mins }) { function hoursAndMinsToSecs({ hours, mins }) {
return hours * 3600 + mins * 60 return hours * 3600 + mins * 60
} }
$: lessDisabled = pendingMoreThan.hours !== 0 || pendingMoreThan.mins !== 0 || pendingFrom.hours !== 0 || pendingFrom.mins !== 0 || pendingTo.hours !== 0 || pendingTo.mins !== 0
$: moreDisabled = pendingLessThan.hours !== 0 || pendingLessThan.mins !== 0 || pendingFrom.hours !== 0 || pendingFrom.mins !== 0 || pendingTo.hours !== 0 || pendingTo.mins !== 0
$: betweenDisabled = pendingMoreThan.hours !== 0 || pendingMoreThan.mins !== 0 || pendingLessThan.hours !== 0 || pendingLessThan.mins !== 0
</script> </script>
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}> <Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader> <ModalHeader>
Select Start Time Select Job Duration
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
<h4>Between</h4> <h4>Duration more than</h4>
<Row> <Row>
<Col> <Col>
<div class="input-group mb-2 mr-sm-2"> <div class="input-group mb-2 mr-sm-2">
<input type="number" class="form-control" bind:value={pendingFrom.hours}> <input type="number" min="0" class="form-control" bind:value={pendingMoreThan.hours} disabled={moreDisabled}>
<div class="input-group-append"> <div class="input-group-append">
<div class="input-group-text">h</div> <div class="input-group-text">h</div>
</div> </div>
@ -46,7 +56,49 @@
</Col> </Col>
<Col> <Col>
<div class="input-group mb-2 mr-sm-2"> <div class="input-group mb-2 mr-sm-2">
<input type="number" class="form-control" bind:value={pendingFrom.mins}> <input type="number" min="0" max="59" class="form-control" bind:value={pendingMoreThan.mins} disabled={moreDisabled}>
<div class="input-group-append">
<div class="input-group-text">m</div>
</div>
</div>
</Col>
</Row>
<hr/>
<h4>Duration less than</h4>
<Row>
<Col>
<div class="input-group mb-2 mr-sm-2">
<input type="number" min="0" class="form-control" bind:value={pendingLessThan.hours} disabled={lessDisabled}>
<div class="input-group-append">
<div class="input-group-text">h</div>
</div>
</div>
</Col>
<Col>
<div class="input-group mb-2 mr-sm-2">
<input type="number" min="0" max="59" class="form-control" bind:value={pendingLessThan.mins} disabled={lessDisabled}>
<div class="input-group-append">
<div class="input-group-text">m</div>
</div>
</div>
</Col>
</Row>
<hr/>
<h4>Duration between</h4>
<Row>
<Col>
<div class="input-group mb-2 mr-sm-2">
<input type="number" min="0" class="form-control" bind:value={pendingFrom.hours} disabled={betweenDisabled}>
<div class="input-group-append">
<div class="input-group-text">h</div>
</div>
</div>
</Col>
<Col>
<div class="input-group mb-2 mr-sm-2">
<input type="number" min="0" max="59" class="form-control" bind:value={pendingFrom.mins} disabled={betweenDisabled}>
<div class="input-group-append"> <div class="input-group-append">
<div class="input-group-text">m</div> <div class="input-group-text">m</div>
</div> </div>
@ -57,7 +109,7 @@
<Row> <Row>
<Col> <Col>
<div class="input-group mb-2 mr-sm-2"> <div class="input-group mb-2 mr-sm-2">
<input type="number" class="form-control" bind:value={pendingTo.hours}> <input type="number" min="0" class="form-control" bind:value={pendingTo.hours} disabled={betweenDisabled}>
<div class="input-group-append"> <div class="input-group-append">
<div class="input-group-text">h</div> <div class="input-group-text">h</div>
</div> </div>
@ -65,7 +117,7 @@
</Col> </Col>
<Col> <Col>
<div class="input-group mb-2 mr-sm-2"> <div class="input-group mb-2 mr-sm-2">
<input type="number" class="form-control" bind:value={pendingTo.mins}> <input type="number" min="0" max="59" class="form-control" bind:value={pendingTo.mins} disabled={betweenDisabled}>
<div class="input-group-append"> <div class="input-group-append">
<div class="input-group-text">m</div> <div class="input-group-text">m</div>
</div> </div>
@ -77,19 +129,30 @@
<Button color="primary" <Button color="primary"
on:click={() => { on:click={() => {
isOpen = false isOpen = false
lessThan = hoursAndMinsToSecs(pendingLessThan)
moreThan = hoursAndMinsToSecs(pendingMoreThan)
from = hoursAndMinsToSecs(pendingFrom) from = hoursAndMinsToSecs(pendingFrom)
to = hoursAndMinsToSecs(pendingTo) to = hoursAndMinsToSecs(pendingTo)
dispatch('update', { from, to }) dispatch('update', { lessThan, moreThan, from, to })
}}> }}>
Close & Apply Close & Apply
</Button> </Button>
<Button color="danger" on:click={() => { <Button color="warning" on:click={() => {
isOpen = false lessThan = null
moreThan = null
from = null from = null
to = null to = null
reset() reset()
dispatch('update', { from, to }) }}>Reset Values</Button>
}}>Reset</Button> <Button color="danger" on:click={() => {
isOpen = false
lessThan = null
moreThan = null
from = null
to = null
reset()
dispatch('update', { lessThan, moreThan, from, to })
}}>Reset Filter</Button>
<Button on:click={() => (isOpen = false)}>Close</Button> <Button on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>

View File

@ -41,7 +41,7 @@
states: filterPresets.states || filterPresets.state ? [filterPresets.state].flat() : allJobStates, states: filterPresets.states || filterPresets.state ? [filterPresets.state].flat() : allJobStates,
startTime: filterPresets.startTime || { from: null, to: null }, startTime: filterPresets.startTime || { from: null, to: null },
tags: filterPresets.tags || [], tags: filterPresets.tags || [],
duration: filterPresets.duration || { from: null, to: null }, duration: filterPresets.duration || { lessThan: null, moreThan: null, from: null, to: null },
jobId: filterPresets.jobId || '', jobId: filterPresets.jobId || '',
arrayJobId: filterPresets.arrayJobId || null, arrayJobId: filterPresets.arrayJobId || null,
user: filterPresets.user || '', user: filterPresets.user || '',
@ -88,6 +88,10 @@
items.push({ tags: filters.tags }) items.push({ tags: filters.tags })
if (filters.duration.from || filters.duration.to) if (filters.duration.from || filters.duration.to)
items.push({ duration: { from: filters.duration.from, to: filters.duration.to } }) items.push({ duration: { from: filters.duration.from, to: filters.duration.to } })
if (filters.duration.lessThan)
items.push({ duration: { from: 0, to: filters.duration.lessThan } })
if (filters.duration.moreThan)
items.push({ duration: { from: filters.duration.moreThan, to: 604800 } }) // 7 days to include special jobs with long runtimes
if (filters.jobId) if (filters.jobId)
items.push({ jobId: { [filters.jobIdMatch]: filters.jobId } }) items.push({ jobId: { [filters.jobIdMatch]: filters.jobId } })
if (filters.arrayJobId != null) if (filters.arrayJobId != null)
@ -144,6 +148,10 @@
opts.push(`tag=${tag}`) opts.push(`tag=${tag}`)
if (filters.duration.from && filters.duration.to) if (filters.duration.from && filters.duration.to)
opts.push(`duration=${filters.duration.from}-${filters.duration.to}`) opts.push(`duration=${filters.duration.from}-${filters.duration.to}`)
if (filters.duration.lessThan)
opts.push(`duration=0-${filters.duration.lessThan}`)
if (filters.duration.moreThan)
opts.push(`duration=${filters.duration.moreThan}-604800`)
if (filters.numNodes.from && filters.numNodes.to) if (filters.numNodes.from && filters.numNodes.to)
opts.push(`numNodes=${filters.numNodes.from}-${filters.numNodes.to}`) opts.push(`numNodes=${filters.numNodes.from}-${filters.numNodes.to}`)
if (filters.numAccelerators.from && filters.numAccelerators.to) if (filters.numAccelerators.from && filters.numAccelerators.to)
@ -267,6 +275,18 @@
</Info> </Info>
{/if} {/if}
{#if filters.duration.lessThan}
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
Duration less than {Math.floor(filters.duration.lessThan / 3600)}h:{Math.floor(filters.duration.lessThan % 3600 / 60)}m
</Info>
{/if}
{#if filters.duration.moreThan}
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
Duration more than {Math.floor(filters.duration.moreThan / 3600)}h:{Math.floor(filters.duration.moreThan % 3600 / 60)}m
</Info>
{/if}
{#if filters.tags.length != 0} {#if filters.tags.length != 0}
<Info icon="tags" on:click={() => (isTagsOpen = true)}> <Info icon="tags" on:click={() => (isTagsOpen = true)}>
{#each filters.tags as tagId} {#each filters.tags as tagId}
@ -325,6 +345,8 @@
<Duration <Duration
bind:isOpen={isDurationOpen} bind:isOpen={isDurationOpen}
bind:lessThan={filters.duration.lessThan}
bind:moreThan={filters.duration.moreThan}
bind:from={filters.duration.from} bind:from={filters.duration.from}
bind:to={filters.duration.to} bind:to={filters.duration.to}
on:update={() => update()} /> on:update={() => update()} />

View File

@ -1,5 +1,6 @@
<script> <script>
import { createEventDispatcher, getContext } from 'svelte' import { createEventDispatcher } from 'svelte'
import { parse, format, sub } from 'date-fns'
import { Row, Button, Input, Modal, ModalBody, ModalHeader, ModalFooter, FormGroup } from 'sveltestrap' import { Row, Button, Input, Modal, ModalBody, ModalHeader, ModalFooter, FormGroup } from 'sveltestrap'
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -11,34 +12,29 @@
let pendingFrom, pendingTo let pendingFrom, pendingTo
const now = new Date(Date.now())
const ago = sub(now, {months: 1})
const defaultFrom = {date: format(ago, 'yyyy-MM-dd'), time: format(ago, 'HH:mm')}
const defaultTo = {date: format(now, 'yyyy-MM-dd'), time: format(now, 'HH:mm')}
function reset() { function reset() {
pendingFrom = from == null ? { date: '0000-00-00', time: '00:00' } : fromRFC3339(from) pendingFrom = from == null ? defaultFrom : fromRFC3339(from)
pendingTo = to == null ? { date: '0000-00-00', time: '00:00' } : fromRFC3339(to) pendingTo = to == null ? defaultTo : fromRFC3339(to)
} }
reset() reset()
function toRFC3339({ date, time }, secs = 0) { function toRFC3339({ date, time }, secs = '00') {
const dparts = date.split('-') const parsedDate = parse(date+' '+time+':'+secs, 'yyyy-MM-dd HH:mm:ss', new Date())
const tparts = time.split(':') return parsedDate.toISOString()
const d = new Date(
Number.parseInt(dparts[0]),
Number.parseInt(dparts[1]) - 1,
Number.parseInt(dparts[2]),
Number.parseInt(tparts[0]),
Number.parseInt(tparts[1]), secs)
return d.toISOString()
} }
function fromRFC3339(rfc3339) { function fromRFC3339(rfc3339) {
const d = new Date(rfc3339) const parsedDate = new Date(rfc3339)
const pad = (n) => n.toString().padStart(2, '0') return { date: format(parsedDate, 'yyyy-MM-dd'), time: format(parsedDate, 'HH:mm') }
const date = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
const time = `${pad(d.getHours())}:${pad(d.getMinutes())}`
return { date, time }
} }
$: isModified = (from != toRFC3339(pendingFrom) || to != toRFC3339(pendingTo, 59)) $: isModified = (from != toRFC3339(pendingFrom) || to != toRFC3339(pendingTo, '59'))
&& !(from == null && pendingFrom.date == '0000-00-00' && pendingFrom.time == '00:00') && !(from == null && pendingFrom.date == '0000-00-00' && pendingFrom.time == '00:00')
&& !(to == null && pendingTo.date == '0000-00-00' && pendingTo.time == '00:00') && !(to == null && pendingTo.date == '0000-00-00' && pendingTo.time == '00:00')
</script> </script>
@ -73,7 +69,7 @@
on:click={() => { on:click={() => {
isOpen = false isOpen = false
from = toRFC3339(pendingFrom) from = toRFC3339(pendingFrom)
to = toRFC3339(pendingTo, 59) to = toRFC3339(pendingTo, '59')
dispatch('update', { from, to }) dispatch('update', { from, to })
}}> }}>
Close & Apply Close & Apply

View File

@ -28,6 +28,17 @@
return `${hours}:${('0' + minutes).slice(-2)}:${('0' + seconds).slice(-2)}`; return `${hours}:${('0' + minutes).slice(-2)}:${('0' + seconds).slice(-2)}`;
} }
function getStateColor(state) {
switch (state) {
case 'running':
return 'success'
case 'completed':
return 'primary'
default:
return 'danger'
}
}
</script> </script>
<div> <div>
@ -86,12 +97,7 @@
<p> <p>
Start: <span class="fw-bold">{(new Date(job.startTime)).toLocaleString()}</span> Start: <span class="fw-bold">{(new Date(job.startTime)).toLocaleString()}</span>
<br/> <br/>
Duration: <span class="fw-bold">{formatDuration(job.duration)}</span> Duration: <span class="fw-bold">{formatDuration(job.duration)}</span> <Badge color="{getStateColor(job.state)}">{job.state}</Badge>
{#if job.state == 'running'}
<Badge color="success">running</Badge>
{:else if job.state != 'completed'}
<Badge color="danger">{job.state}</Badge>
{/if}
{#if job.walltime} {#if job.walltime}
<br/> <br/>
Walltime: <span class="fw-bold">{formatDuration(job.walltime)}</span> Walltime: <span class="fw-bold">{formatDuration(job.walltime)}</span>

View File

@ -28,6 +28,7 @@
export let sorting = { field: "startTime", order: "DESC" }; export let sorting = { field: "startTime", order: "DESC" };
export let matchedJobs = 0; export let matchedJobs = 0;
export let metrics = ccconfig.plot_list_selectedMetrics; export let metrics = ccconfig.plot_list_selectedMetrics;
export let showFootprint;
let itemsPerPage = ccconfig.plot_list_jobsPerPage; let itemsPerPage = ccconfig.plot_list_jobsPerPage;
let page = 1; let page = 1;
@ -73,6 +74,9 @@
name name
} }
metaData metaData
flopsAnyAvg
memBwAvg
loadAvg
} }
count count
} }
@ -89,7 +93,7 @@
// Force refresh list with existing unchanged variables (== usually would not trigger reactivity) // Force refresh list with existing unchanged variables (== usually would not trigger reactivity)
export function refresh() { export function refresh() {
queryStore({ jobs = queryStore({
client: client, client: client,
query: query, query: query,
variables: { paging, sorting, filter }, variables: { paging, sorting, filter },
@ -134,12 +138,19 @@
}) })
}; };
let plotWidth = null;
let tableWidth = null; let tableWidth = null;
let jobInfoColumnWidth = 250; let jobInfoColumnWidth = 250;
$: plotWidth = Math.floor( $: if (showFootprint) {
plotWidth = Math.floor(
(tableWidth - jobInfoColumnWidth) / (metrics.length + 1) - 10
)
} else {
plotWidth = Math.floor(
(tableWidth - jobInfoColumnWidth) / metrics.length - 10 (tableWidth - jobInfoColumnWidth) / metrics.length - 10
); )
}
let headerPaddingTop = 0; let headerPaddingTop = 0;
stickyHeader( stickyHeader(
@ -160,6 +171,15 @@
> >
Job Info Job Info
</th> </th>
{#if showFootprint}
<th
class="position-sticky top-0"
scope="col"
style="width: {plotWidth}px; padding-top: {headerPaddingTop}px"
>
Job Footprint
</th>
{/if}
{#each metrics as metric (metric)} {#each metrics as metric (metric)}
<th <th
class="position-sticky top-0 text-center" class="position-sticky top-0 text-center"
@ -212,7 +232,7 @@
</tr> </tr>
{:else if $jobs.data && $initialized} {:else if $jobs.data && $initialized}
{#each $jobs.data.jobs.items as job (job)} {#each $jobs.data.jobs.items as job (job)}
<JobListRow {job} {metrics} {plotWidth} /> <JobListRow {job} {metrics} {plotWidth} {showFootprint}/>
{:else} {:else}
<tr> <tr>
<td colspan={metrics.length + 1}> <td colspan={metrics.length + 1}>

View File

@ -14,22 +14,28 @@
import { Card, Spinner } from "sveltestrap"; import { Card, Spinner } from "sveltestrap";
import MetricPlot from "../plots/MetricPlot.svelte"; import MetricPlot from "../plots/MetricPlot.svelte";
import JobInfo from "./JobInfo.svelte"; import JobInfo from "./JobInfo.svelte";
import JobFootprint from "../JobFootprint.svelte";
import { maxScope, checkMetricDisabled } from "../utils.js"; import { maxScope, checkMetricDisabled } from "../utils.js";
export let job; export let job;
export let metrics; export let metrics;
export let plotWidth; export let plotWidth;
export let plotHeight = 275; export let plotHeight = 275;
export let showFootprint;
let { id } = job; let { id } = job;
let scopes = [job.numNodes == 1 ? "core" : "node"]; let scopes = [job.numNodes == 1 ? "core" : "node"];
function distinct(value, index, array) {
return array.indexOf(value) === index;
}
const cluster = getContext("clusters").find((c) => c.name == job.cluster); const cluster = getContext("clusters").find((c) => c.name == job.cluster);
const metricConfig = getContext("metrics"); // Get all MetricConfs which include subCluster-specific settings for this job const metricConfig = getContext("metrics"); // Get all MetricConfs which include subCluster-specific settings for this job
const client = getContextClient(); const client = getContextClient();
const query = gql` const query = gql`
query ($id: ID!, $metrics: [String!]!, $scopes: [MetricScope!]!) { query ($id: ID!, $queryMetrics: [String!]!, $scopes: [MetricScope!]!) {
jobMetrics(id: $id, metrics: $metrics, scopes: $scopes) { jobMetrics(id: $id, metrics: $queryMetrics, scopes: $scopes) {
name name
scope scope
metric { metric {
@ -61,14 +67,24 @@
$: metricsQuery = queryStore({ $: metricsQuery = queryStore({
client: client, client: client,
query: query, query: query,
variables: { id, metrics, scopes } variables: { id, queryMetrics, scopes }
}); });
function refresh() { let queryMetrics = null
queryStore({ $: if (showFootprint) {
queryMetrics = ['cpu_load', 'flops_any', 'mem_used', 'mem_bw', 'acc_utilization', ...metrics].filter(distinct)
scopes = ["node"]
} else {
queryMetrics = [...metrics]
scopes = [job.numNodes == 1 ? "core" : "node"]
}
export function refresh() {
metricsQuery = queryStore({
client: client, client: client,
query: query, query: query,
variables: { id, metrics, scopes } variables: { id, queryMetrics, scopes },
// requestPolicy: 'network-only' // use default cache-first for refresh
}); });
} }
@ -121,6 +137,16 @@
</Card> </Card>
</td> </td>
{:else} {:else}
{#if showFootprint}
<td>
<JobFootprint
job={job}
jobMetrics={$metricsQuery.data.jobMetrics}
width={plotWidth}
view="list"
/>
</td>
{/if}
{#each sortAndSelectScope($metricsQuery.data.jobMetrics) as metric, i (metric || i)} {#each sortAndSelectScope($metricsQuery.data.jobMetrics) as metric, i (metric || i)}
<td> <td>
<!-- Subluster Metricconfig remove keyword for jobtables (joblist main, user joblist, project joblist) to be used here as toplevel case--> <!-- Subluster Metricconfig remove keyword for jobtables (joblist main, user joblist, project joblist) to be used here as toplevel case-->

View File

@ -11,6 +11,7 @@
import { Card } from 'sveltestrap' import { Card } from 'sveltestrap'
export let data export let data
export let usesBins = false
export let width = 500 export let width = 500
export let height = 300 export let height = 300
export let title = '' export let title = ''
@ -160,6 +161,14 @@
series: [ series: [
{ {
label: xunit !== '' ? xunit : null, label: xunit !== '' ? xunit : null,
value: (u, ts, sidx, didx) => {
if (usesBins) {
const min = u.data[sidx][didx - 1] ? u.data[sidx][didx - 1] : 0
const max = u.data[sidx][didx]
ts = min + '-' + max // narrow spaces
}
return ts
}
}, },
Object.assign({ Object.assign({
label: yunit !== '' ? yunit : null, label: yunit !== '' ? yunit : null,

View File

@ -161,8 +161,8 @@
? (statisticsSeries.max.reduce((max, x) => Math.max(max, x), thresholds.normal) || thresholds.normal) ? (statisticsSeries.max.reduce((max, x) => Math.max(max, x), thresholds.normal) || thresholds.normal)
: (series.reduce((max, series) => Math.max(max, series.statistics?.max), thresholds.normal) || thresholds.normal) : (series.reduce((max, series) => Math.max(max, series.statistics?.max), thresholds.normal) || thresholds.normal)
if (maxY >= (10 * thresholds.normal)) { // Hard y-range render limit if outliers in series data if (maxY >= (10 * thresholds.peak)) { // Hard y-range render limit if outliers in series data
maxY = (10 * thresholds.normal) maxY = (10 * thresholds.peak)
} }
} }
@ -390,12 +390,12 @@
if (scope == 'node' || metricConfig.aggregation == 'avg') { if (scope == 'node' || metricConfig.aggregation == 'avg') {
if (metricConfig.subClusters && metricConfig.subClusters.length === 0) { if (metricConfig.subClusters && metricConfig.subClusters.length === 0) {
// console.log('subClusterConfigs array empty, use metricConfig defaults') // console.log('subClusterConfigs array empty, use metricConfig defaults')
return { normal: metricConfig.normal, caution: metricConfig.caution, alert: metricConfig.alert } return { normal: metricConfig.normal, caution: metricConfig.caution, alert: metricConfig.alert, peak: metricConfig.peak }
} else if (metricConfig.subClusters && metricConfig.subClusters.length > 0) { } else if (metricConfig.subClusters && metricConfig.subClusters.length > 0) {
// console.log('subClusterConfigs found, use subCluster Settings if matching jobs subcluster:') // console.log('subClusterConfigs found, use subCluster Settings if matching jobs subcluster:')
let forSubCluster = metricConfig.subClusters.find(sc => sc.name == subCluster.name) let forSubCluster = metricConfig.subClusters.find(sc => sc.name == subCluster.name)
if (forSubCluster && forSubCluster.normal && forSubCluster.caution && forSubCluster.alert) return forSubCluster if (forSubCluster && forSubCluster.normal && forSubCluster.caution && forSubCluster.alert && forSubCluster.peak) return forSubCluster
else return { normal: metricConfig.normal, caution: metricConfig.caution, alert: metricConfig.alert } else return { normal: metricConfig.normal, caution: metricConfig.caution, alert: metricConfig.alert, peak: metricConfig.peak}
} else { } else {
console.warn('metricConfig.subClusters not found!') console.warn('metricConfig.subClusters not found!')
return null return null
@ -423,6 +423,7 @@
let mc = metricConfig?.subClusters?.find(sc => sc.name == subCluster.name) || metricConfig let mc = metricConfig?.subClusters?.find(sc => sc.name == subCluster.name) || metricConfig
return { return {
peak: mc.peak / divisor,
normal: mc.normal / divisor, normal: mc.normal / divisor,
caution: mc.caution / divisor, caution: mc.caution / divisor,
alert: mc.alert / divisor alert: mc.alert / divisor

View File

@ -176,12 +176,12 @@
// Debug get zoomLevel from browser // Debug get zoomLevel from browser
// console.log("Zoom", Math.round(window.devicePixelRatio * 100)) // console.log("Zoom", Math.round(window.devicePixelRatio * 100))
if (scalarKneeX < (width * window.devicePixelRatio) - (padding[1] * window.devicePixelRatio)) { // Top horizontal roofline if (scalarKneeX < (width * window.devicePixelRatio) - (padding[1] * window.devicePixelRatio)) { // Lower horizontal roofline
u.ctx.moveTo(scalarKneeX, flopRateScalarY) u.ctx.moveTo(scalarKneeX, flopRateScalarY)
u.ctx.lineTo((width * window.devicePixelRatio) - (padding[1] * window.devicePixelRatio), flopRateScalarY) u.ctx.lineTo((width * window.devicePixelRatio) - (padding[1] * window.devicePixelRatio), flopRateScalarY)
} }
if (simdKneeX < (width * window.devicePixelRatio) - (padding[1] * window.devicePixelRatio)) { // Lower horitontal roofline if (simdKneeX < (width * window.devicePixelRatio) - (padding[1] * window.devicePixelRatio)) { // Top horitontal roofline
u.ctx.moveTo(simdKneeX, flopRateSimdY) u.ctx.moveTo(simdKneeX, flopRateSimdY)
u.ctx.lineTo((width * window.devicePixelRatio) - (padding[1] * window.devicePixelRatio), flopRateSimdY) u.ctx.lineTo((width * window.devicePixelRatio) - (padding[1] * window.devicePixelRatio), flopRateSimdY)
} }
@ -214,6 +214,7 @@
} }
] ]
}, },
// cursor: { drag: { x: true, y: true } } // Activate zoom
}; };
uplot = new uPlot(opts, plotData, plotWrapper); uplot = new uPlot(opts, plotData, plotWrapper);
} else { } else {

View File

@ -316,11 +316,17 @@ export function checkMetricDisabled(m, c, s) { //[m]etric, [c]luster, [s]ubclust
} }
export function convert2uplot(canvasData) { export function convert2uplot(canvasData) {
// initial use: Canvas Histogram Data to Uplot // Prep: Uplot Data Structure
let uplotData = [[],[]] // [X, Y1, Y2, ...] let uplotData = [[],[]] // [X, Y1, Y2, ...]
canvasData.forEach( pair => { // Iterate
uplotData[0].push(pair.value) canvasData.forEach( cd => {
uplotData[1].push(pair.count) if (Object.keys(cd).length == 4) { // MetricHisto Datafromat
uplotData[0].push(cd?.max ? cd.max : 0)
uplotData[1].push(cd.count)
} else { // Default
uplotData[0].push(cd.value)
uplotData[1].push(cd.count)
}
}) })
return uplotData return uplotData
} }