Merge pull request #284 from ClusterCockpit/Refactor-job-footprint

Refactor job footprint
This commit is contained in:
Jan Eitzinger 2024-08-19 12:15:59 +02:00 committed by GitHub
commit 5603c41900
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
78 changed files with 2121 additions and 1693 deletions

View File

@ -272,6 +272,7 @@ input JobFilter {
input OrderByInput { input OrderByInput {
field: String! field: String!
type: String!,
order: SortDirectionEnum! = ASC order: SortDirectionEnum! = ASC
} }
@ -319,6 +320,7 @@ type HistoPoint {
type MetricHistoPoints { type MetricHistoPoints {
metric: String! metric: String!
unit: String! unit: String!
stat: String
data: [MetricHistoPoint!] data: [MetricHistoPoint!]
} }

View File

@ -38,6 +38,15 @@ var (
apiHandle *api.RestApi apiHandle *api.RestApi
) )
func onFailureResponse(rw http.ResponseWriter, r *http.Request, err error) {
rw.Header().Add("Content-Type", "application/json")
rw.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(rw).Encode(map[string]string{
"status": http.StatusText(http.StatusUnauthorized),
"error": err.Error(),
})
}
func serverInit() { func serverInit() {
// Setup the http.Handler/Router used by the server // Setup the http.Handler/Router used by the server
graph.Init() graph.Init()
@ -166,64 +175,32 @@ func serverInit() {
return authHandle.AuthApi( return authHandle.AuthApi(
// On success; // On success;
next, next,
// On failure: JSON Response // On failure: JSON Response
func(rw http.ResponseWriter, r *http.Request, err error) { onFailureResponse)
rw.Header().Add("Content-Type", "application/json")
rw.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(rw).Encode(map[string]string{
"status": http.StatusText(http.StatusUnauthorized),
"error": err.Error(),
})
})
}) })
userapi.Use(func(next http.Handler) http.Handler { userapi.Use(func(next http.Handler) http.Handler {
return authHandle.AuthUserApi( return authHandle.AuthUserApi(
// On success; // On success;
next, next,
// On failure: JSON Response // On failure: JSON Response
func(rw http.ResponseWriter, r *http.Request, err error) { onFailureResponse)
rw.Header().Add("Content-Type", "application/json")
rw.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(rw).Encode(map[string]string{
"status": http.StatusText(http.StatusUnauthorized),
"error": err.Error(),
})
})
}) })
configapi.Use(func(next http.Handler) http.Handler { configapi.Use(func(next http.Handler) http.Handler {
return authHandle.AuthConfigApi( return authHandle.AuthConfigApi(
// On success; // On success;
next, next,
// On failure: JSON Response // On failure: JSON Response
func(rw http.ResponseWriter, r *http.Request, err error) { onFailureResponse)
rw.Header().Add("Content-Type", "application/json")
rw.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(rw).Encode(map[string]string{
"status": http.StatusText(http.StatusUnauthorized),
"error": err.Error(),
})
})
}) })
frontendapi.Use(func(next http.Handler) http.Handler { frontendapi.Use(func(next http.Handler) http.Handler {
return authHandle.AuthFrontendApi( return authHandle.AuthFrontendApi(
// On success; // On success;
next, next,
// On failure: JSON Response // On failure: JSON Response
func(rw http.ResponseWriter, r *http.Request, err error) { onFailureResponse)
rw.Header().Add("Content-Type", "application/json")
rw.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(rw).Encode(map[string]string{
"status": http.StatusText(http.StatusUnauthorized),
"error": err.Error(),
})
})
}) })
} }

View File

@ -119,7 +119,6 @@ func (api *RestApi) MountFrontendApiRoutes(r *mux.Router) {
if api.Authentication != nil { if api.Authentication != nil {
r.HandleFunc("/jwt/", api.getJWT).Methods(http.MethodGet) r.HandleFunc("/jwt/", api.getJWT).Methods(http.MethodGet)
r.HandleFunc("/configuration/", api.updateConfiguration).Methods(http.MethodPost) r.HandleFunc("/configuration/", api.updateConfiguration).Methods(http.MethodPost)
r.HandleFunc("/jobs/metrics/{id}", api.getJobMetrics).Methods(http.MethodGet) // Fetched in Job.svelte: Needs All-User-Access-Session-Auth
} }
} }

View File

@ -211,6 +211,7 @@ type ComplexityRoot struct {
MetricHistoPoints struct { MetricHistoPoints struct {
Data func(childComplexity int) int Data func(childComplexity int) int
Metric func(childComplexity int) int Metric func(childComplexity int) int
Stat func(childComplexity int) int
Unit func(childComplexity int) int Unit func(childComplexity int) int
} }
@ -1104,6 +1105,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.MetricHistoPoints.Metric(childComplexity), true return e.complexity.MetricHistoPoints.Metric(childComplexity), true
case "MetricHistoPoints.stat":
if e.complexity.MetricHistoPoints.Stat == nil {
break
}
return e.complexity.MetricHistoPoints.Stat(childComplexity), true
case "MetricHistoPoints.unit": case "MetricHistoPoints.unit":
if e.complexity.MetricHistoPoints.Unit == nil { if e.complexity.MetricHistoPoints.Unit == nil {
break break
@ -2100,6 +2108,7 @@ input JobFilter {
input OrderByInput { input OrderByInput {
field: String! field: String!
type: String!,
order: SortDirectionEnum! = ASC order: SortDirectionEnum! = ASC
} }
@ -2147,6 +2156,7 @@ type HistoPoint {
type MetricHistoPoints { type MetricHistoPoints {
metric: String! metric: String!
unit: String! unit: String!
stat: String
data: [MetricHistoPoint!] data: [MetricHistoPoint!]
} }
@ -6445,6 +6455,8 @@ func (ec *executionContext) fieldContext_JobsStatistics_histMetrics(_ context.Co
return ec.fieldContext_MetricHistoPoints_metric(ctx, field) return ec.fieldContext_MetricHistoPoints_metric(ctx, field)
case "unit": case "unit":
return ec.fieldContext_MetricHistoPoints_unit(ctx, field) return ec.fieldContext_MetricHistoPoints_unit(ctx, field)
case "stat":
return ec.fieldContext_MetricHistoPoints_stat(ctx, field)
case "data": case "data":
return ec.fieldContext_MetricHistoPoints_data(ctx, field) return ec.fieldContext_MetricHistoPoints_data(ctx, field)
} }
@ -7295,6 +7307,47 @@ func (ec *executionContext) fieldContext_MetricHistoPoints_unit(_ context.Contex
return fc, nil return fc, nil
} }
func (ec *executionContext) _MetricHistoPoints_stat(ctx context.Context, field graphql.CollectedField, obj *model.MetricHistoPoints) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_MetricHistoPoints_stat(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Stat, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*string)
fc.Result = res
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_MetricHistoPoints_stat(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "MetricHistoPoints",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _MetricHistoPoints_data(ctx context.Context, field graphql.CollectedField, obj *model.MetricHistoPoints) (ret graphql.Marshaler) { func (ec *executionContext) _MetricHistoPoints_data(ctx context.Context, field graphql.CollectedField, obj *model.MetricHistoPoints) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_MetricHistoPoints_data(ctx, field) fc, err := ec.fieldContext_MetricHistoPoints_data(ctx, field)
if err != nil { if err != nil {
@ -13217,7 +13270,7 @@ func (ec *executionContext) unmarshalInputOrderByInput(ctx context.Context, obj
asMap["order"] = "ASC" asMap["order"] = "ASC"
} }
fieldsInOrder := [...]string{"field", "order"} fieldsInOrder := [...]string{"field", "type", "order"}
for _, k := range fieldsInOrder { for _, k := range fieldsInOrder {
v, ok := asMap[k] v, ok := asMap[k]
if !ok { if !ok {
@ -13231,6 +13284,13 @@ func (ec *executionContext) unmarshalInputOrderByInput(ctx context.Context, obj
return it, err return it, err
} }
it.Field = data it.Field = data
case "type":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("type"))
data, err := ec.unmarshalNString2string(ctx, v)
if err != nil {
return it, err
}
it.Type = data
case "order": case "order":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("order")) ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("order"))
data, err := ec.unmarshalNSortDirectionEnum2githubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐSortDirectionEnum(ctx, v) data, err := ec.unmarshalNSortDirectionEnum2githubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐSortDirectionEnum(ctx, v)
@ -14673,6 +14733,8 @@ func (ec *executionContext) _MetricHistoPoints(ctx context.Context, sel ast.Sele
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
out.Invalids++ out.Invalids++
} }
case "stat":
out.Values[i] = ec._MetricHistoPoints_stat(ctx, field, obj)
case "data": case "data":
out.Values[i] = ec._MetricHistoPoints_data(ctx, field, obj) out.Values[i] = ec._MetricHistoPoints_data(ctx, field, obj)
default: default:

View File

@ -123,6 +123,7 @@ type MetricHistoPoint struct {
type MetricHistoPoints struct { type MetricHistoPoints struct {
Metric string `json:"metric"` Metric string `json:"metric"`
Unit string `json:"unit"` Unit string `json:"unit"`
Stat *string `json:"stat,omitempty"`
Data []*MetricHistoPoint `json:"data,omitempty"` Data []*MetricHistoPoint `json:"data,omitempty"`
} }
@ -142,6 +143,7 @@ type NodeMetrics struct {
type OrderByInput struct { type OrderByInput struct {
Field string `json:"field"` Field string `json:"field"`
Type string `json:"type"`
Order SortDirectionEnum `json:"order"` Order SortDirectionEnum `json:"order"`
} }

View File

@ -307,6 +307,10 @@ func ArchiveJob(job *schema.Job, ctx context.Context) (*schema.JobMeta, error) {
scopes = append(scopes, schema.MetricScopeCore) scopes = append(scopes, schema.MetricScopeCore)
} }
if job.NumAcc > 0 {
scopes = append(scopes, schema.MetricScopeAccelerator)
}
jobData, err := LoadData(job, allMetrics, scopes, ctx) jobData, err := LoadData(job, allMetrics, scopes, ctx)
if err != nil { if err != nil {
log.Error("Error wile loading job data for archiving") log.Error("Error wile loading job data for archiving")

View File

@ -31,14 +31,28 @@ func (r *JobRepository) QueryJobs(
if order != nil { if order != nil {
field := toSnakeCase(order.Field) field := toSnakeCase(order.Field)
if order.Type == "col" {
switch order.Order { // "col": Fixed column name query
case model.SortDirectionEnumAsc: switch order.Order {
query = query.OrderBy(fmt.Sprintf("job.%s ASC", field)) case model.SortDirectionEnumAsc:
case model.SortDirectionEnumDesc: query = query.OrderBy(fmt.Sprintf("job.%s ASC", field))
query = query.OrderBy(fmt.Sprintf("job.%s DESC", field)) case model.SortDirectionEnumDesc:
default: query = query.OrderBy(fmt.Sprintf("job.%s DESC", field))
return nil, errors.New("REPOSITORY/QUERY > invalid sorting order") default:
return nil, errors.New("REPOSITORY/QUERY > invalid sorting order for column")
}
} else {
// "foot": Order by footprint JSON field values
// Verify and Search Only in Valid Jsons
query = query.Where("JSON_VALID(meta_data)")
switch order.Order {
case model.SortDirectionEnumAsc:
query = query.OrderBy(fmt.Sprintf("JSON_EXTRACT(footprint, \"$.%s\") ASC", field))
case model.SortDirectionEnumDesc:
query = query.OrderBy(fmt.Sprintf("JSON_EXTRACT(footprint, \"$.%s\") DESC", field))
default:
return nil, errors.New("REPOSITORY/QUERY > invalid sorting order for footprint")
}
} }
} }
@ -177,8 +191,8 @@ func BuildWhereClause(filter *model.JobFilter, query sq.SelectBuilder) sq.Select
query = buildStringCondition("job.resources", filter.Node, query) query = buildStringCondition("job.resources", filter.Node, query)
} }
if filter.MetricStats != nil { if filter.MetricStats != nil {
for _, m := range filter.MetricStats { for _, ms := range filter.MetricStats {
query = buildFloatJsonCondition("job.metric_stats", m.Range, query) query = buildFloatJsonCondition(ms.MetricName, ms.Range, query)
} }
} }
return query return query
@ -200,8 +214,10 @@ func buildTimeCondition(field string, cond *schema.TimeRange, query sq.SelectBui
} }
} }
func buildFloatJsonCondition(field string, cond *model.FloatRange, query sq.SelectBuilder) sq.SelectBuilder { func buildFloatJsonCondition(condName string, condRange *model.FloatRange, query sq.SelectBuilder) sq.SelectBuilder {
return query.Where("JSON_EXTRACT(footprint, '$."+field+"') BETWEEN ? AND ?", cond.From, cond.To) // Verify and Search Only in Valid Jsons
query = query.Where("JSON_VALID(footprint)")
return query.Where("JSON_EXTRACT(footprint, \"$."+condName+"\") BETWEEN ? AND ?", condRange.From, condRange.To)
} }
func buildStringCondition(field string, cond *model.StringInput, query sq.SelectBuilder) sq.SelectBuilder { func buildStringCondition(field string, cond *model.StringInput, query sq.SelectBuilder) sq.SelectBuilder {

View File

@ -0,0 +1,21 @@
ALTER TABLE job DROP energy;
ALTER TABLE job DROP energy_footprint;
ALTER TABLE job ADD COLUMN flops_any_avg;
ALTER TABLE job ADD COLUMN mem_bw_avg;
ALTER TABLE job ADD COLUMN mem_used_max;
ALTER TABLE job ADD COLUMN load_avg;
ALTER TABLE job ADD COLUMN net_bw_avg;
ALTER TABLE job ADD COLUMN net_data_vol_total;
ALTER TABLE job ADD COLUMN file_bw_avg;
ALTER TABLE job ADD COLUMN file_data_vol_total;
UPDATE job SET flops_any_avg = json_extract(footprint, '$.flops_any_avg');
UPDATE job SET mem_bw_avg = json_extract(footprint, '$.mem_bw_avg');
UPDATE job SET mem_used_max = json_extract(footprint, '$.mem_used_max');
UPDATE job SET load_avg = json_extract(footprint, '$.cpu_load_avg');
UPDATE job SET net_bw_avg = json_extract(footprint, '$.net_bw_avg');
UPDATE job SET net_data_vol_total = json_extract(footprint, '$.net_data_vol_total');
UPDATE job SET file_bw_avg = json_extract(footprint, '$.file_bw_avg');
UPDATE job SET file_data_vol_total = json_extract(footprint, '$.file_data_vol_total');
ALTER TABLE job DROP footprint;

View File

@ -1,12 +1,26 @@
CREATE INDEX IF NOT EXISTS job_by_project ON job (project);
CREATE INDEX IF NOT EXISTS job_list_projects ON job (project, job_state);
ALTER TABLE job ADD COLUMN energy REAL NOT NULL DEFAULT 0.0; ALTER TABLE job ADD COLUMN energy REAL NOT NULL DEFAULT 0.0;
ALTER TABLE job ADD COLUMN energy_footprint TEXT DEFAULT NULL;
ALTER TABLE job ADD COLUMN footprint TEXT DEFAULT NULL; ALTER TABLE job ADD COLUMN footprint TEXT DEFAULT NULL;
UPDATE job SET footprint = '{"flops_any_avg": 0.0}'; UPDATE job SET footprint = '{"flops_any_avg": 0.0}';
UPDATE job SET footprint = json_replace(footprint, '$.flops_any_avg', job.flops_any_avg); UPDATE job SET footprint = json_replace(footprint, '$.flops_any_avg', job.flops_any_avg);
UPDATE job SET footprint = json_insert(footprint, '$.mem_bw_avg', job.mem_bw_avg); UPDATE job SET footprint = json_insert(footprint, '$.mem_bw_avg', job.mem_bw_avg);
UPDATE job SET footprint = json_insert(footprint, '$.mem_used_max', job.mem_used_max); UPDATE job SET footprint = json_insert(footprint, '$.mem_used_max', job.mem_used_max);
UPDATE job SET footprint = json_insert(footprint, '$.cpu_load_avg', job.load_avg); UPDATE job SET footprint = json_insert(footprint, '$.cpu_load_avg', job.load_avg);
UPDATE job SET footprint = json_insert(footprint, '$.net_bw_avg', job.net_bw_avg) WHERE job.net_bw_avg != 0;
UPDATE job SET footprint = json_insert(footprint, '$.net_data_vol_total', job.net_data_vol_total) WHERE job.net_data_vol_total != 0;
UPDATE job SET footprint = json_insert(footprint, '$.file_bw_avg', job.file_bw_avg) WHERE job.file_bw_avg != 0;
UPDATE job SET footprint = json_insert(footprint, '$.file_data_vol_total', job.file_data_vol_total) WHERE job.file_data_vol_total != 0;
ALTER TABLE job DROP flops_any_avg; ALTER TABLE job DROP flops_any_avg;
ALTER TABLE job DROP mem_bw_avg; ALTER TABLE job DROP mem_bw_avg;
ALTER TABLE job DROP mem_used_max; ALTER TABLE job DROP mem_used_max;
ALTER TABLE job DROP load_avg; ALTER TABLE job DROP load_avg;
ALTER TABLE job DROP net_bw_avg;
ALTER TABLE job DROP net_data_vol_total;
ALTER TABLE job DROP file_bw_avg;
ALTER TABLE job DROP file_data_vol_total;

View File

@ -552,12 +552,14 @@ func (r *JobRepository) jobsMetricStatisticsHistogram(
var metricConfig *schema.MetricConfig var metricConfig *schema.MetricConfig
var peak float64 = 0.0 var peak float64 = 0.0
var unit string = "" var unit string = ""
var footprintStat string = ""
for _, f := range filters { for _, f := range filters {
if f.Cluster != nil { if f.Cluster != nil {
metricConfig = archive.GetMetricConfig(*f.Cluster.Eq, metric) metricConfig = archive.GetMetricConfig(*f.Cluster.Eq, metric)
peak = metricConfig.Peak peak = metricConfig.Peak
unit = metricConfig.Unit.Prefix + metricConfig.Unit.Base unit = metricConfig.Unit.Prefix + metricConfig.Unit.Base
footprintStat = metricConfig.Footprint
log.Debugf("Cluster %s filter found with peak %f for %s", *f.Cluster.Eq, peak, metric) log.Debugf("Cluster %s filter found with peak %f for %s", *f.Cluster.Eq, peak, metric)
} }
} }
@ -572,21 +574,26 @@ func (r *JobRepository) jobsMetricStatisticsHistogram(
if unit == "" { if unit == "" {
unit = m.Unit.Prefix + m.Unit.Base unit = m.Unit.Prefix + m.Unit.Base
} }
if footprintStat == "" {
footprintStat = m.Footprint
}
} }
} }
} }
} }
// log.Debugf("Metric %s: DB %s, Peak %f, Unit %s", metric, dbMetric, peak, unit) // log.Debugf("Metric %s, Peak %f, Unit %s, Aggregation %s", metric, peak, unit, aggreg)
// Make bins, see https://jereze.com/code/sql-histogram/ // Make bins, see https://jereze.com/code/sql-histogram/
start := time.Now() start := time.Now()
jm := fmt.Sprintf(`json_extract(footprint, "$.%s")`, metric) jm := fmt.Sprintf(`json_extract(footprint, "$.%s")`, (metric + "_" + footprintStat))
crossJoinQuery := sq.Select( crossJoinQuery := sq.Select(
fmt.Sprintf(`max(%s) as max`, jm), fmt.Sprintf(`max(%s) as max`, jm),
fmt.Sprintf(`min(%s) as min`, jm), fmt.Sprintf(`min(%s) as min`, jm),
).From("job").Where( ).From("job").Where(
"JSON_VALID(footprint)",
).Where(
fmt.Sprintf(`%s is not null`, jm), fmt.Sprintf(`%s is not null`, jm),
).Where( ).Where(
fmt.Sprintf(`%s <= %f`, jm, peak), fmt.Sprintf(`%s <= %f`, jm, peak),
@ -651,7 +658,7 @@ func (r *JobRepository) jobsMetricStatisticsHistogram(
points = append(points, &point) points = append(points, &point)
} }
result := model.MetricHistoPoints{Metric: metric, Unit: unit, Data: points} result := model.MetricHistoPoints{Metric: metric, Unit: unit, Stat: &footprintStat, Data: points}
log.Debugf("Timer jobsStatisticsHistogram %s", time.Since(start)) log.Debugf("Timer jobsStatisticsHistogram %s", time.Since(start))
return &result, nil return &result, nil

View File

@ -47,11 +47,11 @@ type SubCluster struct {
type SubClusterConfig struct { type SubClusterConfig struct {
Name string `json:"name"` Name string `json:"name"`
Footprint string `json:"footprint,omitempty"`
Peak float64 `json:"peak"` Peak float64 `json:"peak"`
Normal float64 `json:"normal"` Normal float64 `json:"normal"`
Caution float64 `json:"caution"` Caution float64 `json:"caution"`
Alert float64 `json:"alert"` Alert float64 `json:"alert"`
Footprint string `json:"footprint,omitempty"`
Remove bool `json:"remove"` Remove bool `json:"remove"`
LowerIsBetter bool `json:"lowerIsBetter"` LowerIsBetter bool `json:"lowerIsBetter"`
Energy bool `json:"energy"` Energy bool `json:"energy"`
@ -62,14 +62,14 @@ type MetricConfig struct {
Name string `json:"name"` Name string `json:"name"`
Scope MetricScope `json:"scope"` Scope MetricScope `json:"scope"`
Aggregation string `json:"aggregation"` Aggregation string `json:"aggregation"`
Footprint string `json:"footprint,omitempty"`
SubClusters []*SubClusterConfig `json:"subClusters,omitempty"` SubClusters []*SubClusterConfig `json:"subClusters,omitempty"`
Timestep int `json:"timestep"`
Peak float64 `json:"peak"` Peak float64 `json:"peak"`
Normal float64 `json:"normal"` Normal float64 `json:"normal"`
Caution float64 `json:"caution"` Caution float64 `json:"caution"`
Alert float64 `json:"alert"` Alert float64 `json:"alert"`
Timestep int `json:"timestep"`
LowerIsBetter bool `json:"lowerIsBetter"` LowerIsBetter bool `json:"lowerIsBetter"`
Footprint string `json:"footprint,omitempty"`
Energy bool `json:"energy"` Energy bool `json:"energy"`
} }

View File

@ -32,7 +32,7 @@ type BaseJob struct {
Footprint map[string]float64 `json:"footprint"` Footprint map[string]float64 `json:"footprint"`
MetaData map[string]string `json:"metaData"` MetaData map[string]string `json:"metaData"`
ConcurrentJobs JobLinkResultList `json:"concurrentJobs"` ConcurrentJobs JobLinkResultList `json:"concurrentJobs"`
Energy float64 `json:"energy"` Energy float64 `json:"energy" db:"energy"`
ArrayJobId int64 `json:"arrayJobId,omitempty" db:"array_job_id" example:"123000"` ArrayJobId int64 `json:"arrayJobId,omitempty" db:"array_job_id" example:"123000"`
Walltime int64 `json:"walltime,omitempty" db:"walltime" example:"86400" minimum:"1"` Walltime int64 `json:"walltime,omitempty" db:"walltime" example:"86400" minimum:"1"`
JobID int64 `json:"jobId" db:"job_id" example:"123000"` JobID int64 `json:"jobId" db:"job_id" example:"123000"`

View File

@ -1,284 +1,319 @@
{ {
"$schema": "http://json-schema.org/draft/2020-12/schema", "$schema": "http://json-schema.org/draft/2020-12/schema",
"$id": "embedfs://cluster.schema.json", "$id": "embedfs://cluster.schema.json",
"title": "HPC cluster description", "title": "HPC cluster description",
"description": "Meta data information of a HPC cluster", "description": "Meta data information of a HPC cluster",
"type": "object", "type": "object",
"properties": { "properties": {
"name": { "name": {
"description": "The unique identifier of a cluster", "description": "The unique identifier of a cluster",
"type": "string" "type": "string"
},
"metricConfig": {
"description": "Metric specifications",
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"description": "Metric name",
"type": "string"
},
"unit": {
"description": "Metric unit",
"$ref": "embedfs://unit.schema.json"
},
"scope": {
"description": "Native measurement resolution",
"type": "string"
},
"timestep": {
"description": "Frequency of timeseries points",
"type": "integer"
},
"aggregation": {
"description": "How the metric is aggregated",
"type": "string",
"enum": [
"sum",
"avg"
]
},
"peak": {
"description": "Metric peak threshold (Upper metric limit)",
"type": "number"
},
"normal": {
"description": "Metric normal threshold",
"type": "number"
},
"caution": {
"description": "Metric caution threshold (Suspicious but does not require immediate action)",
"type": "number"
},
"alert": {
"description": "Metric alert threshold (Requires immediate action)",
"type": "number"
},
"subClusters": {
"description": "Array of cluster hardware partition metric thresholds",
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"description": "Hardware partition name",
"type": "string"
},
"peak": {
"type": "number"
},
"normal": {
"type": "number"
},
"caution": {
"type": "number"
},
"alert": {
"type": "number"
},
"remove": {
"type": "boolean"
}
},
"required": [
"name"
]
}
}
},
"required": [
"name",
"unit",
"scope",
"timestep",
"aggregation",
"peak",
"normal",
"caution",
"alert"
]
},
"minItems": 1
},
"subClusters": {
"description": "Array of cluster hardware partitions",
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"description": "Hardware partition name",
"type": "string"
},
"processorType": {
"description": "Processor type",
"type": "string"
},
"socketsPerNode": {
"description": "Number of sockets per node",
"type": "integer"
},
"coresPerSocket": {
"description": "Number of cores per socket",
"type": "integer"
},
"threadsPerCore": {
"description": "Number of SMT threads per core",
"type": "integer"
},
"flopRateScalar": {
"description": "Theoretical node peak flop rate for scalar code in GFlops/s",
"type": "object",
"properties": {
"unit": {
"description": "Metric unit",
"$ref": "embedfs://unit.schema.json"
},
"value": {
"type": "number"
}
}
},
"flopRateSimd": {
"description": "Theoretical node peak flop rate for SIMD code in GFlops/s",
"type": "object",
"properties": {
"unit": {
"description": "Metric unit",
"$ref": "embedfs://unit.schema.json"
},
"value": {
"type": "number"
}
}
},
"memoryBandwidth": {
"description": "Theoretical node peak memory bandwidth in GB/s",
"type": "object",
"properties": {
"unit": {
"description": "Metric unit",
"$ref": "embedfs://unit.schema.json"
},
"value": {
"type": "number"
}
}
},
"nodes": {
"description": "Node list expression",
"type": "string"
},
"topology": {
"description": "Node topology",
"type": "object",
"properties": {
"node": {
"description": "HwTread lists of node",
"type": "array",
"items": {
"type": "integer"
}
},
"socket": {
"description": "HwTread lists of sockets",
"type": "array",
"items": {
"type": "array",
"items": {
"type": "integer"
}
}
},
"memoryDomain": {
"description": "HwTread lists of memory domains",
"type": "array",
"items": {
"type": "array",
"items": {
"type": "integer"
}
}
},
"die": {
"description": "HwTread lists of dies",
"type": "array",
"items": {
"type": "array",
"items": {
"type": "integer"
}
}
},
"core": {
"description": "HwTread lists of cores",
"type": "array",
"items": {
"type": "array",
"items": {
"type": "integer"
}
}
},
"accelerators": {
"type": "array",
"description": "List of of accelerator devices",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "The unique device id"
},
"type": {
"type": "string",
"description": "The accelerator type",
"enum": [
"Nvidia GPU",
"AMD GPU",
"Intel GPU"
]
},
"model": {
"type": "string",
"description": "The accelerator model"
}
},
"required": [
"id",
"type",
"model"
]
}
}
},
"required": [
"node",
"socket",
"memoryDomain"
]
}
},
"required": [
"name",
"nodes",
"topology",
"processorType",
"socketsPerNode",
"coresPerSocket",
"threadsPerCore",
"flopRateScalar",
"flopRateSimd",
"memoryBandwidth"
]
},
"minItems": 1
}
}, },
"required": [ "metricConfig": {
"name", "description": "Metric specifications",
"metricConfig", "type": "array",
"subClusters" "items": {
] "type": "object",
"properties": {
"name": {
"description": "Metric name",
"type": "string"
},
"unit": {
"description": "Metric unit",
"$ref": "embedfs://unit.schema.json"
},
"scope": {
"description": "Native measurement resolution",
"type": "string"
},
"timestep": {
"description": "Frequency of timeseries points",
"type": "integer"
},
"aggregation": {
"description": "How the metric is aggregated",
"type": "string",
"enum": [
"sum",
"avg"
]
},
"footprint": {
"description": "Is it a footprint metric and what type",
"type": "string",
"enum": [
"avg",
"max",
"min"
]
},
"energy": {
"description": "Is it used to calculate job energy",
"type": "boolean"
},
"lowerIsBetter": {
"description": "Is lower better.",
"type": "boolean"
},
"peak": {
"description": "Metric peak threshold (Upper metric limit)",
"type": "number"
},
"normal": {
"description": "Metric normal threshold",
"type": "number"
},
"caution": {
"description": "Metric caution threshold (Suspicious but does not require immediate action)",
"type": "number"
},
"alert": {
"description": "Metric alert threshold (Requires immediate action)",
"type": "number"
},
"subClusters": {
"description": "Array of cluster hardware partition metric thresholds",
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"description": "Hardware partition name",
"type": "string"
},
"footprint": {
"description": "Is it a footprint metric and what type. Overwrite global setting",
"type": "string",
"enum": [
"avg",
"max",
"min"
]
},
"energy": {
"description": "Is it used to calculate job energy. Overwrite global",
"type": "boolean"
},
"lowerIsBetter": {
"description": "Is lower better. Overwrite global",
"type": "boolean"
},
"peak": {
"type": "number"
},
"normal": {
"type": "number"
},
"caution": {
"type": "number"
},
"alert": {
"type": "number"
},
"remove": {
"description": "Remove this metric for this subcluster",
"type": "boolean"
}
},
"required": [
"name"
]
}
}
},
"required": [
"name",
"unit",
"scope",
"timestep",
"aggregation",
"peak",
"normal",
"caution",
"alert"
]
},
"minItems": 1
},
"subClusters": {
"description": "Array of cluster hardware partitions",
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"description": "Hardware partition name",
"type": "string"
},
"processorType": {
"description": "Processor type",
"type": "string"
},
"socketsPerNode": {
"description": "Number of sockets per node",
"type": "integer"
},
"coresPerSocket": {
"description": "Number of cores per socket",
"type": "integer"
},
"threadsPerCore": {
"description": "Number of SMT threads per core",
"type": "integer"
},
"flopRateScalar": {
"description": "Theoretical node peak flop rate for scalar code in GFlops/s",
"type": "object",
"properties": {
"unit": {
"description": "Metric unit",
"$ref": "embedfs://unit.schema.json"
},
"value": {
"type": "number"
}
}
},
"flopRateSimd": {
"description": "Theoretical node peak flop rate for SIMD code in GFlops/s",
"type": "object",
"properties": {
"unit": {
"description": "Metric unit",
"$ref": "embedfs://unit.schema.json"
},
"value": {
"type": "number"
}
}
},
"memoryBandwidth": {
"description": "Theoretical node peak memory bandwidth in GB/s",
"type": "object",
"properties": {
"unit": {
"description": "Metric unit",
"$ref": "embedfs://unit.schema.json"
},
"value": {
"type": "number"
}
}
},
"nodes": {
"description": "Node list expression",
"type": "string"
},
"topology": {
"description": "Node topology",
"type": "object",
"properties": {
"node": {
"description": "HwTread lists of node",
"type": "array",
"items": {
"type": "integer"
}
},
"socket": {
"description": "HwTread lists of sockets",
"type": "array",
"items": {
"type": "array",
"items": {
"type": "integer"
}
}
},
"memoryDomain": {
"description": "HwTread lists of memory domains",
"type": "array",
"items": {
"type": "array",
"items": {
"type": "integer"
}
}
},
"die": {
"description": "HwTread lists of dies",
"type": "array",
"items": {
"type": "array",
"items": {
"type": "integer"
}
}
},
"core": {
"description": "HwTread lists of cores",
"type": "array",
"items": {
"type": "array",
"items": {
"type": "integer"
}
}
},
"accelerators": {
"type": "array",
"description": "List of of accelerator devices",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "The unique device id"
},
"type": {
"type": "string",
"description": "The accelerator type",
"enum": [
"Nvidia GPU",
"AMD GPU",
"Intel GPU"
]
},
"model": {
"type": "string",
"description": "The accelerator model"
}
},
"required": [
"id",
"type",
"model"
]
}
}
},
"required": [
"node",
"socket",
"memoryDomain"
]
}
},
"required": [
"name",
"nodes",
"topology",
"processorType",
"socketsPerNode",
"coresPerSocket",
"threadsPerCore",
"flopRateScalar",
"flopRateSimd",
"memoryBandwidth"
]
},
"minItems": 1
}
},
"required": [
"name",
"metricConfig",
"subClusters"
]
} }

View File

@ -1,5 +1,11 @@
<!--
@component Main analysis view component
Properties:
- `filterPresets Object`: Optional predefined filter values
-->
<script> <script>
import { init, convert2uplot } from "./utils.js";
import { getContext, onMount } from "svelte"; import { getContext, onMount } from "svelte";
import { import {
queryStore, queryStore,
@ -15,14 +21,18 @@
Table, Table,
Icon, Icon,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import Filters from "./filters/Filters.svelte"; import {
import PlotSelection from "./PlotSelection.svelte"; init,
import Histogram from "./plots/Histogram.svelte"; convert2uplot,
import Pie, { colors } from "./plots/Pie.svelte"; binsFromFootprint,
import { binsFromFootprint } from "./utils.js"; } from "./generic/utils.js";
import ScatterPlot from "./plots/Scatter.svelte"; import PlotSelection from "./analysis/PlotSelection.svelte";
import PlotTable from "./PlotTable.svelte"; import Filters from "./generic/Filters.svelte";
import RooflineHeatmap from "./plots/RooflineHeatmap.svelte"; import PlotTable from "./generic/PlotTable.svelte";
import Histogram from "./generic/plots/Histogram.svelte";
import Pie, { colors } from "./generic/plots/Pie.svelte";
import ScatterPlot from "./generic/plots/Scatter.svelte";
import RooflineHeatmap from "./generic/plots/RooflineHeatmap.svelte";
const { query: initq } = init(); const { query: initq } = init();
@ -48,8 +58,10 @@
let colWidth1, colWidth2, colWidth3, colWidth4; let colWidth1, colWidth2, colWidth3, colWidth4;
let numBins = 50; let numBins = 50;
let maxY = -1; let maxY = -1;
const initialized = getContext("initialized");
const globalMetrics = getContext("globalMetrics");
const ccconfig = getContext("cc-config"); const ccconfig = getContext("cc-config");
const metricConfig = getContext("metrics");
let metricsInHistograms = ccconfig.analysis_view_histogramMetrics, let metricsInHistograms = ccconfig.analysis_view_histogramMetrics,
metricsInScatterplots = ccconfig.analysis_view_scatterPlotMetrics; metricsInScatterplots = ccconfig.analysis_view_scatterPlotMetrics;
@ -268,10 +280,23 @@
} }
} }
let availableMetrics = [];
let metricUnits = {};
let metricScopes = {};
function loadMetrics(isInitialized) {
if (!isInitialized) return
availableMetrics = [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster.name))]
for (let sm of availableMetrics) {
metricUnits[sm.name] = (sm?.unit?.prefix ? sm.unit.prefix : "") + (sm?.unit?.base ? sm.unit.base : "")
metricScopes[sm.name] = sm?.scope
}
}
$: loadMetrics($initialized)
$: updateEntityConfiguration(groupSelection.key); $: updateEntityConfiguration(groupSelection.key);
$: updateCategoryConfiguration(sortSelection.key); $: updateCategoryConfiguration(sortSelection.key);
onMount(() => filterComponent.update()); onMount(() => filterComponent.updateFilters());
</script> </script>
<Row> <Row>
@ -285,7 +310,7 @@
<Card body color="danger">{$initq.error.message}</Card> <Card body color="danger">{$initq.error.message}</Card>
{:else if cluster} {:else if cluster}
<PlotSelection <PlotSelection
availableMetrics={cluster.metricConfig.map((mc) => mc.name)} availableMetrics={availableMetrics.map((av) => av.name)}
bind:metricsInHistograms bind:metricsInHistograms
bind:metricsInScatterplots bind:metricsInScatterplots
/> />
@ -297,7 +322,7 @@
{filterPresets} {filterPresets}
disableClusterSelection={true} disableClusterSelection={true}
startTimeQuickSelect={true} startTimeQuickSelect={true}
on:update={({ detail }) => { on:update-filters={({ detail }) => {
jobFilters = detail.filters; jobFilters = detail.filters;
}} }}
/> />
@ -430,7 +455,7 @@
width={colWidth2} width={colWidth2}
height={300} height={300}
tiles={$rooflineQuery.data.rooflineHeatmap} tiles={$rooflineQuery.data.rooflineHeatmap}
cluster={cluster.subClusters.length == 1 subCluster={cluster.subClusters.length == 1
? cluster.subClusters[0] ? cluster.subClusters[0]
: null} : null}
maxY={rooflineMaxY} maxY={rooflineMaxY}
@ -506,7 +531,7 @@
metric, metric,
...binsFromFootprint( ...binsFromFootprint(
$footprintsQuery.data.footprints.timeWeights, $footprintsQuery.data.footprints.timeWeights,
metricConfig(cluster.name, metric)?.scope, metricScopes[metric],
$footprintsQuery.data.footprints.metrics.find( $footprintsQuery.data.footprints.metrics.find(
(f) => f.metric == metric, (f) => f.metric == metric,
).data, ).data,
@ -521,22 +546,8 @@
height={250} height={250}
usesBins={true} usesBins={true}
title="Average Distribution of '{item.metric}'" title="Average Distribution of '{item.metric}'"
xlabel={`${item.metric} bin maximum ${ xlabel={`${item.metric} bin maximum [${metricUnits[item.metric]}]`}
(metricConfig(cluster.name, item.metric)?.unit?.prefix xunit={`${metricUnits[item.metric]}`}
? "[" + metricConfig(cluster.name, item.metric)?.unit?.prefix
: "") +
(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
: "") +
(metricConfig(cluster.name, item.metric)?.unit?.base
? metricConfig(cluster.name, item.metric)?.unit?.base
: "")
}`}
ylabel="Normalized Hours" ylabel="Normalized Hours"
yunit="Hours" yunit="Hours"
/> />
@ -578,22 +589,8 @@
{width} {width}
height={250} height={250}
color={"rgba(0, 102, 204, 0.33)"} color={"rgba(0, 102, 204, 0.33)"}
xLabel={`${item.m1} [${ xLabel={`${item.m1} [${metricUnits[item.m1]}]`}
(metricConfig(cluster.name, item.m1)?.unit?.prefix yLabel={`${item.m2} [${metricUnits[item.m2]}]`}
? metricConfig(cluster.name, item.m1)?.unit?.prefix
: "") +
(metricConfig(cluster.name, item.m1)?.unit?.base
? metricConfig(cluster.name, item.m1)?.unit?.base
: "")
}]`}
yLabel={`${item.m2} [${
(metricConfig(cluster.name, item.m2)?.unit?.prefix
? metricConfig(cluster.name, item.m2)?.unit?.prefix
: "") +
(metricConfig(cluster.name, item.m2)?.unit?.base
? metricConfig(cluster.name, item.m2)?.unit?.base
: "")
}]`}
X={item.f1} X={item.f1}
Y={item.f2} Y={item.f2}
S={$footprintsQuery.data.footprints.timeWeights.nodeHours} S={$footprintsQuery.data.footprints.timeWeights.nodeHours}

View File

@ -1,12 +1,17 @@
<script> <!--
// import { init } from "./utils.js"; @component Main Config Option Component, Wrapper for admin and user sub-components
import { Card, CardHeader, CardTitle } from "@sveltestrap/sveltestrap";
Properties:
- `ìsAdmin Bool!`: Is currently logged in user admin authority
- `isApi Bool!`: Is currently logged in user api authority
- `username String!`: Empty string if auth. is disabled, otherwise the username as string
-->
<script>
import { Card, CardHeader, CardTitle } from "@sveltestrap/sveltestrap";
import UserSettings from "./config/UserSettings.svelte"; import UserSettings from "./config/UserSettings.svelte";
import AdminSettings from "./config/AdminSettings.svelte"; import AdminSettings from "./config/AdminSettings.svelte";
// const { query: initq } = init();
export let isAdmin; export let isAdmin;
export let isApi; export let isApi;
export let username; export let username;

View File

@ -1,3 +1,13 @@
<!--
@component Main navbar component; handles view display based on user roles
Properties:
- `username String`: Empty string if auth. is disabled, otherwise the username as string
- `authlevel Number`: The current users authentication level
- `clusters [String]`: List of cluster names
- `roles [Number]`: Enum containing available roles
-->
<script> <script>
import { import {
Icon, Icon,
@ -10,13 +20,13 @@
DropdownToggle, DropdownToggle,
DropdownMenu, DropdownMenu,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import NavbarLinks from "./NavbarLinks.svelte"; import NavbarLinks from "./header/NavbarLinks.svelte";
import NavbarTools from "./NavbarTools.svelte"; import NavbarTools from "./header/NavbarTools.svelte";
export let username; // empty string if auth. is disabled, otherwise the username as string export let username;
export let authlevel; // Integer export let authlevel;
export let clusters; // array of names export let clusters;
export let roles; // Role Enum-Like export let roles;
let isOpen = false; let isOpen = false;
let screenSize; let screenSize;

View File

@ -1,11 +1,19 @@
<!--
@component Main single job display component; displays plots for every metric as well as various information
Properties:
- `username String`: Empty string if auth. is disabled, otherwise the username as string
- `authlevel Number`: The current users authentication level
- `clusters [String]`: List of cluster names
- `roles [Number]`: Enum containing available roles
-->
<script> <script>
import { import {
init, queryStore,
groupByScope, gql,
fetchMetricsStore, getContextClient
checkMetricDisabled, } from "@urql/svelte";
transformDataForRoofline,
} from "./utils.js";
import { import {
Row, Row,
Col, Col,
@ -19,30 +27,47 @@
Button, Button,
Icon, Icon,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import PlotTable from "./PlotTable.svelte";
import Metric from "./Metric.svelte";
import Polar from "./plots/Polar.svelte";
import Roofline from "./plots/Roofline.svelte";
import JobInfo from "./joblist/JobInfo.svelte";
import TagManagement from "./TagManagement.svelte";
import MetricSelection from "./MetricSelection.svelte";
import StatsTable from "./StatsTable.svelte";
import JobFootprint from "./JobFootprint.svelte";
import { getContext } from "svelte"; import { getContext } from "svelte";
import {
init,
groupByScope,
checkMetricDisabled,
transformDataForRoofline,
} from "./generic/utils.js";
import Metric from "./job/Metric.svelte";
import TagManagement from "./job/TagManagement.svelte";
import StatsTable from "./job/StatsTable.svelte";
import JobFootprint from "./generic/helper/JobFootprint.svelte";
import PlotTable from "./generic/PlotTable.svelte";
import Polar from "./generic/plots/Polar.svelte";
import Roofline from "./generic/plots/Roofline.svelte";
import JobInfo from "./generic/joblist/JobInfo.svelte";
import MetricSelection from "./generic/select/MetricSelection.svelte";
export let dbid; export let dbid;
export let authlevel; export let authlevel;
export let roles; export let roles;
const accMetrics = [ // Setup General
"acc_utilization",
"acc_mem_used", const ccconfig = getContext("cc-config")
"acc_power",
"nv_mem_util", let isMetricsSelectionOpen = false,
"nv_sm_clock", showFootprint = !!ccconfig[`job_view_showFootprint`],
"nv_temp", selectedMetrics = [],
]; selectedScopes = [];
let accNodeOnly;
let plots = {},
jobTags,
statsTable
let missingMetrics = [],
missingHosts = [],
somethingMissing = false;
// Setup GQL
// First: Add Job Query to init function -> Only requires DBID as argument, received via URL-ID
// Second: Trigger jobMetrics query with now received jobInfos (scopes: from job metadata, selectedMetrics: from config or all, job: from url-id)
const { query: initq } = init(` const { query: initq } = init(`
job(id: "${dbid}") { job(id: "${dbid}") {
@ -55,99 +80,100 @@
metaData, metaData,
userData { name, email }, userData { name, email },
concurrentJobs { items { id, jobId }, count, listQuery }, concurrentJobs { items { id, jobId }, count, listQuery },
flopsAnyAvg, memBwAvg, loadAvg footprint { name, stat, value }
} }
`); `);
const ccconfig = getContext("cc-config"), const client = getContextClient();
clusters = getContext("clusters"), const query = gql`
metrics = getContext("metrics"); query ($dbid: ID!, $selectedMetrics: [String!]!, $selectedScopes: [MetricScope!]!) {
jobMetrics(id: $dbid, metrics: $selectedMetrics, scopes: $selectedScopes) {
name
scope
metric {
unit {
prefix
base
}
timestep
statisticsSeries {
min
median
max
}
series {
hostname
id
data
statistics {
min
avg
max
}
}
}
}
}
`;
let isMetricsSelectionOpen = false, $: jobMetrics = queryStore({
selectedMetrics = [], client: client,
showFootprint = true, query: query,
isFetched = new Set(); variables: { dbid, selectedMetrics, selectedScopes },
const [jobMetrics, startFetching] = fetchMetricsStore(); });
function loadAllScopes() {
selectedScopes = [...selectedScopes, "socket", "core"]
jobMetrics = queryStore({
client: client,
query: query,
variables: { dbid, selectedMetrics, selectedScopes},
});
}
// Handle Job Query on Init -> is not executed anymore
getContext("on-init")(() => { getContext("on-init")(() => {
let job = $initq.data.job; let job = $initq.data.job;
if (!job) return; if (!job) return;
selectedMetrics = const pendingMetrics = [
ccconfig[`job_view_selectedMetrics:${job.cluster}`] ||
clusters
.find((c) => c.name == job.cluster)
.metricConfig.map((mc) => mc.name);
showFootprint =
ccconfig[`job_view_showFootprint`]
let toFetch = new Set([
"flops_any", "flops_any",
"mem_bw", "mem_bw",
...selectedMetrics, ...(ccconfig[`job_view_selectedMetrics:${job.cluster}`] ||
$initq.data.globalMetrics.reduce((names, gm) => {
if (gm.availability.find((av) => av.cluster === job.cluster)) {
names.push(gm.name);
}
return names;
}, [])
),
...(ccconfig[`job_view_polarPlotMetrics:${job.cluster}`] || ...(ccconfig[`job_view_polarPlotMetrics:${job.cluster}`] ||
ccconfig[`job_view_polarPlotMetrics`]), ccconfig[`job_view_polarPlotMetrics`]
),
...(ccconfig[`job_view_nodestats_selectedMetrics:${job.cluster}`] || ...(ccconfig[`job_view_nodestats_selectedMetrics:${job.cluster}`] ||
ccconfig[`job_view_nodestats_selectedMetrics`]), ccconfig[`job_view_nodestats_selectedMetrics`]
]); ),
];
// Select default Scopes to load: Check before if accelerator metrics are not on accelerator scope by default // Select default Scopes to load: Check before if any metric has accelerator scope by default
accNodeOnly = [...toFetch].some(function (m) { const accScopeDefault = [...pendingMetrics].some(function (m) {
if (accMetrics.includes(m)) { const cluster = $initq.data.clusters.find((c) => c.name == job.cluster);
const mc = metrics(job.cluster, m); const subCluster = cluster.subClusters.find((sc) => sc.name == job.subCluster);
return mc.scope !== "accelerator"; return subCluster.metricConfig.find((smc) => smc.name == m)?.scope === "accelerator";
} else {
return false;
}
}); });
if (job.numAcc === 0 || accNodeOnly === true) { const pendingScopes = ["node"]
// No Accels or Accels on Node Scope if (accScopeDefault) pendingScopes.push("accelerator")
startFetching( if (job.numNodes === 1) {
job, pendingScopes.push("socket")
[...toFetch], pendingScopes.push("core")
job.numNodes > 2 ? ["node"] : ["node", "socket", "core"],
);
} else {
// Accels and not on node scope
startFetching(
job,
[...toFetch],
job.numNodes > 2
? ["node", "accelerator"]
: ["node", "accelerator", "socket", "core"],
);
} }
isFetched = toFetch; selectedMetrics = [...new Set(pendingMetrics)];
selectedScopes = [...new Set(pendingScopes)];
}); });
const lazyFetchMoreMetrics = () => { // Interactive Document Title
let notYetFetched = new Set();
for (let m of selectedMetrics) {
if (!isFetched.has(m)) {
notYetFetched.add(m);
isFetched.add(m);
}
}
if (notYetFetched.size > 0)
startFetching(
$initq.data.job,
[...notYetFetched],
$initq.data.job.numNodes > 2 ? ["node"] : ["node", "core"],
);
};
// Fetch more data once required:
$: if ($initq.data && $jobMetrics.data && selectedMetrics)
lazyFetchMoreMetrics();
let plots = {},
jobTags,
statsTable,
jobFootprint;
$: document.title = $initq.fetching $: document.title = $initq.fetching
? "Loading..." ? "Loading..."
: $initq.error : $initq.error
@ -155,15 +181,15 @@
: `Job ${$initq.data.job.jobId} - ClusterCockpit`; : `Job ${$initq.data.job.jobId} - ClusterCockpit`;
// Find out what metrics or hosts are missing: // Find out what metrics or hosts are missing:
let missingMetrics = [], $: if ($initq?.data && $jobMetrics?.data?.jobMetrics) {
missingHosts = [],
somethingMissing = false;
$: if ($initq.data && $jobMetrics.data) {
let job = $initq.data.job, let job = $initq.data.job,
metrics = $jobMetrics.data.jobMetrics, metrics = $jobMetrics.data.jobMetrics,
metricNames = clusters metricNames = $initq.data.globalMetrics.reduce((names, gm) => {
.find((c) => c.name == job.cluster) if (gm.availability.find((av) => av.cluster === job.cluster)) {
.metricConfig.map((mc) => mc.name); names.push(gm.name);
}
return names;
}, []);
// Metric not found in JobMetrics && Metric not explicitly disabled in config or deselected: Was expected, but is Missing // Metric not found in JobMetrics && Metric not explicitly disabled in config or deselected: Was expected, but is Missing
missingMetrics = metricNames.filter( missingMetrics = metricNames.filter(
@ -192,6 +218,7 @@
somethingMissing = missingMetrics.length > 0 || missingHosts.length > 0; somethingMissing = missingMetrics.length > 0 || missingHosts.length > 0;
} }
// Helper
const orderAndMap = (grouped, selectedMetrics) => const orderAndMap = (grouped, selectedMetrics) =>
selectedMetrics.map((metric) => ({ selectedMetrics.map((metric) => ({
metric: metric, metric: metric,
@ -214,18 +241,14 @@
<Spinner secondary /> <Spinner secondary />
{/if} {/if}
</Col> </Col>
{#if $jobMetrics.data && showFootprint} {#if $initq.data && showFootprint}
{#key $jobMetrics.data} <Col>
<Col> <JobFootprint
<JobFootprint job={$initq.data.job}
bind:this={jobFootprint} />
job={$initq.data.job} </Col>
jobMetrics={$jobMetrics.data.jobMetrics}
/>
</Col>
{/key}
{/if} {/if}
{#if $jobMetrics.data && $initq.data} {#if $initq?.data && $jobMetrics?.data?.jobMetrics}
{#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}
<Col> <Col>
@ -270,27 +293,29 @@
`job_view_polarPlotMetrics:${$initq.data.job.cluster}` `job_view_polarPlotMetrics:${$initq.data.job.cluster}`
] || ccconfig[`job_view_polarPlotMetrics`]} ] || ccconfig[`job_view_polarPlotMetrics`]}
cluster={$initq.data.job.cluster} cluster={$initq.data.job.cluster}
subCluster={$initq.data.job.subCluster}
jobMetrics={$jobMetrics.data.jobMetrics} jobMetrics={$jobMetrics.data.jobMetrics}
/> />
</Col> </Col>
<Col> <Col>
<Roofline <Roofline
renderTime={true} renderTime={true}
cluster={clusters subCluster={$initq.data.clusters
.find((c) => c.name == $initq.data.job.cluster) .find((c) => c.name == $initq.data.job.cluster)
.subClusters.find((sc) => sc.name == $initq.data.job.subCluster)} .subClusters.find((sc) => sc.name == $initq.data.job.subCluster)}
data={transformDataForRoofline( data={transformDataForRoofline(
$jobMetrics.data.jobMetrics.find( $jobMetrics.data.jobMetrics.find(
(m) => m.name == "flops_any" && m.scope == "node", (m) => m.name == "flops_any" && m.scope == "node",
).metric, )?.metric,
$jobMetrics.data.jobMetrics.find( $jobMetrics.data.jobMetrics.find(
(m) => m.name == "mem_bw" && m.scope == "node", (m) => m.name == "mem_bw" && m.scope == "node",
).metric, )?.metric,
)} )}
/> />
</Col> </Col>
{:else} {:else}
<Col /> <Col />
<Spinner secondary />
<Col /> <Col />
{/if} {/if}
</Row> </Row>
@ -318,7 +343,7 @@
<Card body color="danger">{$jobMetrics.error.message}</Card> <Card body color="danger">{$jobMetrics.error.message}</Card>
{:else if $jobMetrics.fetching} {:else if $jobMetrics.fetching}
<Spinner secondary /> <Spinner secondary />
{:else if $jobMetrics.data && $initq.data} {:else if $initq?.data && $jobMetrics?.data?.jobMetrics}
<PlotTable <PlotTable
let:item let:item
let:width let:width
@ -332,9 +357,11 @@
{#if item.data} {#if item.data}
<Metric <Metric
bind:this={plots[item.metric]} bind:this={plots[item.metric]}
on:more-loaded={({ detail }) => statsTable.moreLoaded(detail)} on:load-all={loadAllScopes}
job={$initq.data.job} job={$initq.data.job}
metricName={item.metric} metricName={item.metric}
metricUnit={$initq.data.globalMetrics.find((gm) => gm.name == item.metric)?.unit}
nativeScope={$initq.data.globalMetrics.find((gm) => gm.name == item.metric)?.scope}
rawData={item.data.map((x) => x.metric)} rawData={item.data.map((x) => x.metric)}
scopes={item.data.map((x) => x.scope)} scopes={item.data.map((x) => x.scope)}
{width} {width}
@ -388,8 +415,8 @@
tab="Statistics Table" tab="Statistics Table"
active={!somethingMissing} active={!somethingMissing}
> >
{#if $jobMetrics.data} {#if $jobMetrics?.data?.jobMetrics}
{#key $jobMetrics.data} {#key $jobMetrics.data.jobMetrics}
<StatsTable <StatsTable
bind:this={statsTable} bind:this={statsTable}
job={$initq.data.job} job={$initq.data.job}

View File

@ -1,268 +0,0 @@
<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,
};
// Job_Exclusivity does not matter, only aggregation
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 defaultThresholds;
}
}
</script>
<script>
import { getContext } from "svelte";
import {
Card,
CardHeader,
CardTitle,
CardBody,
Progress,
Icon,
Tooltip,
} from "@sveltestrap/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 // GPU
? ["acc_utilization", "acc_mem_used", "nv_sm_clock", "nv_mem_util"] // Shared
: ["acc_utilization", "acc_mem_used", "nv_sm_clock", "nv_mem_util"] // Exclusive
: (job.exclusive !== 1) // CPU Only
? ["flops_any", "mem_used"] // Shared
: ["cpu_load", "flops_any", "mem_used", "mem_bw"]; // Exclusive
const footprintData = footprintMetrics.map((fm) => {
// 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);
// Value: Primarily use backend sourced avgs from job.*, secondarily calculate/read from metricdata
// Exclusivity does not matter
let mv = 0.0;
if (fmc.aggregation === "avg") {
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 Avg from jobMetrics
const jm = jobMetrics.find((jm) => jm.name === fm && jm.scope === "node");
if (jm?.metric?.statisticsSeries) {
const noNan = jm.metric.statisticsSeries.median.filter(function (val) {
return val != null;
});
mv = round(mean(noNan), 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 if (fmc.aggregation === "sum") {
// Calculate Sum from jobMetrics: Sum all node averages
const jm = jobMetrics.find((jm) => jm.name === fm && jm.scope === "node");
if (jm?.metric?.series?.length > 1) { // More than 1 node
const avgs = jm.metric.series.map((jms) => jms.statistics.avg);
mv = round(avgs.reduce((a, b) => a + b, 0));
} else if (jm?.metric?.series) {
mv = round(jm.metric.series[0].statistics.avg, 2);
}
} else {
console.warn(
"Missing or unkown aggregation mode (sum/avg) for metric:",
metricConfig,
);
}
// 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, notify levels triggered if mean > threshold
switch (level) {
case "peak":
if (metric === "mem_used")
return false; // mem_used over peak -> return false to trigger impact -1
else return mean <= thresholds.peak && mean > thresholds.normal;
case "alert":
if (metric === "mem_used")
return mean <= thresholds.peak && mean >= thresholds.alert;
else return mean <= thresholds.alert && mean >= 0;
case "caution":
if (metric === "mem_used")
return mean < thresholds.alert && mean >= thresholds.caution;
else return mean <= thresholds.caution && mean > thresholds.alert;
case "normal":
if (metric === "mem_used")
return mean < thresholds.caution && mean >= 0;
else return mean <= thresholds.normal && mean > thresholds.caution;
default:
return false;
}
}
</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

@ -1,6 +1,14 @@
<script> <!--
@component Main job list component
Properties:
- `filterPresets Object?`: Optional predefined filter values [Default: {}]
- `authlevel Number`: The current users authentication level
- `roles [Number]`: Enum containing available roles
-->
<script>
import { onMount, getContext } from "svelte"; import { onMount, getContext } from "svelte";
import { init } from "./utils.js";
import { import {
Row, Row,
Col, Col,
@ -9,12 +17,13 @@
Card, Card,
Spinner, Spinner,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import Filters from "./filters/Filters.svelte"; import { init } from "./generic/utils.js";
import JobList from "./joblist/JobList.svelte"; import Filters from "./generic/Filters.svelte";
import Refresher from "./joblist/Refresher.svelte"; import JobList from "./generic/JobList.svelte";
import Sorting from "./joblist/SortSelection.svelte"; import TextFilter from "./generic/helper/TextFilter.svelte";
import MetricSelection from "./MetricSelection.svelte"; import Refresher from "./generic/helper/Refresher.svelte";
import TextFilter from "./filters/TextFilter.svelte"; import Sorting from "./generic/select/SortSelection.svelte";
import MetricSelection from "./generic/select/MetricSelection.svelte";
const { query: initq } = init(); const { query: initq } = init();
@ -27,7 +36,7 @@
let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
let jobList, let jobList,
matchedJobs = null; matchedJobs = null;
let sorting = { field: "startTime", order: "DESC" }, let sorting = { field: "startTime", type: "col", order: "DESC" },
isSortingOpen = false, isSortingOpen = false,
isMetricsSelectionOpen = false; isMetricsSelectionOpen = false;
let metrics = filterPresets.cluster let metrics = filterPresets.cluster
@ -43,7 +52,7 @@
// The filterPresets are handled by the Filters component, // The filterPresets are handled by the Filters component,
// so we need to wait for it to be ready before we can start a query. // so we need to wait for it to be ready before we can start a query.
// This is also why JobList component starts out with a paused query. // This is also why JobList component starts out with a paused query.
onMount(() => filterComponent.update()); onMount(() => filterComponent.updateFilters());
</script> </script>
<Row> <Row>
@ -77,11 +86,11 @@
<Filters <Filters
{filterPresets} {filterPresets}
bind:this={filterComponent} bind:this={filterComponent}
on:update={({ detail }) => { on:update-filters={({ detail }) => {
selectedCluster = detail.filters[0]?.cluster selectedCluster = detail.filters[0]?.cluster
? detail.filters[0].cluster.eq ? detail.filters[0].cluster.eq
: null; : null;
jobList.update(detail.filters); jobList.queryJobs(detail.filters);
}} }}
/> />
</Col> </Col>
@ -91,11 +100,14 @@
{presetProject} {presetProject}
bind:authlevel bind:authlevel
bind:roles bind:roles
on:update={({ detail }) => filterComponent.update(detail)} on:set-filter={({ detail }) => filterComponent.updateFilters(detail)}
/> />
</Col> </Col>
<Col xs="2"> <Col xs="2">
<Refresher on:reload={() => jobList.refresh()} /> <Refresher on:refresh={() => {
jobList.refreshJobs()
jobList.refreshAllMetrics()
}} />
</Col> </Col>
</Row> </Row>
<br /> <br />
@ -119,5 +131,5 @@
bind:metrics bind:metrics
bind:isOpen={isMetricsSelectionOpen} bind:isOpen={isMetricsSelectionOpen}
bind:showFootprint bind:showFootprint
view="list" footprintSelect={true}
/> />

View File

@ -1,9 +1,13 @@
<!-- <!--
@component List of users or projects @component Main component for listing users or projects
Properties:
- `type String?`: The type of list ['USER' || 'PROJECT']
- `filterPresets Object?`: Optional predefined filter values [Default: {}]
--> -->
<script> <script>
import { onMount } from "svelte"; import { onMount } from "svelte";
import { init } from "./utils.js";
import { import {
Row, Row,
Col, Col,
@ -15,9 +19,17 @@
InputGroup, InputGroup,
Input, Input,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import Filters from "./filters/Filters.svelte"; import {
import { queryStore, gql, getContextClient } from "@urql/svelte"; queryStore,
import { scramble, scrambleNames } from "./joblist/JobInfo.svelte"; gql,
getContextClient,
} from "@urql/svelte";
import {
init,
scramble,
scrambleNames,
} from "./generic/utils.js";
import Filters from "./generic/Filters.svelte";
const {} = init(); const {} = init();
@ -89,7 +101,7 @@
return stats.filter((u) => u.id.includes(nameFilter)).sort(cmp); return stats.filter((u) => u.id.includes(nameFilter)).sort(cmp);
} }
onMount(() => filterComponent.update()); onMount(() => filterComponent.updateFilters());
</script> </script>
<Row> <Row>
@ -113,7 +125,7 @@
{filterPresets} {filterPresets}
startTimeQuickSelect={true} startTimeQuickSelect={true}
menuText="Only {type.toLowerCase()}s with jobs that match the filters will show up" menuText="Only {type.toLowerCase()}s with jobs that match the filters will show up"
on:update={({ detail }) => { on:update-filters={({ detail }) => {
jobFilters = detail.filters; jobFilters = detail.filters;
}} }}
/> />

View File

@ -1,5 +1,15 @@
<!--
@component System-View subcomponent; renders all current metrics for specified node
Properties:
- `cluster String`: Currently selected cluster
- `hostname String`: Currently selected host (== node)
- `from Date?`: Custom Time Range selection 'from' [Default: null]
- `to Date?`: Custom Time Range selection 'to' [Default: null]
-->
<script> <script>
import { init, checkMetricDisabled } from "./utils.js"; import { getContext } from "svelte";
import { import {
Row, Row,
Col, Col,
@ -9,12 +19,19 @@
Spinner, Spinner,
Card, Card,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { queryStore, gql, getContextClient } from "@urql/svelte"; import {
import TimeSelection from "./filters/TimeSelection.svelte"; queryStore,
import Refresher from "./joblist/Refresher.svelte"; gql,
import PlotTable from "./PlotTable.svelte"; getContextClient,
import MetricPlot from "./plots/MetricPlot.svelte"; } from "@urql/svelte";
import { getContext } from "svelte"; import {
init,
checkMetricDisabled,
} from "./generic/utils.js";
import PlotTable from "./generic/PlotTable.svelte";
import MetricPlot from "./generic/plots/MetricPlot.svelte";
import TimeSelection from "./generic/select/TimeSelection.svelte";
import Refresher from "./generic/helper/Refresher.svelte";
export let cluster; export let cluster;
export let hostname; export let hostname;
@ -29,6 +46,8 @@
from.setMinutes(from.getMinutes() - 30); from.setMinutes(from.getMinutes() - 30);
} }
const initialized = getContext("initialized")
const globalMetrics = getContext("globalMetrics")
const ccconfig = getContext("cc-config"); const ccconfig = getContext("cc-config");
const clusters = getContext("clusters"); const clusters = getContext("clusters");
const client = getContextClient(); const client = getContextClient();
@ -74,15 +93,11 @@
let itemsPerPage = ccconfig.plot_list_jobsPerPage; let itemsPerPage = ccconfig.plot_list_jobsPerPage;
let page = 1; let page = 1;
let paging = { itemsPerPage, page }; let paging = { itemsPerPage, page };
let sorting = { field: "startTime", order: "DESC" }; let sorting = { field: "startTime", type: "col", order: "DESC" };
$: filter = [ $: filter = [
{ cluster: { eq: cluster } }, { cluster: { eq: cluster } },
{ node: { contains: hostname } }, { node: { contains: hostname } },
{ state: ["running"] }, { state: ["running"] },
// {startTime: {
// from: from.toISOString(),
// to: to.toISOString()
// }}
]; ];
const nodeJobsQuery = gql` const nodeJobsQuery = gql`
@ -92,10 +107,6 @@
$paging: PageRequest! $paging: PageRequest!
) { ) {
jobs(filter: $filter, order: $sorting, page: $paging) { jobs(filter: $filter, order: $sorting, page: $paging) {
# items {
# id
# jobId
# }
count count
} }
} }
@ -107,26 +118,16 @@
variables: { paging, sorting, filter }, variables: { paging, sorting, filter },
}); });
let metricUnits = {}; let systemUnits = {};
$: if ($nodeMetricsData.data) { function loadUnits(isInitialized) {
let thisCluster = clusters.find((c) => c.name == cluster); if (!isInitialized) return
if (thisCluster) { const systemMetrics = [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster))]
for (let metric of thisCluster.metricConfig) { for (let sm of systemMetrics) {
if (metric.unit.prefix || metric.unit.base) { systemUnits[sm.name] = (sm?.unit?.prefix ? sm.unit.prefix : "") + (sm?.unit?.base ? sm.unit.base : "")
metricUnits[metric.name] =
"(" +
(metric.unit.prefix ? metric.unit.prefix : "") +
(metric.unit.base ? metric.unit.base : "") +
")";
} else {
// If no unit defined: Omit Unit Display
metricUnits[metric.name] = "";
}
}
} }
} }
const dateToUnixEpoch = (rfc3339) => Math.floor(Date.parse(rfc3339) / 1000); $: loadUnits($initialized)
</script> </script>
<Row> <Row>
@ -157,7 +158,7 @@
</Col> </Col>
<Col> <Col>
<Refresher <Refresher
on:reload={() => { on:refresh={() => {
const diff = Date.now() - to; const diff = Date.now() - to;
from = new Date(from.getTime() + diff); from = new Date(from.getTime() + diff);
to = new Date(to.getTime() + diff); to = new Date(to.getTime() + diff);
@ -195,7 +196,7 @@
> >
<h4 style="text-align: center; padding-top:15px;"> <h4 style="text-align: center; padding-top:15px;">
{item.name} {item.name}
{metricUnits[item.name]} {systemUnits[item.name] ? "(" + systemUnits[item.name] + ")" : ""}
</h4> </h4>
{#if item.disabled === false && item.metric} {#if item.disabled === false && item.metric}
<MetricPlot <MetricPlot

View File

@ -1,9 +1,12 @@
<script> <!--
@component Main cluster status view component; renders current system-usage information
Properties:
- `cluster String`: The cluster to show status information for
-->
<script>
import { getContext } from "svelte"; import { getContext } from "svelte";
import Refresher from "./joblist/Refresher.svelte";
import Roofline from "./plots/Roofline.svelte";
import Pie, { colors } from "./plots/Pie.svelte";
import Histogram from "./plots/Histogram.svelte";
import { import {
Row, Row,
Col, Col,
@ -17,20 +20,24 @@
Icon, Icon,
Button, Button,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import {
init,
convert2uplot,
transformPerNodeDataForRoofline,
} from "./utils.js";
import { scaleNumbers } from "./units.js";
import { import {
queryStore, queryStore,
gql, gql,
getContextClient, getContextClient,
mutationStore, mutationStore,
} from "@urql/svelte"; } from "@urql/svelte";
import PlotTable from "./PlotTable.svelte"; import {
import HistogramSelection from "./HistogramSelection.svelte"; init,
convert2uplot,
transformPerNodeDataForRoofline,
} from "./generic/utils.js";
import { scaleNumbers } from "./generic/units.js";
import PlotTable from "./generic/PlotTable.svelte";
import Roofline from "./generic/plots/Roofline.svelte";
import Pie, { colors } from "./generic/plots/Pie.svelte";
import Histogram from "./generic/plots/Histogram.svelte";
import Refresher from "./generic/helper/Refresher.svelte";
import HistogramSelection from "./generic/select/HistogramSelection.svelte";
const { query: initq } = init(); const { query: initq } = init();
const ccconfig = getContext("cc-config"); const ccconfig = getContext("cc-config");
@ -146,7 +153,7 @@
`, `,
variables: { variables: {
cluster: cluster, cluster: cluster,
metrics: ["flops_any", "mem_bw"], metrics: ["flops_any", "mem_bw"], // Fixed names for roofline and status bars
from: from.toISOString(), from: from.toISOString(),
to: to.toISOString(), to: to.toISOString(),
filter: [{ state: ["running"] }, { cluster: { eq: cluster } }], filter: [{ state: ["running"] }, { cluster: { eq: cluster } }],
@ -331,7 +338,7 @@
<Col class="mt-2 mt-md-0"> <Col class="mt-2 mt-md-0">
<Refresher <Refresher
initially={120} initially={120}
on:reload={() => { on:refresh={() => {
from = new Date(Date.now() - 5 * 60 * 1000); from = new Date(Date.now() - 5 * 60 * 1000);
to = new Date(Date.now()); to = new Date(Date.now());
}} }}
@ -442,7 +449,7 @@
allowSizeChange={true} allowSizeChange={true}
width={plotWidths[i] - 10} width={plotWidths[i] - 10}
height={300} height={300}
cluster={subCluster} subCluster={subCluster}
data={transformPerNodeDataForRoofline( data={transformPerNodeDataForRoofline(
$mainQuery.data.nodeMetrics.filter( $mainQuery.data.nodeMetrics.filter(
(data) => data.subCluster == subCluster.name, (data) => data.subCluster == subCluster.name,

View File

@ -1,6 +1,14 @@
<!--
@component Main cluster metric status view component; renders current state of metrics / nodes
Properties:
- `cluster String`: The cluster to show status information for
- `from Date?`: Custom Time Range selection 'from' [Default: null]
- `to Date?`: Custom Time Range selection 'to' [Default: null]
-->
<script> <script>
import { init, checkMetricDisabled } from "./utils.js"; import { getContext } from "svelte";
import Refresher from "./joblist/Refresher.svelte";
import { import {
Row, Row,
Col, Col,
@ -11,11 +19,19 @@
Spinner, Spinner,
Card, Card,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { queryStore, gql, getContextClient } from "@urql/svelte"; import {
import TimeSelection from "./filters/TimeSelection.svelte"; queryStore,
import PlotTable from "./PlotTable.svelte"; gql,
import MetricPlot from "./plots/MetricPlot.svelte"; getContextClient,
import { getContext } from "svelte"; } from "@urql/svelte";
import {
init,
checkMetricDisabled,
} from "./generic/utils.js";
import PlotTable from "./generic/PlotTable.svelte";
import MetricPlot from "./generic/plots/MetricPlot.svelte";
import TimeSelection from "./generic/select/TimeSelection.svelte";
import Refresher from "./generic/helper/Refresher.svelte";
export let cluster; export let cluster;
export let from = null; export let from = null;
@ -29,9 +45,10 @@
from.setMinutes(from.getMinutes() - 30); from.setMinutes(from.getMinutes() - 30);
} }
const clusters = getContext("clusters"); const initialized = getContext("initialized");
const ccconfig = getContext("cc-config"); const ccconfig = getContext("cc-config");
const metricConfig = getContext("metrics"); const clusters = getContext("clusters");
const globalMetrics = getContext("globalMetrics");
let plotHeight = 300; let plotHeight = 300;
let hostnameFilter = ""; let hostnameFilter = "";
@ -80,24 +97,18 @@
}, },
}); });
let metricUnits = {}; let systemMetrics = [];
$: if ($nodesQuery.data) { let systemUnits = {};
let thisCluster = clusters.find((c) => c.name == cluster); function loadMetrics(isInitialized) {
if (thisCluster) { if (!isInitialized) return
for (let metric of thisCluster.metricConfig) { systemMetrics = [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster))]
if (metric.unit.prefix || metric.unit.base) { for (let sm of systemMetrics) {
metricUnits[metric.name] = systemUnits[sm.name] = (sm?.unit?.prefix ? sm.unit.prefix : "") + (sm?.unit?.base ? sm.unit.base : "")
"(" +
(metric.unit.prefix ? metric.unit.prefix : "") +
(metric.unit.base ? metric.unit.base : "") +
")";
} else {
// If no unit defined: Omit Unit Display
metricUnits[metric.name] = "";
}
}
} }
} }
$: loadMetrics($initialized)
</script> </script>
<Row> <Row>
@ -108,7 +119,7 @@
{:else} {:else}
<Col> <Col>
<Refresher <Refresher
on:reload={() => { on:refresh={() => {
const diff = Date.now() - to; const diff = Date.now() - to;
from = new Date(from.getTime() + diff); from = new Date(from.getTime() + diff);
to = new Date(to.getTime() + diff); to = new Date(to.getTime() + diff);
@ -123,9 +134,9 @@
<InputGroupText><Icon name="graph-up" /></InputGroupText> <InputGroupText><Icon name="graph-up" /></InputGroupText>
<InputGroupText>Metric</InputGroupText> <InputGroupText>Metric</InputGroupText>
<select class="form-select" bind:value={selectedMetric}> <select class="form-select" bind:value={selectedMetric}>
{#each clusters.find((c) => c.name == cluster).metricConfig as metric} {#each systemMetrics as metric}
<option value={metric.name} <option value={metric.name}
>{metric.name} {metricUnits[metric.name]}</option >{metric.name} {systemUnits[metric.name] ? "("+systemUnits[metric.name]+")" : ""}</option
> >
{/each} {/each}
</select> </select>

View File

@ -1,6 +1,13 @@
<!--
@component Main user jobs list display component; displays job list and additional information for a given user
Properties:
- `user Object`: The GraphQL user object
- `filterPresets Object`: Optional predefined filter values
-->
<script> <script>
import { onMount, getContext } from "svelte"; import { onMount, getContext } from "svelte";
import { init, convert2uplot } from "./utils.js";
import { import {
Table, Table,
Row, Row,
@ -10,17 +17,26 @@
Card, Card,
Spinner, Spinner,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { queryStore, gql, getContextClient } from "@urql/svelte"; import {
import Filters from "./filters/Filters.svelte"; queryStore,
import TextFilter from "./filters/TextFilter.svelte" gql,
import JobList from "./joblist/JobList.svelte"; getContextClient,
import Sorting from "./joblist/SortSelection.svelte"; } from "@urql/svelte";
import Refresher from "./joblist/Refresher.svelte"; import {
import Histogram from "./plots/Histogram.svelte"; init,
import MetricSelection from "./MetricSelection.svelte"; convert2uplot,
import HistogramSelection from "./HistogramSelection.svelte"; scramble,
import PlotTable from "./PlotTable.svelte"; scrambleNames,
import { scramble, scrambleNames } from "./joblist/JobInfo.svelte"; } from "./generic/utils.js";
import JobList from "./generic/JobList.svelte";
import Filters from "./generic/Filters.svelte";
import PlotTable from "./generic/PlotTable.svelte";
import Histogram from "./generic/plots/Histogram.svelte";
import MetricSelection from "./generic/select/MetricSelection.svelte";
import HistogramSelection from "./generic/select/HistogramSelection.svelte";
import Sorting from "./generic/select/SortSelection.svelte";
import TextFilter from "./generic/helper/TextFilter.svelte"
import Refresher from "./generic/helper/Refresher.svelte";
const { query: initq } = init(); const { query: initq } = init();
@ -32,7 +48,7 @@
let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
let jobList; let jobList;
let jobFilters = []; let jobFilters = [];
let sorting = { field: "startTime", order: "DESC" }, let sorting = { field: "startTime", type: "col", order: "DESC" },
isSortingOpen = false; isSortingOpen = false;
let metrics = ccconfig.plot_list_selectedMetrics, let metrics = ccconfig.plot_list_selectedMetrics,
isMetricsSelectionOpen = false; isMetricsSelectionOpen = false;
@ -70,6 +86,7 @@
histMetrics { histMetrics {
metric metric
unit unit
stat
data { data {
min min
max max
@ -83,7 +100,7 @@
variables: { jobFilters, metricsInHistograms }, variables: { jobFilters, metricsInHistograms },
}); });
onMount(() => filterComponent.update()); onMount(() => filterComponent.updateFilters());
</script> </script>
<Row> <Row>
@ -123,22 +140,25 @@
{filterPresets} {filterPresets}
startTimeQuickSelect={true} startTimeQuickSelect={true}
bind:this={filterComponent} bind:this={filterComponent}
on:update={({ detail }) => { on:update-filters={({ detail }) => {
jobFilters = [...detail.filters, { user: { eq: user.username } }]; jobFilters = [...detail.filters, { user: { eq: user.username } }];
selectedCluster = jobFilters[0]?.cluster selectedCluster = jobFilters[0]?.cluster
? jobFilters[0].cluster.eq ? jobFilters[0].cluster.eq
: null; : null;
jobList.update(jobFilters); jobList.queryJobs(jobFilters);
}} }}
/> />
</Col> </Col>
<Col xs="auto" style="margin-left: auto;"> <Col xs="auto" style="margin-left: auto;">
<TextFilter <TextFilter
on:update={({ detail }) => filterComponent.update(detail)} on:set-filter={({ detail }) => filterComponent.updateFilters(detail)}
/> />
</Col> </Col>
<Col xs="auto"> <Col xs="auto">
<Refresher on:reload={() => jobList.refresh()} /> <Refresher on:refresh={() => {
jobList.refreshJobs()
jobList.refreshAllMetrics()
}} />
</Col> </Col>
</Row> </Row>
<br /> <br />
@ -245,7 +265,7 @@
usesBins={true} usesBins={true}
{width} {width}
height={250} height={250}
title="Distribution of '{item.metric}' averages" title="Distribution of '{item.metric} ({item.stat})' footprints"
xlabel={`${item.metric} bin maximum ${item?.unit ? `[${item.unit}]` : ``}`} xlabel={`${item.metric} bin maximum ${item?.unit ? `[${item.unit}]` : ``}`}
xunit={item.unit} xunit={item.unit}
ylabel="Number of Jobs" ylabel="Number of Jobs"
@ -272,7 +292,7 @@
bind:metrics bind:metrics
bind:isOpen={isMetricsSelectionOpen} bind:isOpen={isMetricsSelectionOpen}
bind:showFootprint bind:showFootprint
view="list" footprintSelect={true}
/> />
<HistogramSelection <HistogramSelection

View File

@ -1,65 +0,0 @@
<script>
import { Icon, InputGroup, InputGroupText } from "@sveltestrap/sveltestrap";
export let timeseriesPlots;
let windowSize = 100; // Goes from 0 to 100
let windowPosition = 50; // Goes from 0 to 100
function updatePlots() {
let ws = windowSize / (100 * 2),
wp = windowPosition / 100;
let from = wp - ws,
to = wp + ws;
Object.values(timeseriesPlots).forEach((plot) =>
plot.setTimeRange(from, to),
);
}
// Rendering a big job can take a long time, so we
// throttle the rerenders to every 100ms here.
let timeoutId = null;
function requestUpdatePlots() {
if (timeoutId != null) window.cancelAnimationFrame(timeoutId);
timeoutId = window.requestAnimationFrame(() => {
updatePlots();
timeoutId = null;
}, 100);
}
$: requestUpdatePlots(windowSize, windowPosition);
</script>
<div>
<InputGroup>
<InputGroupText>
<Icon name="zoom-in" />
</InputGroupText>
<InputGroupText>
Window Size:
<input
style="margin: 0em 0em 0em 1em"
type="range"
bind:value={windowSize}
min="1"
max="100"
step="1"
/>
<span style="width: 5em;">
({windowSize}%)
</span>
</InputGroupText>
<InputGroupText>
Window Position:
<input
style="margin: 0em 0em 0em 1em"
type="range"
bind:value={windowPosition}
min="0"
max="100"
step="1"
/>
</InputGroupText>
</InputGroup>
</div>

View File

@ -1,3 +1,12 @@
<!--
@component Analysis-View subcomponent; allows selection for normalized histograms and scatterplots
Properties:
- `availableMetrics [String]`: Available metrics in selected cluster
- `metricsInHistograms [String]`: The currently selected metrics to display as histogram
- `metricsInScatterplots [[String, String]]`: The currently selected metrics to display as scatterplot
-->
<script> <script>
import { import {
Modal, Modal,
@ -41,7 +50,6 @@
}).subscribe((res) => { }).subscribe((res) => {
if (res.fetching === false && res.error) { if (res.fetching === false && res.error) {
throw res.error; throw res.error;
// console.log('Error on subscription: ' + res.error)
} }
}); });
} }

View File

@ -1,3 +1,7 @@
<!--
@component Admin settings wrapper
-->
<script> <script>
import { Row, Col } from "@sveltestrap/sveltestrap"; import { Row, Col } from "@sveltestrap/sveltestrap";
import { onMount } from "svelte"; import { onMount } from "svelte";

View File

@ -1,3 +1,11 @@
<!--
@component User settings wrapper
Properties:
- `username String!`: Empty string if auth. is disabled, otherwise the username as string
- `isApi Bool!`: Is currently logged in user api authority
-->
<script> <script>
import { getContext } from "svelte"; import { getContext } from "svelte";
import UserOptions from "./user/UserOptions.svelte"; import UserOptions from "./user/UserOptions.svelte";
@ -23,7 +31,6 @@
popMessage(text, target, "#048109"); popMessage(text, target, "#048109");
} else { } else {
let text = await res.text(); let text = await res.text();
// console.log(res.statusText)
throw new Error("Response Code " + res.status + "-> " + text); throw new Error("Response Code " + res.status + "-> " + text);
} }
} catch (err) { } catch (err) {
@ -42,6 +49,6 @@
} }
</script> </script>
<UserOptions config={ccconfig} {username} {isApi} bind:message bind:displayMessage on:update={(e) => handleSettingSubmit(e)}/> <UserOptions config={ccconfig} {username} {isApi} bind:message bind:displayMessage on:update-config={(e) => handleSettingSubmit(e)}/>
<PlotRenderOptions config={ccconfig} bind:message bind:displayMessage on:update={(e) => handleSettingSubmit(e)}/> <PlotRenderOptions config={ccconfig} bind:message bind:displayMessage on:update-config={(e) => handleSettingSubmit(e)}/>
<PlotColorScheme config={ccconfig} bind:message bind:displayMessage on:update={(e) => handleSettingSubmit(e)}/> <PlotColorScheme config={ccconfig} bind:message bind:displayMessage on:update-config={(e) => handleSettingSubmit(e)}/>

View File

@ -1,4 +1,14 @@
<script> <!--
@component User creation form card
Properties:
- `roles [String]!`: List of roles used in app as strings
Events:
- `reload`: Trigger upstream reload of user list after user creation
-->
<script>
import { Button, Card, CardTitle } from "@sveltestrap/sveltestrap"; import { Button, Card, CardTitle } from "@sveltestrap/sveltestrap";
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
@ -8,7 +18,7 @@
let message = { msg: "", color: "#d63384" }; let message = { msg: "", color: "#d63384" };
let displayMessage = false; let displayMessage = false;
export let roles = []; export let roles;
async function handleUserSubmit() { async function handleUserSubmit() {
let form = document.querySelector("#create-user-form"); let form = document.querySelector("#create-user-form");
@ -23,7 +33,6 @@
form.reset(); form.reset();
} else { } else {
let text = await res.text(); let text = await res.text();
// console.log(res.statusText)
throw new Error("Response Code " + res.status + "-> " + text); throw new Error("Response Code " + res.status + "-> " + text);
} }
} catch (err) { } catch (err) {

View File

@ -1,3 +1,10 @@
<!--
@component User managed project edit form card
Events:
- `reload`: Trigger upstream reload of user list after project update
-->
<script> <script>
import { Card, CardTitle, CardBody } from "@sveltestrap/sveltestrap"; import { Card, CardTitle, CardBody } from "@sveltestrap/sveltestrap";
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
@ -32,7 +39,6 @@
reloadUserList(); reloadUserList();
} else { } else {
let text = await res.text(); let text = await res.text();
// console.log(res.statusText)
throw new Error("Response Code " + res.status + "-> " + text); throw new Error("Response Code " + res.status + "-> " + text);
} }
} catch (err) { } catch (err) {
@ -64,7 +70,6 @@
reloadUserList(); reloadUserList();
} else { } else {
let text = await res.text(); let text = await res.text();
// console.log(res.statusText)
throw new Error("Response Code " + res.status + "-> " + text); throw new Error("Response Code " + res.status + "-> " + text);
} }
} catch (err) { } catch (err) {

View File

@ -1,3 +1,13 @@
<!--
@component User role edit form card
Properties:
- `roles [String]!`: List of roles used in app as strings
Events:
- `reload`: Trigger upstream reload of user list after role edit
-->
<script> <script>
import { Card, CardTitle, CardBody } from "@sveltestrap/sveltestrap"; import { Card, CardTitle, CardBody } from "@sveltestrap/sveltestrap";
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
@ -8,7 +18,7 @@
let message = { msg: "", color: "#d63384" }; let message = { msg: "", color: "#d63384" };
let displayMessage = false; let displayMessage = false;
export let roles = []; export let roles;
async function handleAddRole() { async function handleAddRole() {
const username = document.querySelector("#role-username").value; const username = document.querySelector("#role-username").value;
@ -34,7 +44,6 @@
reloadUserList(); reloadUserList();
} else { } else {
let text = await res.text(); let text = await res.text();
// console.log(res.statusText)
throw new Error("Response Code " + res.status + "-> " + text); throw new Error("Response Code " + res.status + "-> " + text);
} }
} catch (err) { } catch (err) {
@ -66,7 +75,6 @@
reloadUserList(); reloadUserList();
} else { } else {
let text = await res.text(); let text = await res.text();
// console.log(res.statusText)
throw new Error("Response Code " + res.status + "-> " + text); throw new Error("Response Code " + res.status + "-> " + text);
} }
} catch (err) { } catch (err) {

View File

@ -1,3 +1,7 @@
<!--
@component Admin option select card
-->
<script> <script>
import { onMount } from "svelte"; import { onMount } from "svelte";
import { Card, CardBody, CardTitle } from "@sveltestrap/sveltestrap"; import { Card, CardBody, CardTitle } from "@sveltestrap/sveltestrap";

View File

@ -1,3 +1,13 @@
<!--
@component User management table
Properties:
- `users [Object]?`: List of users
Events:
- `reload`: Trigger upstream reload of user list
-->
<script> <script>
import { import {
Button, Button,

View File

@ -1,6 +1,14 @@
<!--
@component User data row for table
Properties:
- `user Object!`: User Object
- {username: String, name: String, roles: [String], projects: String, email: String}
-->
<script> <script>
import { Button } from "@sveltestrap/sveltestrap"; import { Button } from "@sveltestrap/sveltestrap";
import { fetchJwt } from "../../utils.js" import { fetchJwt } from "../../generic/utils.js"
export let user; export let user;

View File

@ -1,3 +1,15 @@
<!--
@component Plot color scheme selection for users
Properties:
- `config Object`: Current cc-config
- `message Object`: Message to display on success or error
- `displayMessage Bool`: If to display message content
Events:
- `update-config, {selector: String, target: String}`: Trigger upstream update of the config option
-->
<script> <script>
import { import {
Table, Table,
@ -15,7 +27,7 @@
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
function updateSetting(selector, target) { function updateSetting(selector, target) {
dispatch('update', { dispatch('update-config', {
selector: selector, selector: selector,
target: target target: target
}); });

View File

@ -1,4 +1,16 @@
<script> <!--
@component Plot render option selection for users
Properties:
- `config Object`: Current cc-config
- `message Object`: Message to display on success or error
- `displayMessage Bool`: If to display message content
Events:
- `update-config, {selector: String, target: String}`: Trigger upstream update of the config option
-->
<script>
import { import {
Button, Button,
Row, Row,
@ -15,7 +27,7 @@
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
function updateSetting(selector, target) { function updateSetting(selector, target) {
dispatch('update', { dispatch('update-config', {
selector: selector, selector: selector,
target: target target: target
}); });

View File

@ -1,3 +1,17 @@
<!--
@component General option selection for users
Properties:
- `config Object`: Current cc-config
- `message Object`: Message to display on success or error
- `displayMessage Bool`: If to display message content
- `username String!`: Empty string if auth. is disabled, otherwise the username as string
- `isApi Bool!`: Is currently logged in user api authority
Events:
- `update-config, {selector: String, target: String}`: Trigger upstream update of the config option
-->
<script> <script>
import { import {
Button, Button,
@ -9,7 +23,7 @@
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import { fetchJwt } from "../../utils.js"; import { fetchJwt } from "../../generic/utils.js";
export let config; export let config;
export let message; export let message;
@ -37,7 +51,7 @@
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
function updateSetting(selector, target) { function updateSetting(selector, target) {
dispatch('update', { dispatch('update-config', {
selector: selector, selector: selector,
target: target target: target
}); });

View File

@ -1,137 +0,0 @@
<script>
import { createEventDispatcher, getContext } from "svelte";
import {
Button,
Modal,
ModalBody,
ModalHeader,
ModalFooter,
} from "@sveltestrap/sveltestrap";
import DoubleRangeSlider from "./DoubleRangeSlider.svelte";
const clusters = getContext("clusters"),
initialized = getContext("initialized"),
dispatch = createEventDispatcher();
export let cluster = null;
export let isModified = false;
export let isOpen = false;
export let stats = [];
let statistics = [
{
field: "flopsAnyAvg",
text: "FLOPs (Avg.)",
metric: "flops_any",
from: 0,
to: 0,
peak: 0,
enabled: false,
},
{
field: "memBwAvg",
text: "Mem. Bw. (Avg.)",
metric: "mem_bw",
from: 0,
to: 0,
peak: 0,
enabled: false,
},
{
field: "loadAvg",
text: "Load (Avg.)",
metric: "cpu_load",
from: 0,
to: 0,
peak: 0,
enabled: false,
},
{
field: "memUsedMax",
text: "Mem. used (Max.)",
metric: "mem_used",
from: 0,
to: 0,
peak: 0,
enabled: false,
},
];
$: isModified = !statistics.every((a) => {
let b = stats.find((s) => s.field == a.field);
if (b == null) return !a.enabled;
return a.from == b.from && a.to == b.to;
});
function getPeak(cluster, metric) {
const mc = cluster.metricConfig.find((mc) => mc.name == metric);
return mc ? mc.peak : 0;
}
function resetRange(isInitialized, cluster) {
if (!isInitialized) return;
if (cluster != null) {
let c = clusters.find((c) => c.name == cluster);
for (let stat of statistics) {
stat.peak = getPeak(c, stat.metric);
stat.from = 0;
stat.to = stat.peak;
}
} else {
for (let stat of statistics) {
for (let c of clusters) {
stat.peak = Math.max(stat.peak, getPeak(c, stat.metric));
}
stat.from = 0;
stat.to = stat.peak;
}
}
statistics = [...statistics];
}
$: resetRange($initialized, cluster);
</script>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>Filter based on statistics (of non-running jobs)</ModalHeader>
<ModalBody>
{#each statistics as stat}
<h4>{stat.text}</h4>
<DoubleRangeSlider
on:change={({ detail }) => (
(stat.from = detail[0]), (stat.to = detail[1]), (stat.enabled = true)
)}
min={0}
max={stat.peak}
firstSlider={stat.from}
secondSlider={stat.to}
inputFieldFrom={stat.from}
inputFieldTo={stat.to}
/>
{/each}
</ModalBody>
<ModalFooter>
<Button
color="primary"
on:click={() => {
isOpen = false;
stats = statistics.filter((stat) => stat.enabled);
dispatch("update", { stats });
}}>Close & Apply</Button
>
<Button
color="danger"
on:click={() => {
isOpen = false;
resetRange($initialized, cluster);
statistics.forEach((stat) => (stat.enabled = false));
stats = [];
dispatch("update", { stats });
}}>Reset</Button
>
<Button on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
</Modal>

View File

@ -1,15 +1,21 @@
<!-- <!--
@component @component Main filter component; handles filter object on sub-component changes before dispatching it
Properties: Properties:
- menuText: String? (Optional text to show in the dropdown menu) - `menuText String?`: Optional text to show in the dropdown menu [Default: null]
- filterPresets: Object? (Optional predefined filter values) - `filterPresets Object?`: Optional predefined filter values [Default: {}]
- `disableClusterSelection Bool?`: Is the selection disabled [Default: false]
- `startTimeQuickSelect Bool?`: Render startTime quick selections [Default: false]
Events: Events:
- 'update': The detail's 'filters' prop are new filter items to be applied - `update-filters, {filters: [Object]?}`: The detail's 'filters' prop are new filter items to be applied
Functions: Functions:
- void update(additionalFilters: Object?): Triggers an update - `void updateFilters (additionalFilters: Object?)`: Handles new filters from nested components, triggers upstream update event
--> -->
<script> <script>
import { createEventDispatcher } from "svelte";
import { import {
Row, Row,
Col, Col,
@ -19,17 +25,15 @@
ButtonDropdown, ButtonDropdown,
Icon, Icon,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { createEventDispatcher } from "svelte"; import Tag from "./helper/Tag.svelte";
import Info from "./InfoBox.svelte"; import Info from "./filters/InfoBox.svelte";
import Cluster from "./Cluster.svelte"; import Cluster from "./filters/Cluster.svelte";
import JobStates, { allJobStates } from "./JobStates.svelte"; import JobStates, { allJobStates } from "./filters/JobStates.svelte";
import StartTime from "./StartTime.svelte"; import StartTime from "./filters/StartTime.svelte";
import Tags from "./Tags.svelte"; import Tags from "./filters/Tags.svelte";
import Tag from "../Tag.svelte"; import Duration from "./filters/Duration.svelte";
import Duration from "./Duration.svelte"; import Resources from "./filters/Resources.svelte";
import Resources from "./Resources.svelte"; import Statistics from "./filters/Stats.svelte";
import Statistics from "./Stats.svelte";
// import TimeSelection from './TimeSelection.svelte'
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@ -83,7 +87,7 @@
isAccsModified = false; isAccsModified = false;
// Can be called from the outside to trigger a 'update' event from this component. // Can be called from the outside to trigger a 'update' event from this component.
export function update(additionalFilters = null) { export function updateFilters(additionalFilters = null) {
if (additionalFilters != null) if (additionalFilters != null)
for (let key in additionalFilters) filters[key] = additionalFilters[key]; for (let key in additionalFilters) filters[key] = additionalFilters[key];
@ -136,10 +140,10 @@
if (filters.project) if (filters.project)
items.push({ project: { [filters.projectMatch]: filters.project } }); items.push({ project: { [filters.projectMatch]: filters.project } });
if (filters.jobName) items.push({ jobName: { contains: filters.jobName } }); if (filters.jobName) items.push({ jobName: { contains: filters.jobName } });
for (let stat of filters.stats) if (filters.stats.length != 0)
items.push({ [stat.field]: { from: stat.from, to: stat.to } }); items.push({ metricStats: filters.stats.map((st) => { return { metricName: st.field, range: { from: st.from, to: st.to }} }) });
dispatch("update", { filters: items }); dispatch("update-filters", { filters: items });
changeURL(); changeURL();
return items; return items;
} }
@ -249,7 +253,7 @@
).toISOString(); ).toISOString();
filters.startTime.to = new Date(Date.now()).toISOString(); filters.startTime.to = new Date(Date.now()).toISOString();
(filters.startTime.text = text), (filters.startTime.url = url); (filters.startTime.text = text), (filters.startTime.url = url);
update(); updateFilters();
}} }}
> >
<Icon name="calendar-range" /> <Icon name="calendar-range" />
@ -363,23 +367,23 @@
bind:isOpen={isClusterOpen} bind:isOpen={isClusterOpen}
bind:cluster={filters.cluster} bind:cluster={filters.cluster}
bind:partition={filters.partition} bind:partition={filters.partition}
on:update={() => update()} on:set-filter={() => updateFilters()}
/> />
<JobStates <JobStates
bind:isOpen={isJobStatesOpen} bind:isOpen={isJobStatesOpen}
bind:states={filters.states} bind:states={filters.states}
on:update={() => update()} on:set-filter={() => updateFilters()}
/> />
<StartTime <StartTime
bind:isOpen={isStartTimeOpen} bind:isOpen={isStartTimeOpen}
bind:from={filters.startTime.from} bind:from={filters.startTime.from}
bind:to={filters.startTime.to} bind:to={filters.startTime.to}
on:update={() => { on:set-filter={() => {
delete filters.startTime["text"]; delete filters.startTime["text"];
delete filters.startTime["url"]; delete filters.startTime["url"];
update(); updateFilters();
}} }}
/> />
@ -389,13 +393,13 @@
bind:moreThan={filters.duration.moreThan} 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:set-filter={() => updateFilters()}
/> />
<Tags <Tags
bind:isOpen={isTagsOpen} bind:isOpen={isTagsOpen}
bind:tags={filters.tags} bind:tags={filters.tags}
on:update={() => update()} on:set-filter={() => updateFilters()}
/> />
<Resources <Resources
@ -408,14 +412,13 @@
bind:isNodesModified bind:isNodesModified
bind:isHwthreadsModified bind:isHwthreadsModified
bind:isAccsModified bind:isAccsModified
on:update={() => update()} on:set-filter={() => updateFilters()}
/> />
<Statistics <Statistics
cluster={filters.cluster}
bind:isOpen={isStatsOpen} bind:isOpen={isStatsOpen}
bind:stats={filters.stats} bind:stats={filters.stats}
on:update={() => update()} on:set-filter={() => updateFilters()}
/> />
<style> <style>

View File

@ -1,31 +1,36 @@
<!-- <!--
@component @component Main jobList component; lists jobs according to set filters
Properties: Properties:
- metrics: [String] (can change from outside) - `sorting Object?`: Currently active sorting [Default: {field: "startTime", type: "col", order: "DESC"}]
- sorting: { field: String, order: "DESC" | "ASC" } (can change from outside) - `matchedJobs Number?`: Number of matched jobs for selected filters [Default: 0]
- matchedJobs: Number (changes from inside) - `metrics [String]?`: The currently selected metrics [Default: User-Configured Selection]
- `showFootprint Bool`: If to display the jobFootprint component
Functions: Functions:
- update(filters?: [JobFilter]) - `refreshJobs()`: Load jobs data with unchanged parameters and 'network-only' keyword
- `refreshAllMetrics()`: Trigger downstream refresh of all running jobs' metric data
- `queryJobs(filters?: [JobFilter])`: Load jobs data with new filters, starts from page 1
--> -->
<script> <script>
import { getContext } from "svelte";
import { import {
queryStore, queryStore,
gql, gql,
getContextClient, getContextClient,
mutationStore, mutationStore,
} from "@urql/svelte"; } from "@urql/svelte";
import { getContext } from "svelte";
import { Row, Table, Card, Spinner } from "@sveltestrap/sveltestrap"; import { Row, Table, Card, Spinner } from "@sveltestrap/sveltestrap";
import Pagination from "./Pagination.svelte"; import { stickyHeader } from "./utils.js";
import JobListRow from "./Row.svelte"; import Pagination from "./joblist/Pagination.svelte";
import { stickyHeader } from "../utils.js"; import JobListRow from "./joblist/JobListRow.svelte";
const ccconfig = getContext("cc-config"), const ccconfig = getContext("cc-config"),
clusters = getContext("clusters"), initialized = getContext("initialized"),
initialized = getContext("initialized"); globalMetrics = getContext("globalMetrics");
export let sorting = { field: "startTime", order: "DESC" }; export let sorting = { field: "startTime", type: "col", 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; export let showFootprint;
@ -35,6 +40,12 @@
let page = 1; let page = 1;
let paging = { itemsPerPage, page }; let paging = { itemsPerPage, page };
let filter = []; let filter = [];
let triggerMetricRefresh = false;
function getUnit(m) {
const rawUnit = globalMetrics.find((gm) => gm.name === m)?.unit
return (rawUnit?.prefix ? rawUnit.prefix : "") + (rawUnit?.base ? rawUnit.base : "")
}
const client = getContextClient(); const client = getContextClient();
const query = gql` const query = gql`
@ -75,7 +86,11 @@
name name
} }
metaData metaData
footprint footprint {
name
stat
value
}
} }
count count
hasNextPage hasNextPage
@ -97,7 +112,7 @@
$: matchedJobs = $jobsStore.data != null ? $jobsStore.data.jobs.count : 0; $: matchedJobs = $jobsStore.data != null ? $jobsStore.data.jobs.count : 0;
// 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 refreshJobs() {
jobsStore = queryStore({ jobsStore = queryStore({
client: client, client: client,
query: query, query: query,
@ -106,8 +121,16 @@
}); });
} }
// (Re-)query and optionally set new filters. export function refreshAllMetrics() {
export function update(filters) { // Refresh Job Metrics (Downstream will only query for running jobs)
triggerMetricRefresh = true
setTimeout(function () {
triggerMetricRefresh = false;
}, 100);
}
// (Re-)query and optionally set new filters; Query will be started reactively.
export function queryJobs(filters) {
if (filters != null) { if (filters != null) {
let minRunningFor = ccconfig.plot_list_hideShortRunningJobs; let minRunningFor = ccconfig.plot_list_hideShortRunningJobs;
if (minRunningFor && minRunningFor > 0) { if (minRunningFor && minRunningFor > 0) {
@ -141,7 +164,6 @@
paging = { itemsPerPage: value, page: page }; // Trigger reload of jobList paging = { itemsPerPage: value, page: page }; // Trigger reload of jobList
} else if (res.fetching === false && res.error) { } else if (res.fetching === false && res.error) {
throw res.error; throw res.error;
// console.log('Error on subscription: ' + res.error)
} }
}); });
} }
@ -215,22 +237,7 @@
> >
{metric} {metric}
{#if $initialized} {#if $initialized}
({clusters ({getUnit(metric)})
.map((cluster) =>
cluster.metricConfig.find((m) => m.name == metric),
)
.filter((m) => m != null)
.map(
(m) =>
(m.unit?.prefix ? m.unit?.prefix : "") +
(m.unit?.base ? m.unit?.base : ""),
) // Build unitStr
.reduce(
(arr, unitStr) =>
arr.includes(unitStr) ? arr : [...arr, unitStr],
[],
) // w/o this, output would be [unitStr, unitStr]
.join(", ")})
{/if} {/if}
</th> </th>
{/each} {/each}
@ -247,7 +254,7 @@
</tr> </tr>
{:else} {:else}
{#each jobs as job (job)} {#each jobs as job (job)}
<JobListRow {job} {metrics} {plotWidth} {showFootprint} /> <JobListRow bind:triggerMetricRefresh {job} {metrics} {plotWidth} {showFootprint} />
{:else} {:else}
<tr> <tr>
<td colspan={metrics.length + 1}> No jobs found </td> <td colspan={metrics.length + 1}> No jobs found </td>
@ -274,7 +281,7 @@
{itemsPerPage} {itemsPerPage}
itemText="Jobs" itemText="Jobs"
totalItems={matchedJobs} totalItems={matchedJobs}
on:update={({ detail }) => { on:update-paging={({ detail }) => {
if (detail.itemsPerPage != itemsPerPage) { if (detail.itemsPerPage != itemsPerPage) {
updateConfiguration(detail.itemsPerPage.toString(), detail.page); updateConfiguration(detail.itemsPerPage.toString(), detail.page);
} else { } else {

View File

@ -1,9 +1,11 @@
<!-- <!--
@component @component Organized display of plots as table
Properties: Properties:
- itemsPerRow: Number - `itemsPerRow Number`: Elements to render per row
- items: [Any] - `items [Any]`: List of plot components to render
- `padding Number`: Padding between plot elements
- `renderFor String`: If 'job', filter disabled metrics
--> -->
<script> <script>

View File

@ -1,3 +1,17 @@
<!--
@component Filter sub-component for selecting cluster and subCluster
Properties:
- `disableClusterSelection Bool?`: Is the selection disabled [Default: false]
- `isModified Bool?`: Is this filter component modified [Default: false]
- `isOpen Bool?`: Is this filter component opened [Default: false]
- `cluster String?`: The currently selected cluster [Default: null]
- `partition String?`: The currently selected partition (i.e. subCluster) [Default: null]
Events:
- `set-filter, {String?, String?}`: Set 'cluster, subCluster' filter in upstream component
-->
<script> <script>
import { createEventDispatcher, getContext } from "svelte"; import { createEventDispatcher, getContext } from "svelte";
import { import {
@ -78,7 +92,7 @@
isOpen = false; isOpen = false;
cluster = pendingCluster; cluster = pendingCluster;
partition = pendingPartition; partition = pendingPartition;
dispatch("update", { cluster, partition }); dispatch("set-filter", { cluster, partition });
}}>Close & Apply</Button }}>Close & Apply</Button
> >
<Button <Button
@ -87,7 +101,7 @@
isOpen = false; isOpen = false;
cluster = pendingCluster = null; cluster = pendingCluster = null;
partition = pendingPartition = null; partition = pendingPartition = null;
dispatch("update", { cluster, partition }); dispatch("set-filter", { cluster, partition });
}}>Reset</Button }}>Reset</Button
> >
<Button on:click={() => (isOpen = false)}>Close</Button> <Button on:click={() => (isOpen = false)}>Close</Button>

View File

@ -1,4 +1,18 @@
<script> <!--
@component Filter sub-component for selecting job duration
Properties:
- `isOpen Bool?`: Is this filter component opened [Default: false]
- `lessThan Number?`: Amount of seconds [Default: null]
- `moreThan Number?`: Amount of seconds [Default: null]
- `from Number?`: Epoch time in seconds [Default: null]
- `to Number?`: Epoch time in seconds [Default: null]
Events:
- `set-filter, {Number, Number, Number, Number}`: Set 'lessThan, moreThan, from, to' filter in upstream component
-->
<script>
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import { import {
Row, Row,
@ -212,7 +226,7 @@
moreThan = hoursAndMinsToSecs(pendingMoreThan); moreThan = hoursAndMinsToSecs(pendingMoreThan);
from = hoursAndMinsToSecs(pendingFrom); from = hoursAndMinsToSecs(pendingFrom);
to = hoursAndMinsToSecs(pendingTo); to = hoursAndMinsToSecs(pendingTo);
dispatch("update", { lessThan, moreThan, from, to }); dispatch("set-filter", { lessThan, moreThan, from, to });
}} }}
> >
Close & Apply Close & Apply
@ -236,7 +250,7 @@
from = null; from = null;
to = null; to = null;
reset(); reset();
dispatch("update", { lessThan, moreThan, from, to }); dispatch("set-filter", { lessThan, moreThan, from, to });
}}>Reset Filter</Button }}>Reset Filter</Button
> >
<Button on:click={() => (isOpen = false)}>Close</Button> <Button on:click={() => (isOpen = false)}>Close</Button>

View File

@ -1,4 +1,12 @@
<script> <!--
@component Info pill displayed for active filters
Properties:
- `icon String`: Sveltestrap icon name
- `modified Bool?`: Optional if filter is modified [Default: false]
-->
<script>
import { Button, Icon } from "@sveltestrap/sveltestrap"; import { Button, Icon } from "@sveltestrap/sveltestrap";
export let icon; export let icon;

View File

@ -1,3 +1,18 @@
<!--
@component Filter sub-component for selecting job states
Properties:
- `isModified Bool?`: Is this filter component modified [Default: false]
- `isOpen Bool?`: Is this filter component opened [Default: false]
- `states [String]?`: The currently selected states [Default: [...allJobStates]]
Events:
- `set-filter, {[String]}`: Set 'states' filter in upstream component
Exported:
- `const allJobStates [String]`: List of all available job states used in cc-backend
-->
<script context="module"> <script context="module">
export const allJobStates = [ export const allJobStates = [
"running", "running",
@ -59,7 +74,7 @@
on:click={() => { on:click={() => {
isOpen = false; isOpen = false;
states = [...pendingStates]; states = [...pendingStates];
dispatch("update", { states }); dispatch("set-filter", { states });
}}>Close & Apply</Button }}>Close & Apply</Button
> >
<Button <Button
@ -68,7 +83,7 @@
isOpen = false; isOpen = false;
states = [...allJobStates]; states = [...allJobStates];
pendingStates = [...allJobStates]; pendingStates = [...allJobStates];
dispatch("update", { states }); dispatch("set-filter", { states });
}}>Reset</Button }}>Reset</Button
> >
<Button on:click={() => (isOpen = false)}>Close</Button> <Button on:click={() => (isOpen = false)}>Close</Button>

View File

@ -1,4 +1,22 @@
<script> <!--
@component Filter sub-component for selecting job resources
Properties:
- `cluster Object?`: The currently selected cluster config [Default: null]
- `isOpen Bool?`: Is this filter component opened [Default: false]
- `numNodes Object?`: The currently selected numNodes filter [Default: {from:null, to:null}]
- `numHWThreads Object?`: The currently selected numHWTreads filter [Default: {from:null, to:null}]
- `numAccelerators Object?`: The currently selected numAccelerators filter [Default: {from:null, to:null}]
- `isNodesModified Bool?`: Is the node filter modified [Default: false]
- `isHwtreadsModified Bool?`: Is the Hwthreads filter modified [Default: false]
- `isAccsModified Bool?`: Is the Accelerator filter modified [Default: false]
- `namedNode String?`: The currently selected single named node (= hostname) [Default: null]
Events:
- `set-filter, {Object, Object, Object, String}`: Set 'numNodes, numHWThreads, numAccelerators, namedNode' filter in upstream component
-->
<script>
import { createEventDispatcher, getContext } from "svelte"; import { createEventDispatcher, getContext } from "svelte";
import { import {
Button, Button,
@ -7,7 +25,7 @@
ModalHeader, ModalHeader,
ModalFooter, ModalFooter,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import DoubleRangeSlider from "./DoubleRangeSlider.svelte"; import DoubleRangeSlider from "../select/DoubleRangeSlider.svelte";
const clusters = getContext("clusters"), const clusters = getContext("clusters"),
initialized = getContext("initialized"), initialized = getContext("initialized"),
@ -59,7 +77,6 @@
0, 0,
); );
// console.log(header)
let minNumNodes = 1, let minNumNodes = 1,
maxNumNodes = 0, maxNumNodes = 0,
minNumHWThreads = 1, minNumHWThreads = 1,
@ -198,7 +215,7 @@
to: pendingNumAccelerators.to, to: pendingNumAccelerators.to,
}; };
namedNode = pendingNamedNode; namedNode = pendingNamedNode;
dispatch("update", { dispatch("set-filter", {
numNodes, numNodes,
numHWThreads, numHWThreads,
numAccelerators, numAccelerators,
@ -229,7 +246,7 @@
isHwthreadsModified = false; isHwthreadsModified = false;
isAccsModified = false; isAccsModified = false;
namedNode = pendingNamedNode; namedNode = pendingNamedNode;
dispatch("update", { dispatch("set-filter", {
numNodes, numNodes,
numHWThreads, numHWThreads,
numAccelerators, numAccelerators,

View File

@ -1,3 +1,16 @@
<!--
@component Filter sub-component for selecting job starttime
Properties:
- `isModified Bool?`: Is this filter component modified [Default: false]
- `isOpen Bool?`: Is this filter component opened [Default: false]
- `from Object?`: The currently selected from startime [Default: null]
- `to Object?`: The currently selected to starttime (i.e. subCluster) [Default: null]
Events:
- `set-filter, {String?, String?}`: Set 'from, to' filter in upstream component
-->
<script> <script>
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import { parse, format, sub } from "date-fns"; import { parse, format, sub } from "date-fns";
@ -101,7 +114,7 @@
isOpen = false; isOpen = false;
from = toRFC3339(pendingFrom); from = toRFC3339(pendingFrom);
to = toRFC3339(pendingTo, "59"); to = toRFC3339(pendingTo, "59");
dispatch("update", { from, to }); dispatch("set-filter", { from, to });
}} }}
> >
Close & Apply Close & Apply
@ -113,7 +126,7 @@
from = null; from = null;
to = null; to = null;
reset(); reset();
dispatch("update", { from, to }); dispatch("set-filter", { from, to });
}}>Reset</Button }}>Reset</Button
> >
<Button on:click={() => (isOpen = false)}>Close</Button> <Button on:click={() => (isOpen = false)}>Close</Button>

View File

@ -0,0 +1,95 @@
<!--
@component Filter sub-component for selecting job statistics
Properties:
- `isModified Bool?`: Is this filter component modified [Default: false]
- `isOpen Bool?`: Is this filter component opened [Default: false]
- `stats [Object]?`: The currently selected statistics filter [Default: []]
Events:
- `set-filter, {[Object]}`: Set 'stats' filter in upstream component
-->
<script>
import { createEventDispatcher, getContext } from "svelte";
import { getStatsItems } from "../utils.js";
import {
Button,
Modal,
ModalBody,
ModalHeader,
ModalFooter,
} from "@sveltestrap/sveltestrap";
import DoubleRangeSlider from "../select/DoubleRangeSlider.svelte";
const initialized = getContext("initialized"),
dispatch = createEventDispatcher();
export let isModified = false;
export let isOpen = false;
export let stats = [];
let statistics = []
function loadRanges(isInitialized) {
if (!isInitialized) return;
statistics = getStatsItems();
}
function resetRanges() {
for (let st of statistics) {
st.enabled = false
st.from = 0
st.to = st.peak
}
}
$: isModified = !statistics.every((a) => {
let b = stats.find((s) => s.field == a.field);
if (b == null) return !a.enabled;
return a.from == b.from && a.to == b.to;
});
$: loadRanges($initialized);
</script>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>Filter based on statistics (of non-running jobs)</ModalHeader>
<ModalBody>
{#each statistics as stat}
<h4>{stat.text}</h4>
<DoubleRangeSlider
on:change={({ detail }) => (
(stat.from = detail[0]), (stat.to = detail[1]), (stat.enabled = true)
)}
min={0}
max={stat.peak}
firstSlider={stat.from}
secondSlider={stat.to}
inputFieldFrom={stat.from}
inputFieldTo={stat.to}
/>
{/each}
</ModalBody>
<ModalFooter>
<Button
color="primary"
on:click={() => {
isOpen = false;
stats = statistics.filter((stat) => stat.enabled);
dispatch("set-filter", { stats });
}}>Close & Apply</Button
>
<Button
color="danger"
on:click={() => {
isOpen = false;
resetRanges();
stats = [];
dispatch("set-filter", { stats });
}}>Reset</Button
>
<Button on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
</Modal>

View File

@ -1,3 +1,15 @@
<!--
@component Filter sub-component for selecting tags
Properties:
- `isModified Bool?`: Is this filter component modified [Default: false]
- `isOpen Bool?`: Is this filter component opened [Default: false]
- `tags [Number]?`: The currently selected tags (as IDs) [Default: []]
Events:
- `set-filter, {[Number]}`: Set 'tag' filter in upstream component
-->
<script> <script>
import { createEventDispatcher, getContext } from "svelte"; import { createEventDispatcher, getContext } from "svelte";
import { import {
@ -12,7 +24,7 @@
Icon, Icon,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { fuzzySearchTags } from "../utils.js"; import { fuzzySearchTags } from "../utils.js";
import Tag from "../Tag.svelte"; import Tag from "../helper/Tag.svelte";
const allTags = getContext("tags"), const allTags = getContext("tags"),
initialized = getContext("initialized"), initialized = getContext("initialized"),
@ -72,7 +84,7 @@
on:click={() => { on:click={() => {
isOpen = false; isOpen = false;
tags = [...pendingTags]; tags = [...pendingTags];
dispatch("update", { tags }); dispatch("set-filter", { tags });
}}>Close & Apply</Button }}>Close & Apply</Button
> >
<Button <Button
@ -81,7 +93,7 @@
isOpen = false; isOpen = false;
tags = []; tags = [];
pendingTags = []; pendingTags = [];
dispatch("update", { tags }); dispatch("set-filter", { tags });
}}>Reset</Button }}>Reset</Button
> >
<Button on:click={() => (isOpen = false)}>Close</Button> <Button on:click={() => (isOpen = false)}>Close</Button>

View File

@ -0,0 +1,264 @@
<!--
@component Footprint component; Displays job.footprint data as bars in relation to thresholds
Properties:
- `job Object`: The GQL job object
- `displayTitle Bool?`: If to display cardHeader with title [Default: true]
- `width String?`: Width of the card [Default: 'auto']
- `height String?`: Height of the card [Default: '310px']
-->
<script context="module">
function findJobThresholds(job, metricConfig) {
if (!job || !metricConfig) {
console.warn("Argument missing for findJobThresholds!");
return null;
}
// metricConfig is on subCluster-Level
const defaultThresholds = {
peak: metricConfig.peak,
normal: metricConfig.normal,
caution: metricConfig.caution,
alert: metricConfig.alert
};
// Job_Exclusivity does not matter, only aggregation
if (metricConfig.aggregation === "avg") {
return defaultThresholds;
} else if (metricConfig.aggregation === "sum") {
const topol = getContext("getHardwareTopology")(job.cluster, job.subCluster)
const jobFraction = job.numHWThreads / topol.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 defaultThresholds;
}
}
</script>
<script>
import { getContext } from "svelte";
import {
Card,
CardHeader,
CardTitle,
CardBody,
Progress,
Icon,
Tooltip,
Row,
Col
} from "@sveltestrap/sveltestrap";
import { round } from "mathjs";
export let job;
export let displayTitle = true;
export let width = "auto";
export let height = "310px";
const footprintData = job?.footprint?.map((jf) => {
const fmc = getContext("getMetricConfig")(job.cluster, job.subCluster, jf.name);
if (fmc) {
// Unit
const unit = (fmc?.unit?.prefix ? fmc.unit.prefix : "") + (fmc?.unit?.base ? fmc.unit.base : "")
// Threshold / -Differences
const fmt = findJobThresholds(job, fmc);
if (jf.name === "flops_any") fmt.peak = round(fmt.peak * 0.85, 0);
// Define basic data -> Value: Use as Provided
const fmBase = {
name: jf.name + ' (' + jf.stat + ')',
avg: jf.value,
unit: unit,
max: fmt.peak,
dir: fmc.lowerIsBetter
};
if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "alert")) {
return {
...fmBase,
color: "danger",
message: `Metric average way ${fmc.lowerIsBetter ? "above" : "below"} expected normal thresholds.`,
impact: 3
};
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "caution")) {
return {
...fmBase,
color: "warning",
message: `Metric average ${fmc.lowerIsBetter ? "above" : "below"} expected normal thresholds.`,
impact: 2,
};
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "normal")) {
return {
...fmBase,
color: "success",
message: "Metric average within expected thresholds.",
impact: 1,
};
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "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,
};
}
} else { // No matching metric config: display as single value
return {
name: jf.name + ' (' + jf.stat + ')',
avg: jf.value,
message:
`No config for metric ${jf.name} found.`,
impact: 4,
};
}
}).sort(function (a, b) { // Sort by impact value primarily, within impact sort name alphabetically
return a.impact - b.impact || ((a.name > b.name) ? 1 : ((b.name > a.name) ? -1 : 0));
});;
function evalFootprint(mean, thresholds, lowerIsBetter, level) {
// Handle Metrics in which less value is better
switch (level) {
case "peak":
if (lowerIsBetter)
return false; // metric over peak -> return false to trigger impact -1
else return mean <= thresholds.peak && mean > thresholds.normal;
case "alert":
if (lowerIsBetter)
return mean <= thresholds.peak && mean >= thresholds.alert;
else return mean <= thresholds.alert && mean >= 0;
case "caution":
if (lowerIsBetter)
return mean < thresholds.alert && mean >= thresholds.caution;
else return mean <= thresholds.caution && mean > thresholds.alert;
case "normal":
if (lowerIsBetter)
return mean < thresholds.caution && mean >= 0;
else return mean <= thresholds.normal && mean > thresholds.caution;
default:
return false;
}
}
</script>
<Card class="mt-1 overflow-auto" style="width: {width}; height: {height}">
{#if displayTitle}
<CardHeader>
<CardTitle class="mb-0 d-flex justify-content-center">
Core Metrics Footprint
</CardTitle>
</CardHeader>
{/if}
<CardBody>
{#each footprintData as fpd, index}
{#if fpd.impact !== 4}
<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>
<Row cols={12} class="{(footprintData.length == (index + 1)) ? 'mb-0' : 'mb-2'}">
{#if fpd.dir}
<Col xs="1">
<Icon name="caret-left-fill" />
</Col>
{/if}
<Col xs="11" class="align-content-center">
<Progress value={fpd.avg} max={fpd.max} color={fpd.color} />
</Col>
{#if !fpd.dir}
<Col xs="1">
<Icon name="caret-right-fill" />
</Col>
{/if}
</Row>
{:else}
<div class="mb-1 d-flex justify-content-between">
<div>
&nbsp;<b>{fpd.name}</b>
</div>
<div
class="cursor-help d-inline-flex"
id={`footprint-${job.jobId}-${index}`}
>
<div class="mx-1">
<Icon name="info-circle"/>
</div>
<div>
{fpd.avg}&nbsp;
</div>
</div>
</div>
<Tooltip
target={`footprint-${job.jobId}-${index}`}
placement="right"
offset={[0, 20]}>{fpd.message}</Tooltip
>
{/if}
{/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

@ -1,8 +1,11 @@
<!-- <!--
@component @component Triggers upstream data refresh in selectable intervals
Properties:
- `initially Number?`: Initial refresh interval on component mount, in seconds [Default: null]
Events: Events:
- 'reload': When fired, the parent component shoud refresh its contents - `refresh`: When fired, the upstream component refreshes its contents
--> -->
<script> <script>
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
@ -17,10 +20,11 @@
if (refreshInterval == null) return; if (refreshInterval == null) return;
refreshIntervalId = setInterval(() => dispatch("reload"), refreshInterval); refreshIntervalId = setInterval(() => dispatch("refresh"), refreshInterval);
} }
export let initially = null; export let initially = null;
if (initially != null) { if (initially != null) {
refreshInterval = initially * 1000; refreshInterval = initially * 1000;
refreshIntervalChanged(); refreshIntervalChanged();
@ -30,17 +34,17 @@
<InputGroup> <InputGroup>
<Button <Button
outline outline
on:click={() => dispatch("reload")} on:click={() => dispatch("refresh")}
disabled={refreshInterval != null} disabled={refreshInterval != null}
> >
<Icon name="arrow-clockwise" /> Reload <Icon name="arrow-clockwise" /> Refresh
</Button> </Button>
<select <select
class="form-select" class="form-select"
bind:value={refreshInterval} bind:value={refreshInterval}
on:change={refreshIntervalChanged} on:change={refreshIntervalChanged}
> >
<option value={null}>No periodic reload</option> <option value={null}>No periodic refresh</option>
<option value={30 * 1000}>Update every 30 seconds</option> <option value={30 * 1000}>Update every 30 seconds</option>
<option value={60 * 1000}>Update every minute</option> <option value={60 * 1000}>Update every minute</option>
<option value={2 * 60 * 1000}>Update every two minutes</option> <option value={2 * 60 * 1000}>Update every two minutes</option>

View File

@ -1,5 +1,5 @@
<!-- <!--
@component @component Single tag pill component
Properties: Properties:
- id: ID! (if the tag-id is known but not the tag type/name, this can be used) - id: ID! (if the tag-id is known but not the tag type/name, this can be used)

View File

@ -1,7 +1,19 @@
<!--
@component Search Field for Job-Lists with separate mode if project filter is active
Properties:
- `presetProject String?`: Currently active project filter [Default: '']
- `authlevel Number?`: The current users authentication level [Default: null]
- `roles [Number]?`: Enum containing available roles [Default: null]
Events:
- `set-filter, {String?, String?, String?}`: Set 'user, project, jobName' filter in upstream component
-->
<script> <script>
import { InputGroup, Input, Button, Icon } from "@sveltestrap/sveltestrap"; import { InputGroup, Input, Button, Icon } from "@sveltestrap/sveltestrap";
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import { scramble, scrambleNames } from "../joblist/JobInfo.svelte"; import { scramble, scrambleNames } from "../utils.js";
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@ -40,7 +52,7 @@
if (timeoutId != null) clearTimeout(timeoutId); if (timeoutId != null) clearTimeout(timeoutId);
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
dispatch("update", { dispatch("set-filter", {
user, user,
project, project,
jobName jobName
@ -53,7 +65,7 @@
if (timeoutId != null) clearTimeout(timeoutId); if (timeoutId != null) clearTimeout(timeoutId);
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
dispatch("update", { dispatch("set-filter", {
project, project,
jobName jobName
}); });

View File

@ -1,25 +1,15 @@
<!-- <!--
@component @component Displays job metaData, serves links to detail pages
Properties: Properties:
- job: GraphQL.Job - `job Object`: The Job Object (GraphQL.Job)
- jobTags: Defaults to job.tags, usefull for dynamically updating the tags. - `jobTags [Number]?`: The jobs tags as IDs, default useful for dynamically updating the tags [Default: job.tags]
--> -->
<script context="module">
export const scrambleNames = window.localStorage.getItem("cc-scramble-names");
export const scramble = function (str) {
if (str === "-") return str;
else
return [...str]
.reduce((x, c, i) => x * 7 + c.charCodeAt(0) * i * 21, 5)
.toString(32)
.substr(0, 6);
};
</script>
<script> <script>
import Tag from "../Tag.svelte";
import { Badge, Icon } from "@sveltestrap/sveltestrap"; import { Badge, Icon } from "@sveltestrap/sveltestrap";
import { scrambleNames, scramble } from "../utils.js";
import Tag from "../helper/Tag.svelte";
export let job; export let job;
export let jobTags = job.tags; export let jobTags = job.tags;

View File

@ -1,27 +1,30 @@
<!-- <!--
@component @component Data row for a single job displaying metric plots
Properties: Properties:
- job: GraphQL.Job (constant/key) - `job Object`: The job object (GraphQL.Job)
- metrics: [String] (can change) - `metrics [String]`: Currently selected metrics
- plotWidth: Number - `plotWidth Number`: Width of the sub-components
- plotHeight: Number - `plotHeight Number?`: Height of the sub-components [Default: 275]
- `showFootprint Bool`: Display of footprint component for job
- `triggerMetricRefresh Bool?`: If changed to true from upstream, will trigger metric query
--> -->
<script> <script>
import { queryStore, gql, getContextClient } from "@urql/svelte"; import { queryStore, gql, getContextClient } from "@urql/svelte";
import { getContext } from "svelte"; import { getContext } from "svelte";
import { Card, Spinner } from "@sveltestrap/sveltestrap"; import { Card, Spinner } from "@sveltestrap/sveltestrap";
import MetricPlot from "../plots/MetricPlot.svelte";
import JobInfo from "./JobInfo.svelte";
import JobFootprint from "../JobFootprint.svelte";
import { maxScope, checkMetricDisabled } from "../utils.js"; import { maxScope, checkMetricDisabled } from "../utils.js";
import JobInfo from "./JobInfo.svelte";
import MetricPlot from "../plots/MetricPlot.svelte";
import JobFootprint from "../helper/JobFootprint.svelte";
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; export let showFootprint;
export let triggerMetricRefresh = false;
let { id } = job; let { id } = job;
let scopes = job.numNodes == 1 let scopes = job.numNodes == 1
@ -30,16 +33,11 @@
: ["core"] : ["core"]
: ["node"]; : ["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 client = getContextClient(); const client = getContextClient();
const query = gql` const query = gql`
query ($id: ID!, $queryMetrics: [String!]!, $scopes: [MetricScope!]!) { query ($id: ID!, $metrics: [String!]!, $scopes: [MetricScope!]!) {
jobMetrics(id: $id, metrics: $queryMetrics, scopes: $scopes) { jobMetrics(id: $id, metrics: $metrics, scopes: $scopes) {
name name
scope scope
metric { metric {
@ -71,38 +69,23 @@
$: metricsQuery = queryStore({ $: metricsQuery = queryStore({
client: client, client: client,
query: query, query: query,
variables: { id, queryMetrics, scopes }, variables: { id, metrics, scopes },
}); });
let queryMetrics = null; function refreshMetrics() {
$: 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
? job.numAcc >= 1
? ["core", "accelerator"]
: ["core"]
: ["node"];
}
export function refresh() {
metricsQuery = queryStore({ metricsQuery = queryStore({
client: client, client: client,
query: query, query: query,
variables: { id, queryMetrics, scopes }, variables: { id, metrics, scopes },
// requestPolicy: 'network-only' // use default cache-first for refresh // requestPolicy: 'network-only' // use default cache-first for refresh
}); });
} }
$: if (job.state === 'running' && triggerMetricRefresh === true) {
refreshMetrics();
}
// Helper
const selectScope = (jobMetrics) => const selectScope = (jobMetrics) =>
jobMetrics.reduce( jobMetrics.reduce(
(a, b) => (a, b) =>
@ -138,7 +121,6 @@
} }
}); });
if (job.monitoringStatus) refresh();
</script> </script>
<tr> <tr>
@ -166,9 +148,9 @@
<td> <td>
<JobFootprint <JobFootprint
{job} {job}
jobMetrics={$metricsQuery.data.jobMetrics}
width={plotWidth} width={plotWidth}
view="list" height="{plotHeight}px"
displayTitle={false}
/> />
</td> </td>
{/if} {/if}

View File

@ -1,12 +1,13 @@
<!-- <!--
@component @component Pagination selection component
Properties: Properties:
- page: Number (changes from inside) - page: Number (changes from inside)
- itemsPerPage: Number (changes from inside) - itemsPerPage: Number (changes from inside)
- totalItems: Number (only displayed) - totalItems: Number (only displayed)
Events: Events:
- "update": { page: Number, itemsPerPage: Number } - "update-paging": { page: Number, itemsPerPage: Number }
- Dispatched once immediately and then each time page or itemsPerPage changes - Dispatched once immediately and then each time page or itemsPerPage changes
--> -->
@ -60,7 +61,7 @@
itemsPerPage = Number(itemsPerPage); itemsPerPage = Number(itemsPerPage);
} }
dispatch("update", { itemsPerPage, page }); dispatch("update-paging", { itemsPerPage, page });
} }
$: backButtonDisabled = (page === 1); $: backButtonDisabled = (page === 1);
$: nextButtonDisabled = (page >= (totalItems / itemsPerPage)); $: nextButtonDisabled = (page >= (totalItems / itemsPerPage));

View File

@ -1,7 +1,16 @@
<!-- <!--
@component @component Histogram Plot based on uPlot Bars
Properties: Properties:
- Todo - `data [[],[]]`: uPlot data structure array ( [[],[]] == [X, Y] )
- `usesBins Bool?`: If X-Axis labels are bins ("XX-YY") [Default: false]
- `width Number?`: Plot width (reactively adaptive) [Default: 500]
- `height Number?`: Plot height (reactively adaptive) [Default: 300]
- `title String?`: Plot title [Default: ""]
- `xlabel String?`: Plot X axis label [Default: ""]
- `xunit String?`: Plot X axis unit [Default: ""]
- `ylabel String?`: Plot Y axis label [Default: ""]
- `yunit String?`: Plot Y axis unit [Default: ""]
--> -->
<script> <script>
@ -16,9 +25,9 @@
export let height = 300; export let height = 300;
export let title = ""; export let title = "";
export let xlabel = ""; export let xlabel = "";
export let xunit = "X"; export let xunit = "";
export let ylabel = ""; export let ylabel = "";
export let yunit = "Y"; export let yunit = "";
const { bars } = uPlot.paths; const { bars } = uPlot.paths;

View File

@ -1,5 +1,28 @@
<!--
@component Main plot component, based on uPlot; metricdata values by time
Only width/height should change reactively.
Properties:
- `metric String`: The metric name
- `scope String?`: Scope of the displayed data [Default: node]
- `resources [GraphQL.Resource]`: List of resources used for parent job
- `width Number`: The plot width
- `height Number`: The plot height
- `timestep Number`: The timestep used for X-axis rendering
- `series [GraphQL.Series]`: The metric data object
- `useStatsSeries Bool?`: If this plot uses the statistics Min/Max/Median representation; automatically set to according bool [Default: null]
- `statisticsSeries [GraphQL.StatisticsSeries]?`: Min/Max/Median representation of metric data [Default: null]
- `cluster GraphQL.Cluster`: Cluster Object of the parent job
- `subCluster String`: Name of the subCluster of the parent job
- `isShared Bool?`: If this job used shared resources; will adapt threshold indicators accordingly [Default: false]
- `forNode Bool?`: If this plot is used for node data display; will render x-axis as negative time with $now as maximum [Default: false]
- `numhwthreads Number?`: Number of job HWThreads [Default: 0]
- `numaccs Number?`: Number of job Accelerators [Default: 0]
-->
<script context="module"> <script context="module">
export function formatTime(t, forNode = false) { function formatTime(t, forNode = false) {
if (t !== null) { if (t !== null) {
if (isNaN(t)) { if (isNaN(t)) {
return t; return t;
@ -15,7 +38,7 @@
} }
} }
export function timeIncrs(timestep, maxX, forNode) { function timeIncrs(timestep, maxX, forNode) {
if (forNode === true) { if (forNode === true) {
return [60, 300, 900, 1800, 3600, 7200, 14400, 21600]; // forNode fixed increments return [60, 300, 900, 1800, 3600, 7200, 14400, 21600]; // forNode fixed increments
} else { } else {
@ -27,118 +50,66 @@
} }
} }
export function findThresholds( // removed arg "subcluster": input metricconfig and topology now directly derived from subcluster
function findThresholds(
subClusterTopology,
metricConfig, metricConfig,
scope, scope,
subCluster,
isShared, isShared,
numhwthreads, numhwthreads,
numaccs numaccs
) { ) {
// console.log('NAME ' + metricConfig.name + ' / SCOPE ' + scope + ' / SUBCLUSTER ' + subCluster.name)
if (!metricConfig || !scope || !subCluster) { if (!subClusterTopology || !metricConfig || !scope) {
console.warn("Argument missing for findThresholds!"); console.warn("Argument missing for findThresholds!");
return null; return null;
} }
if ( if (
(scope == "node" && isShared == false) || (scope == "node" && isShared == false) ||
metricConfig.aggregation == "avg" metricConfig?.aggregation == "avg"
) { ) {
if (metricConfig.subClusters && metricConfig.subClusters.length === 0) {
// console.log('subClusterConfigs array empty, use metricConfig defaults')
return { return {
normal: metricConfig.normal, normal: metricConfig.normal,
caution: metricConfig.caution, caution: metricConfig.caution,
alert: metricConfig.alert, alert: metricConfig.alert,
peak: metricConfig.peak, peak: metricConfig.peak,
}; };
} else if ( }
metricConfig.subClusters &&
metricConfig.subClusters.length > 0
) { if (metricConfig?.aggregation == "sum") {
// console.log('subClusterConfigs found, use subCluster Settings if matching jobs subcluster:') let divisor = 1
let forSubCluster = metricConfig.subClusters.find( if (isShared == true) { // Shared
(sc) => sc.name == subCluster.name, if (numaccs > 0) divisor = subClusterTopology.accelerators.length / numaccs;
); else if (numhwthreads > 0) divisor = subClusterTopology.node.length / numhwthreads;
if ( }
forSubCluster && else if (scope == 'socket') divisor = subClusterTopology.socket.length;
forSubCluster.normal && else if (scope == "core") divisor = subClusterTopology.core.length;
forSubCluster.caution && else if (scope == "accelerator")
forSubCluster.alert && divisor = subClusterTopology.accelerators.length;
forSubCluster.peak else if (scope == "hwthread") divisor = subClusterTopology.node.length;
) else {
return forSubCluster; // console.log('TODO: how to calc thresholds for ', scope)
else
return {
normal: metricConfig.normal,
caution: metricConfig.caution,
alert: metricConfig.alert,
peak: metricConfig.peak,
};
} else {
console.warn("metricConfig.subClusters not found!");
return null; return null;
} }
return {
peak: metricConfig.peak / divisor,
normal: metricConfig.normal / divisor,
caution: metricConfig.caution / divisor,
alert: metricConfig.alert / divisor,
};
} }
if (metricConfig.aggregation != "sum") { console.warn(
console.warn( "Missing or unkown aggregation mode (sum/avg) for metric:",
"Missing or unkown aggregation mode (sum/avg) for metric:", metricConfig,
metricConfig, );
); return null;
return null;
}
let divisor = 1
if (isShared == true) { // Shared
if (numaccs > 0) divisor = subCluster.topology.accelerators.length / numaccs;
else if (numhwthreads > 0) divisor = subCluster.topology.node.length / numhwthreads;
}
else if (scope == 'socket') divisor = subCluster.topology.socket.length;
else if (scope == "core") divisor = subCluster.topology.core.length;
else if (scope == "accelerator")
divisor = subCluster.topology.accelerators.length;
else if (scope == "hwthread") divisor = subCluster.topology.node.length;
else {
// console.log('TODO: how to calc thresholds for ', scope)
return null;
}
let mc =
metricConfig?.subClusters?.find((sc) => sc.name == subCluster.name) ||
metricConfig;
return {
peak: mc.peak / divisor,
normal: mc.normal / divisor,
caution: mc.caution / divisor,
alert: mc.alert / divisor,
};
} }
</script> </script>
<!--
@component
Only width/height should change reactively.
Properties:
- width: Number
- height: Number
- timestep: Number
- series: [GraphQL.Series]
- statisticsSeries: [GraphQL.StatisticsSeries]
- cluster: GraphQL.Cluster
- subCluster: String
- metric: String
- scope: String
- useStatsSeries: Boolean
Functions:
- setTimeRange(from, to): Void
// TODO: Move helper functions to module context?
-->
<script> <script>
import uPlot from "uplot"; import uPlot from "uplot";
import { formatNumber } from "../units.js"; import { formatNumber } from "../units.js";
@ -165,7 +136,8 @@
if (useStatsSeries == false && series == null) useStatsSeries = true; if (useStatsSeries == false && series == null) useStatsSeries = true;
const metricConfig = getContext("metrics")(cluster, metric); const subClusterTopology = getContext("getHardwareTopology")(cluster, subCluster);
const metricConfig = getContext("getMetricConfig")(cluster, subCluster, metric);
const clusterCockpitConfig = getContext("cc-config"); const clusterCockpitConfig = getContext("cc-config");
const resizeSleepTime = 250; const resizeSleepTime = 250;
const normalLineColor = "#000000"; const normalLineColor = "#000000";
@ -178,11 +150,9 @@
alert: "rgba(255, 0, 0, 0.3)", alert: "rgba(255, 0, 0, 0.3)",
}; };
const thresholds = findThresholds( const thresholds = findThresholds(
subClusterTopology,
metricConfig, metricConfig,
scope, scope,
typeof subCluster == "string"
? cluster.subClusters.find((sc) => sc.name == subCluster)
: subCluster,
isShared, isShared,
numhwthreads, numhwthreads,
numaccs numaccs
@ -479,8 +449,6 @@
cursor: { drag: { x: true, y: true } }, cursor: { drag: { x: true, y: true } },
}; };
// console.log(opts)
let plotWrapper = null; let plotWrapper = null;
let uplot = null; let uplot = null;
let timeoutId = null; let timeoutId = null;
@ -531,14 +499,6 @@
if (timeoutId != null) clearTimeout(timeoutId); if (timeoutId != null) clearTimeout(timeoutId);
}); });
// `from` and `to` must be numbers between 0 and 1.
export function setTimeRange(from, to) {
if (!uplot || from > to) return false;
uplot.setScale("x", { min: from * maxX, max: to * maxX });
return true;
}
</script> </script>
{#if series[0].data.length > 0} {#if series[0].data.length > 0}

View File

@ -1,3 +1,17 @@
<!--
@component Pie Plot based on uPlot Pie
Properties:
- `size Number`: X and Y size of the plot, for square shape
- `sliceLabel String`: Label used in segment legends
- `quantities [Number]`: Data values
- `entities [String]`: Data identifiers
- `displayLegend?`: Display uPlot legend [Default: false]
Exported:
- `colors ['rgb(x,y,z)', ...]`: Color range used for segments; upstream used for legend
-->
<script context="module"> <script context="module">
// http://tsitsul.in/blog/coloropt/ : 12 colors normal // http://tsitsul.in/blog/coloropt/ : 12 colors normal
export const colors = [ export const colors = [

View File

@ -1,3 +1,14 @@
<!--
@component Polar Plot based on chartJS Radar
Properties:
- `metrics [String]`: Metric names to display as polar plot
- `cluster GraphQL.Cluster`: Cluster Object of the parent job
- `subCluster GraphQL.SubCluster`: SubCluster Object of the parent job
- `jobMetrics [GraphQL.JobMetricWithName]`: Metric data
- `height Number?`: Plot height [Default: 365]
-->
<script> <script>
import { getContext } from 'svelte' import { getContext } from 'svelte'
import { Radar } from 'svelte-chartjs'; import { Radar } from 'svelte-chartjs';
@ -24,10 +35,11 @@
export let metrics export let metrics
export let cluster export let cluster
export let subCluster
export let jobMetrics export let jobMetrics
export let height = 365 export let height = 365
const metricConfig = getContext('metrics') const getMetricConfig = getContext("getMetricConfig")
const labels = metrics.filter(name => { const labels = metrics.filter(name => {
if (!jobMetrics.find(m => m.name == name && m.scope == "node")) { if (!jobMetrics.find(m => m.name == name && m.scope == "node")) {
@ -38,7 +50,7 @@
}) })
const getValuesForStat = (getStat) => labels.map(name => { const getValuesForStat = (getStat) => labels.map(name => {
const peak = metricConfig(cluster, name).peak const peak = getMetricConfig(cluster, subCluster, name).peak
const metric = jobMetrics.find(m => m.name == name && m.scope == "node") const metric = jobMetrics.find(m => m.name == name && m.scope == "node")
const value = getStat(metric.metric) / peak const value = getStat(metric.metric) / peak
return value <= 1. ? value : 1. return value <= 1. ? value : 1.

View File

@ -1,3 +1,27 @@
<!--
@component Roofline Model Plot based on uPlot
Properties:
- `data [null, [], []]`: Roofline Data Structure, see below for details [Default: null]
- `renderTime Bool?`: If time information should be rendered as colored dots [Default: false]
- `allowSizeChange Bool?`: If dimensions of rendered plot can change [Default: false]
- `subCluster GraphQL.SubCluster?`: SubCluster Object; contains required topology information [Default: null]
- `width Number?`: Plot width (reactively adaptive) [Default: 600]
- `height Number?`: Plot height (reactively adaptive) [Default: 350]
Data Format:
- `data = [null, [], []]`
- Index 0: null-axis required for scatter
- Index 1: Array of XY-Arrays for Scatter
- Index 2: Optional Time Info
- `data[1][0] = [100, 200, 500, ...]`
- X Axis: Intensity (Vals up to clusters' flopRateScalar value)
- `data[1][1] = [1000, 2000, 1500, ...]`
- Y Axis: Performance (Vals up to clusters' flopRateSimd value)
- `data[2] = [0.1, 0.15, 0.2, ...]`
- Color Code: Time Information (Floats from 0 to 1) (Optional)
-->
<script> <script>
import uPlot from "uplot"; import uPlot from "uplot";
import { formatNumber } from "../units.js"; import { formatNumber } from "../units.js";
@ -7,7 +31,7 @@
export let data = null; export let data = null;
export let renderTime = false; export let renderTime = false;
export let allowSizeChange = false; export let allowSizeChange = false;
export let cluster = null; export let subCluster = null;
export let width = 600; export let width = 600;
export let height = 350; export let height = 350;
@ -17,12 +41,7 @@
const lineWidth = clusterCockpitConfig.plot_general_lineWidth; const lineWidth = clusterCockpitConfig.plot_general_lineWidth;
/* Data Format
* data = [null, [], []] // 0: null-axis required for scatter, 1: Array of XY-Array for Scatter, 2: Optional Time Info
* data[1][0] = [100, 200, 500, ...] // X Axis -> Intensity (Vals up to clusters' flopRateScalar value)
* data[1][1] = [1000, 2000, 1500, ...] // Y Axis -> Performance (Vals up to clusters' flopRateSimd value)
* data[2] = [0.1, 0.15, 0.2, ...] // Color Code -> Time Information (Floats from 0 to 1) (Optional)
*/
// Helpers // Helpers
function getGradientR(x) { function getGradientR(x) {
@ -189,8 +208,8 @@
y: { y: {
range: [ range: [
1.0, 1.0,
cluster?.flopRateSimd?.value subCluster?.flopRateSimd?.value
? nearestThousand(cluster.flopRateSimd.value) ? nearestThousand(subCluster.flopRateSimd.value)
: 10000, : 10000,
], ],
distr: 3, // Render as log distr: 3, // Render as log
@ -208,38 +227,34 @@
], ],
draw: [ draw: [
(u) => { (u) => {
// draw roofs when cluster set // draw roofs when subCluster set
// console.log(u) if (subCluster != null) {
if (cluster != null) {
const padding = u._padding; // [top, right, bottom, left] const padding = u._padding; // [top, right, bottom, left]
u.ctx.strokeStyle = "black"; u.ctx.strokeStyle = "black";
u.ctx.lineWidth = lineWidth; u.ctx.lineWidth = lineWidth;
u.ctx.beginPath(); u.ctx.beginPath();
const ycut = 0.01 * cluster.memoryBandwidth.value; const ycut = 0.01 * subCluster.memoryBandwidth.value;
const scalarKnee = const scalarKnee =
(cluster.flopRateScalar.value - ycut) / (subCluster.flopRateScalar.value - ycut) /
cluster.memoryBandwidth.value; subCluster.memoryBandwidth.value;
const simdKnee = const simdKnee =
(cluster.flopRateSimd.value - ycut) / (subCluster.flopRateSimd.value - ycut) /
cluster.memoryBandwidth.value; subCluster.memoryBandwidth.value;
const scalarKneeX = u.valToPos(scalarKnee, "x", true), // Value, axis, toCanvasPixels const scalarKneeX = u.valToPos(scalarKnee, "x", true), // Value, axis, toCanvasPixels
simdKneeX = u.valToPos(simdKnee, "x", true), simdKneeX = u.valToPos(simdKnee, "x", true),
flopRateScalarY = u.valToPos( flopRateScalarY = u.valToPos(
cluster.flopRateScalar.value, subCluster.flopRateScalar.value,
"y", "y",
true, true,
), ),
flopRateSimdY = u.valToPos( flopRateSimdY = u.valToPos(
cluster.flopRateSimd.value, subCluster.flopRateSimd.value,
"y", "y",
true, true,
); );
// Debug get zoomLevel from browser
// console.log("Zoom", Math.round(window.devicePixelRatio * 100))
if ( if (
scalarKneeX < scalarKneeX <
width * window.devicePixelRatio - width * window.devicePixelRatio -
@ -323,7 +338,7 @@
}; };
uplot = new uPlot(opts, plotData, plotWrapper); uplot = new uPlot(opts, plotData, plotWrapper);
} else { } else {
console.log("No data for roofline!"); // console.log("No data for roofline!");
} }
} }

View File

@ -1,6 +1,14 @@
<div class="cc-plot"> <!--
<canvas bind:this={canvasElement} width="{prevWidth}" height="{prevHeight}"></canvas> @component Roofline Model Plot as Heatmap of multiple Jobs based on Canvas
</div>
Properties:
- `subCluster GraphQL.SubCluster?`: SubCluster Object; contains required topology information [Default: null]
- **Note**: Object of first subCluster is used, how to handle multiple topologies within one cluster? [TODO]
- `tiles [[Float!]!]?`: Data tiles to be rendered [Default: null]
- `maxY Number?`: maximum flopRateSimd of all subClusters [Default: null]
- `width Number?`: Plot width (reactively adaptive) [Default: 500]
- `height Number?`: Plot height (reactively adaptive) [Default: 300]
-->
<script context="module"> <script context="module">
const axesColor = '#aaaaaa' const axesColor = '#aaaaaa'
@ -33,11 +41,11 @@
return 2 return 2
} }
function render(ctx, data, cluster, width, height, defaultMaxY) { function render(ctx, data, subCluster, width, height, defaultMaxY) {
if (width <= 0) if (width <= 0)
return return
const [minX, maxX, minY, maxY] = [0.01, 1000, 1., cluster?.flopRateSimd?.value || defaultMaxY] const [minX, maxX, minY, maxY] = [0.01, 1000, 1., subCluster?.flopRateSimd?.value || defaultMaxY]
const w = width - paddingLeft - paddingRight const w = width - paddingLeft - paddingRight
const h = height - paddingTop - paddingBottom const h = height - paddingTop - paddingBottom
@ -138,14 +146,14 @@
ctx.strokeStyle = 'black' ctx.strokeStyle = 'black'
ctx.lineWidth = 2 ctx.lineWidth = 2
ctx.beginPath() ctx.beginPath()
if (cluster != null) { if (subCluster != null) {
const ycut = 0.01 * cluster.memoryBandwidth.value const ycut = 0.01 * subCluster.memoryBandwidth.value
const scalarKnee = (cluster.flopRateScalar.value - ycut) / cluster.memoryBandwidth.value const scalarKnee = (subCluster.flopRateScalar.value - ycut) / subCluster.memoryBandwidth.value
const simdKnee = (cluster.flopRateSimd.value - ycut) / cluster.memoryBandwidth.value const simdKnee = (subCluster.flopRateSimd.value - ycut) / subCluster.memoryBandwidth.value
const scalarKneeX = getCanvasX(scalarKnee), const scalarKneeX = getCanvasX(scalarKnee),
simdKneeX = getCanvasX(simdKnee), simdKneeX = getCanvasX(simdKnee),
flopRateScalarY = getCanvasY(cluster.flopRateScalar.value), flopRateScalarY = getCanvasY(subCluster.flopRateScalar.value),
flopRateSimdY = getCanvasY(cluster.flopRateSimd.value) flopRateSimdY = getCanvasY(subCluster.flopRateSimd.value)
if (scalarKneeX < width - paddingRight) { if (scalarKneeX < width - paddingRight) {
ctx.moveTo(scalarKneeX, flopRateScalarY) ctx.moveTo(scalarKneeX, flopRateScalarY)
@ -182,7 +190,7 @@
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { formatNumber } from '../units.js' import { formatNumber } from '../units.js'
export let cluster = null export let subCluster = null
export let tiles = null export let tiles = null
export let maxY = null export let maxY = null
export let width = 500 export let width = 500
@ -206,7 +214,7 @@
canvasElement.width = width canvasElement.width = width
canvasElement.height = height canvasElement.height = height
render(ctx, data, cluster, width, height, maxY) render(ctx, data, subCluster, width, height, maxY)
}) })
let timeoutId = null let timeoutId = null
@ -226,9 +234,13 @@
timeoutId = null timeoutId = null
canvasElement.width = width canvasElement.width = width
canvasElement.height = height canvasElement.height = height
render(ctx, data, cluster, width, height, maxY) render(ctx, data, subCluster, width, height, maxY)
}, 250) }, 250)
} }
$: sizeChanged(width, height) $: sizeChanged(width, height)
</script> </script>
<div class="cc-plot">
<canvas bind:this={canvasElement} width="{prevWidth}" height="{prevHeight}"></canvas>
</div>

View File

@ -1,6 +1,16 @@
<div class="cc-plot"> <!--
<canvas bind:this={canvasElement} width="{width}" height="{height}"></canvas> @component Scatter plot of two metrics at identical timesteps, based on canvas
</div>
Properties:
- `X [Number]`: Data from first selected metric as X-values
- `Y [Number]`: Data from second selected metric as Y-values
- `S GraphQl.TimeWeights.X?`: Float to scale the data with [Default: null]
- `color String`: Color of the drawn scatter circles
- `width Number`:
- `height Number`:
- `xLabel String`:
- `yLabel String`:
-->
<script context="module"> <script context="module">
import { formatNumber } from '../units.js' import { formatNumber } from '../units.js'
@ -169,3 +179,7 @@
$: sizeChanged(width, height); $: sizeChanged(width, height);
</script> </script>
<div class="cc-plot">
<canvas bind:this={canvasElement} width="{width}" height="{height}"></canvas>
</div>

View File

@ -4,13 +4,14 @@ Originally created by Michael Keller (https://github.com/mhkeller/svelte-double-
Changes: remove dependency, text inputs, configurable value ranges, on:change event Changes: remove dependency, text inputs, configurable value ranges, on:change event
--> -->
<!-- <!--
@component @component Selector component to display range selections via min and max double-sliders
Properties: Properties:
- min: Number - min: Number
- max: Number - max: Number
- firstSlider: Number (Starting position of slider #1) - firstSlider: Number (Starting position of slider #1)
- secondSlider: Number (Starting position of slider #2) - secondSlider: Number (Starting position of slider #2)
Events: Events:
- `change`: [Number, Number] (Positions of the two sliders) - `change`: [Number, Number] (Positions of the two sliders)
--> -->

View File

@ -1,4 +1,14 @@
<!--
@component Selector component for (footprint) metrics to be displayed as histogram
Properties:
- `cluster String`: Currently selected cluster
- `metricsInHistograms [String]`: The currently selected metrics to display as histogram
- ìsOpen Bool`: Is selection opened
-->
<script> <script>
import { getContext } from "svelte";
import { import {
Modal, Modal,
ModalBody, ModalBody,
@ -14,9 +24,18 @@
export let metricsInHistograms; export let metricsInHistograms;
export let isOpen; 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 client = getContextClient();
const initialized = getContext("initialized");
let availableMetrics = []
function loadHistoMetrics(isInitialized) {
if (!isInitialized) return;
const rawAvailableMetrics = getContext("globalMetrics").filter((gm) => gm?.footprint).map((fgm) => { return fgm.name })
availableMetrics = [...rawAvailableMetrics]
}
let pendingMetrics = [...metricsInHistograms]; // Copy
const updateConfigurationMutation = ({ name, value }) => { const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({ return mutationStore({
@ -37,7 +56,6 @@
}).subscribe((res) => { }).subscribe((res) => {
if (res.fetching === false && res.error) { if (res.fetching === false && res.error) {
throw res.error; throw res.error;
// console.log('Error on subscription: ' + res.error)
} }
}); });
} }
@ -52,6 +70,9 @@
value: metricsInHistograms, value: metricsInHistograms,
}); });
} }
$: loadHistoMetrics($initialized);
</script> </script>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}> <Modal {isOpen} toggle={() => (isOpen = !isOpen)}>

View File

@ -1,13 +1,18 @@
<!-- <!--
@component @component Metric selector component; allows reorder via drag and drop
Properties: Properties:
- metrics: [String] (changes from inside, needs to be initialised, list of selected metrics) - `metrics [String]`: (changes from inside, needs to be initialised, list of selected metrics)
- isOpen: Boolean (can change from inside and outside) - `isOpen Bool`: (can change from inside and outside)
- configName: String (constant) - `configName String`: The config key for the last saved selection (constant)
- `allMetrics [String]?`: List of all available metrics [Default: null]
- `cluster String?`: The currently selected cluster [Default: null]
- `showFootprint Bool?`: Upstream state of wether to render footpritn card [Default: false]
- `footprintSelect Bool?`: Render checkbox for footprint display in upstream component [Default: false]
--> -->
<script> <script>
import { getContext } from "svelte";
import { import {
Modal, Modal,
ModalBody, ModalBody,
@ -16,7 +21,6 @@
Button, Button,
ListGroup, ListGroup,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { getContext } from "svelte";
import { gql, getContextClient, mutationStore } from "@urql/svelte"; import { gql, getContextClient, mutationStore } from "@urql/svelte";
export let metrics; export let metrics;
@ -25,10 +29,10 @@
export let allMetrics = null; export let allMetrics = null;
export let cluster = null; export let cluster = null;
export let showFootprint = false; export let showFootprint = false;
export let view = "job"; export let footprintSelect = false;
const clusters = getContext("clusters"), const onInit = getContext("on-init")
onInit = getContext("on-init"); const globalMetrics = getContext("globalMetrics")
let newMetricsOrder = []; let newMetricsOrder = [];
let unorderedMetrics = [...metrics]; let unorderedMetrics = [...metrics];
@ -36,30 +40,34 @@
onInit(() => { onInit(() => {
if (allMetrics == null) allMetrics = new Set(); if (allMetrics == null) allMetrics = new Set();
for (let c of clusters) for (let metric of globalMetrics) allMetrics.add(metric.name);
for (let metric of c.metricConfig) allMetrics.add(metric.name);
}); });
$: { $: {
if (allMetrics != null) { if (allMetrics != null) {
if (cluster == null) { if (cluster == null) {
// console.log('Reset to full metric list') for (let metric of globalMetrics) allMetrics.add(metric.name);
for (let c of clusters)
for (let metric of c.metricConfig) allMetrics.add(metric.name);
} else { } else {
// console.log('Recalculate available metrics for ' + cluster)
allMetrics.clear(); allMetrics.clear();
for (let c of clusters) for (let gm of globalMetrics) {
if (c.name == cluster) if (gm.availability.find((av) => av.cluster === cluster)) allMetrics.add(gm.name);
for (let metric of c.metricConfig) allMetrics.add(metric.name); }
} }
newMetricsOrder = [...allMetrics].filter((m) => !metrics.includes(m)); newMetricsOrder = [...allMetrics].filter((m) => !metrics.includes(m));
newMetricsOrder.unshift(...metrics.filter((m) => allMetrics.has(m))); newMetricsOrder.unshift(...metrics.filter((m) => allMetrics.has(m)));
unorderedMetrics = unorderedMetrics.filter((m) => allMetrics.has(m)); unorderedMetrics = unorderedMetrics.filter((m) => allMetrics.has(m));
} }
} }
function printAvailability(metric, cluster) {
const avail = globalMetrics.find((gm) => gm.name === metric)?.availability
if (cluster == null) {
return avail.map((av) => av.cluster).join(',')
} else {
return avail.find((av) => av.cluster === cluster).subClusters.join(',')
}
}
const client = getContextClient(); const client = getContextClient();
const updateConfigurationMutation = ({ name, value }) => { const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({ return mutationStore({
@ -106,7 +114,6 @@
}).subscribe((res) => { }).subscribe((res) => {
if (res.fetching === false && res.error) { if (res.fetching === false && res.error) {
throw res.error; throw res.error;
// console.log('Error on subscription: ' + res.error)
} }
}); });
@ -118,7 +125,6 @@
value: JSON.stringify(showFootprint), value: JSON.stringify(showFootprint),
}).subscribe((res) => { }).subscribe((res) => {
if (res.fetching === false && res.error) { if (res.fetching === false && res.error) {
console.log("Error on footprint subscription: " + res.error);
throw res.error; throw res.error;
} }
}); });
@ -129,7 +135,7 @@
<ModalHeader>Configure columns (Metric availability shown)</ModalHeader> <ModalHeader>Configure columns (Metric availability shown)</ModalHeader>
<ModalBody> <ModalBody>
<ListGroup> <ListGroup>
{#if view === "list"} {#if footprintSelect}
<li class="list-group-item"> <li class="list-group-item">
<input type="checkbox" bind:checked={pendingShowFootprint} /> Show Footprint <input type="checkbox" bind:checked={pendingShowFootprint} /> Show Footprint
</li> </li>
@ -161,34 +167,7 @@
{/if} {/if}
{metric} {metric}
<span style="float: right;"> <span style="float: right;">
{cluster == null {printAvailability(metric, cluster)}
? clusters // No single cluster specified: List Clusters with Metric
.filter(
(c) => c.metricConfig.find((m) => m.name == metric) != null,
)
.map((c) => c.name)
.join(", ")
: clusters // Single cluster requested: List Subclusters with do not have metric remove flag
.filter((c) => c.name == cluster)
.filter(
(c) => c.metricConfig.find((m) => m.name == metric) != null,
)
.map(function (c) {
let scNames = c.subClusters.map((sc) => sc.name);
scNames.forEach(function (scName) {
let met = c.metricConfig.find((m) => m.name == metric);
let msc = met.subClusters.find(
(msc) => msc.name == scName,
);
if (msc != null) {
if (msc.remove == true) {
scNames = scNames.filter((scn) => scn != msc.name);
}
}
});
return scNames;
})
.join(", ")}
</span> </span>
</li> </li>
{/each} {/each}

View File

@ -1,5 +1,5 @@
<!-- <!--
@component @component Selector for sorting field and direction
Properties: Properties:
- sorting: { field: String, order: "DESC" | "ASC" } (changes from inside) - sorting: { field: String, order: "DESC" | "ASC" } (changes from inside)
@ -7,6 +7,7 @@
--> -->
<script> <script>
import { getContext } from "svelte";
import { import {
Icon, Icon,
Button, Button,
@ -17,24 +18,38 @@
ModalHeader, ModalHeader,
ModalFooter, ModalFooter,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { getSortItems } from "../utils.js";
export let isOpen = false; export let isOpen = false;
export let sorting = { field: "startTime", order: "DESC" }; export let sorting = { field: "startTime", type: "col", order: "DESC" };
let sortableColumns = [ let sortableColumns = [];
{ field: "startTime", text: "Start Time", order: "DESC" }, let activeColumnIdx;
{ field: "duration", text: "Duration", order: "DESC" },
{ field: "numNodes", text: "Number of Nodes", order: "DESC" },
{ field: "memUsedMax", text: "Max. Memory Used", order: "DESC" },
{ field: "flopsAnyAvg", text: "Avg. FLOPs", order: "DESC" },
{ field: "memBwAvg", text: "Avg. Memory Bandwidth", order: "DESC" },
{ field: "netBwAvg", text: "Avg. Network Bandwidth", order: "DESC" },
];
let activeColumnIdx = sortableColumns.findIndex( const initialized = getContext("initialized");
(col) => col.field == sorting.field,
); function loadSortables(isInitialized) {
sortableColumns[activeColumnIdx].order = sorting.order; if (!isInitialized) return;
sortableColumns = [
{ field: "startTime", type: "col", text: "Start Time", order: "DESC" },
{ field: "duration", type: "col", text: "Duration", order: "DESC" },
{ field: "numNodes", type: "col", text: "Number of Nodes", order: "DESC" },
{ field: "numHwthreads", type: "col", text: "Number of HWThreads", order: "DESC" },
{ field: "numAcc", type: "col", text: "Number of Accelerators", order: "DESC" },
...getSortItems()
]
}
function loadActiveIndex(isInitialized) {
if (!isInitialized) return;
activeColumnIdx = sortableColumns.findIndex(
(col) => col.field == sorting.field,
);
sortableColumns[activeColumnIdx].order = sorting.order;
}
$: loadSortables($initialized);
$: loadActiveIndex($initialized)
</script> </script>
<Modal <Modal
@ -62,7 +77,7 @@
sortableColumns[i] = { ...sortableColumns[i] }; sortableColumns[i] = { ...sortableColumns[i] };
activeColumnIdx = i; activeColumnIdx = i;
sortableColumns = [...sortableColumns]; sortableColumns = [...sortableColumns];
sorting = { field: col.field, order: col.order }; sorting = { field: col.field, type: col.type, order: col.order };
}} }}
> >
<Icon <Icon

View File

@ -1,16 +1,28 @@
<!--
@component Selector for specified real time ranges for data cutoff; used in systems and nodes view
Properties:
- `from Date`: The datetime to start data display from
- `to Date`: The datetime to end data display at
- `customEnabled Bool?`: Allow custom time window selection [Default: true]
- `options Object? {String:Number}`: The quick time selection options [Default: {..., "Last 24hrs": 24*60*60}]
Events:
- `change, {Date, Date}`: Set 'from, to' values in upstream component
-->
<script> <script>
import { createEventDispatcher } from "svelte";
import { import {
Icon, Icon,
Input, Input,
InputGroup, InputGroup,
InputGroupText, InputGroupText,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { createEventDispatcher } from "svelte";
export let from; export let from;
export let to; export let to;
export let customEnabled = true; export let customEnabled = true;
export let anyEnabled = false;
export let options = { export let options = {
"Last quarter hour": 15 * 60, "Last quarter hour": 15 * 60,
"Last half hour": 30 * 60, "Last half hour": 30 * 60,
@ -25,21 +37,15 @@
$: pendingTo = to; $: pendingTo = to;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
let timeRange = let timeRange = // If both times set, return diff, else: display custom select
to && from ? (to.getTime() - from.getTime()) / 1000 : anyEnabled ? -2 : -1; (to && from) ? ((to.getTime() - from.getTime()) / 1000) : -1;
function updateTimeRange(event) { function updateTimeRange() {
if (timeRange == -1) { if (timeRange == -1) {
pendingFrom = null; pendingFrom = null;
pendingTo = null; pendingTo = null;
return; return;
} }
if (timeRange == -2) {
from = pendingFrom = null;
to = pendingTo = null;
dispatch("change", { from, to });
return;
}
let now = Date.now(), let now = Date.now(),
t = timeRange * 1000; t = timeRange * 1000;
@ -63,9 +69,6 @@
<InputGroup class="inline-from"> <InputGroup class="inline-from">
<InputGroupText><Icon name="clock-history" /></InputGroupText> <InputGroupText><Icon name="clock-history" /></InputGroupText>
<!-- <InputGroupText>
Time
</InputGroupText> -->
<select <select
class="form-select" class="form-select"
bind:value={timeRange} bind:value={timeRange}
@ -74,9 +77,6 @@
{#if customEnabled} {#if customEnabled}
<option value={-1}>Custom</option> <option value={-1}>Custom</option>
{/if} {/if}
{#if anyEnabled}
<option value={-2}>Any</option>
{/if}
{#each Object.entries(options) as [name, seconds]} {#each Object.entries(options) as [name, seconds]}
<option value={seconds}>{name}</option> <option value={seconds}>{name}</option>
{/each} {/each}

View File

@ -31,3 +31,4 @@ export function scaleNumbers(x, y , p = '') {
return Math.abs(rawYValue) >= 1000 ? `${rawXValue.toExponential()} / ${rawYValue.toExponential()}` : `${rawYValue.toString()} / ${rawYValue.toString()}` return Math.abs(rawYValue) >= 1000 ? `${rawXValue.toExponential()} / ${rawYValue.toExponential()}` : `${rawYValue.toString()} / ${rawYValue.toString()}`
} }
// export const dateToUnixEpoch = (rfc3339) => Math.floor(Date.parse(rfc3339) / 1000);

View File

@ -6,7 +6,6 @@ import {
} from "@urql/svelte"; } from "@urql/svelte";
import { setContext, getContext, hasContext, onDestroy, tick } from "svelte"; import { setContext, getContext, hasContext, onDestroy, tick } from "svelte";
import { readable } from "svelte/store"; import { readable } from "svelte/store";
// import { formatNumber } from './units.js'
/* /*
* Call this function only at component initialization time! * Call this function only at component initialization time!
@ -16,7 +15,9 @@ import { readable } from "svelte/store";
* - Creates a readable store 'initialization' which indicates when the values below can be used. * - Creates a readable store 'initialization' which indicates when the values below can be used.
* - Adds 'tags' to the context (list of all tags) * - Adds 'tags' to the context (list of all tags)
* - Adds 'clusters' to the context (object with cluster names as keys) * - Adds 'clusters' to the context (object with cluster names as keys)
* - Adds 'metrics' to the context, a function that takes a cluster and metric name and returns the MetricConfig (or undefined) * - Adds 'globalMetrics' to the context (list of globally available metric infos)
* - Adds 'getMetricConfig' to the context, a function that takes a cluster, subCluster and metric name and returns the MetricConfig (or undefined)
* - Adds 'getHardwareTopology' to the context, a function that takes a cluster nad subCluster and returns the subCluster topology (or undefined)
*/ */
export function init(extraInitQuery = "") { export function init(extraInitQuery = "") {
const jwt = hasContext("jwt") const jwt = hasContext("jwt")
@ -71,11 +72,19 @@ export function init(extraInitQuery = "") {
normal normal
caution caution
alert alert
lowerIsBetter
} }
footprint footprint
} }
} }
tags { id, name, type } tags { id, name, type }
globalMetrics {
name
scope
footprint
unit { base, prefix }
availability { cluster, subClusters }
}
${extraInitQuery} ${extraInitQuery}
}` }`
) )
@ -91,21 +100,31 @@ export function init(extraInitQuery = "") {
}; };
}; };
const tags = [], const tags = []
clusters = []; const clusters = []
const allMetrics = []; const globalMetrics = []
setContext("tags", tags); setContext("tags", tags);
setContext("clusters", clusters); setContext("clusters", clusters);
setContext("allmetrics", allMetrics); setContext("globalMetrics", globalMetrics);
setContext("getMetricConfig", (cluster, subCluster, metric) => { setContext("getMetricConfig", (cluster, subCluster, metric) => {
// Load objects if input is string
if (typeof cluster !== "object") if (typeof cluster !== "object")
cluster = clusters.find((c) => c.name == cluster); cluster = clusters.find((c) => c.name == cluster);
if (typeof subCluster !== "object") if (typeof subCluster !== "object")
subCluster = cluster.subClusters.find((sc) => sc.name == subCluster); subCluster = cluster.subClusters.find((sc) => sc.name == subCluster);
return subCluster.metricConfig.find((m) => m.name == metric); return subCluster.metricConfig.find((m) => m.name == metric);
}); });
setContext("getHardwareTopology", (cluster, subCluster) => {
// Load objects if input is string
if (typeof cluster !== "object")
cluster = clusters.find((c) => c.name == cluster);
if (typeof subCluster !== "object")
subCluster = cluster.subClusters.find((sc) => sc.name == subCluster);
return subCluster?.topology;
});
setContext("on-init", (callback) => setContext("on-init", (callback) =>
state.fetching ? subscribers.push(callback) : callback(state) state.fetching ? subscribers.push(callback) : callback(state)
); );
@ -124,32 +143,11 @@ export function init(extraInitQuery = "") {
} }
for (let tag of data.tags) tags.push(tag); for (let tag of data.tags) tags.push(tag);
for (let cluster of data.clusters) clusters.push(cluster);
for (let gm of data.globalMetrics) globalMetrics.push(gm);
let globalmetrics = []; // Unified Sort
for (let cluster of data.clusters) { globalMetrics.sort((a, b) => a.name.localeCompare(b.name))
// Add full info to context object
clusters.push(cluster);
// Build global metric list with availability for joblist metricselect
for (let subcluster of cluster.subClusters) {
for (let scm of subcluster.metricConfig) {
let match = globalmetrics.find((gm) => gm.name == scm.name);
if (match) {
let submatch = match.availability.find((av) => av.cluster == cluster.name);
if (submatch) {
submatch.subclusters.push(subcluster.name)
} else {
match.availability.push({cluster: cluster.name, subclusters: [subcluster.name]})
}
} else {
globalmetrics.push({name: scm.name, availability: [{cluster: cluster.name, subclusters: [subcluster.name]}]});
}
}
}
}
// Add to ctx object
for (let gm of globalmetrics) allMetrics.push(gm);
console.log('All Metrics List', allMetrics);
state.data = data; state.data = data;
tick().then(() => subscribers.forEach((cb) => cb(state))); tick().then(() => subscribers.forEach((cb) => cb(state)));
@ -159,6 +157,7 @@ export function init(extraInitQuery = "") {
query: { subscribe }, query: { subscribe },
tags, tags,
clusters, clusters,
globalMetrics
}; };
} }
@ -171,6 +170,22 @@ function fuzzyMatch(term, string) {
return string.toLowerCase().includes(term); return string.toLowerCase().includes(term);
} }
// Use in filter() function to return only unique values
export function distinct(value, index, array) {
return array.indexOf(value) === index;
}
// Load Local Bool and Handle Scrambling of input string
export const scrambleNames = window.localStorage.getItem("cc-scramble-names");
export const scramble = function (str) {
if (str === "-") return str;
else
return [...str]
.reduce((x, c, i) => x * 7 + c.charCodeAt(0) * i * 21, 5)
.toString(32)
.substr(0, 6);
};
export function fuzzySearchTags(term, tags) { export function fuzzySearchTags(term, tags) {
if (!tags) return []; if (!tags) return [];
@ -260,56 +275,6 @@ export function minScope(scopes) {
return sm; return sm;
} }
export async function fetchMetrics(job, metrics, scopes) {
if (job.monitoringStatus == 0) return null;
let query = [];
if (metrics != null) {
for (let metric of metrics) {
query.push(`metric=${metric}`);
}
}
if (scopes != null) {
for (let scope of scopes) {
query.push(`scope=${scope}`);
}
}
try {
let res = await fetch(
`/frontend/jobs/metrics/${job.id}${query.length > 0 ? "?" : ""}${query.join(
"&"
)}`
);
if (res.status != 200) {
return { error: { status: res.status, message: await res.text() } };
}
return await res.json();
} catch (e) {
return { error: e };
}
}
export function fetchMetricsStore() {
let set = null;
let prev = { fetching: true, error: null, data: null };
return [
readable(prev, (_set) => {
set = _set;
}),
(job, metrics, scopes) =>
fetchMetrics(job, metrics, scopes).then((res) => {
let next = { fetching: false, error: res.error, data: res.data };
if (prev.data && next.data)
next.data.jobMetrics.push(...prev.data.jobMetrics);
prev = next;
set(next);
}),
];
}
export function stickyHeader(datatableHeaderSelector, updatePading) { export function stickyHeader(datatableHeaderSelector, updatePading) {
const header = document.querySelector("header > nav.navbar"); const header = document.querySelector("header > nav.navbar");
if (!header) return; if (!header) return;
@ -336,22 +301,98 @@ export function stickyHeader(datatableHeaderSelector, updatePading) {
onDestroy(() => document.removeEventListener("scroll", onscroll)); onDestroy(() => document.removeEventListener("scroll", onscroll));
} }
// Outdated: Frontend Will Now Receive final MetricList from backend
export function checkMetricDisabled(m, c, s) { //[m]etric, [c]luster, [s]ubcluster export function checkMetricDisabled(m, c, s) { //[m]etric, [c]luster, [s]ubcluster
const mc = getContext("metrics"); const metrics = getContext("globalMetrics");
const thisConfig = mc(c, m); const result = metrics?.find((gm) => gm.name === m)?.availability?.find((av) => av.cluster === c)?.subClusters?.includes(s)
let thisSCIndex = -1; return !result
if (thisConfig) { }
thisSCIndex = thisConfig.subClusters.findIndex(
(subcluster) => subcluster.name == s export function getStatsItems() {
); // console.time('stats')
}; // console.log('getStatsItems ...')
if (thisSCIndex >= 0) { const globalMetrics = getContext("globalMetrics")
if (thisConfig.subClusters[thisSCIndex].remove == true) { const result = globalMetrics.map((gm) => {
return true; if (gm?.footprint) {
// Footprint contains suffix naming the used stat-type
// console.time('deep')
// console.log('Deep Config for', gm.name)
const mc = getMetricConfigDeep(gm.name, null, null)
// console.timeEnd('deep')
return {
field: gm.name + '_' + gm.footprint,
text: gm.name + ' (' + gm.footprint + ')',
metric: gm.name,
from: 0,
to: mc.peak,
peak: mc.peak,
enabled: false
}
} }
return null
}).filter((r) => r != null)
// console.timeEnd('stats')
return [...result];
};
export function getSortItems() {
//console.time('sort')
//console.log('getSortItems ...')
const globalMetrics = getContext("globalMetrics")
const result = globalMetrics.map((gm) => {
if (gm?.footprint) {
// Footprint contains suffix naming the used stat-type
return {
field: gm.name + '_' + gm.footprint,
type: 'foot',
text: gm.name + ' (' + gm.footprint + ')',
order: 'DESC'
}
}
return null
}).filter((r) => r != null)
//console.timeEnd('sort')
return [...result];
};
function getMetricConfigDeep(metric, cluster, subCluster) {
const clusters = getContext("clusters");
if (cluster != null) {
let c = clusters.find((c) => c.name == cluster);
if (subCluster != null) {
let sc = c.subClusters.find((sc) => sc.name == subCluster);
return sc.metricConfig.find((mc) => mc.name == metric)
} else {
let result;
for (let sc of c.subClusters) {
const mc = sc.metricConfig.find((mc) => mc.name == metric)
if (result) { // If lowerIsBetter: Peak is still maximum value, no special case required
result.alert = (mc.alert > result.alert) ? mc.alert : result.alert
result.caution = (mc.caution > result.caution) ? mc.caution : result.caution
result.normal = (mc.normal > result.normal) ? mc.normal : result.normal
result.peak = (mc.peak > result.peak) ? mc.peak : result.peak
} else {
if (mc) result = {...mc};
}
}
return result
}
} else {
let result;
for (let c of clusters) {
for (let sc of c.subClusters) {
const mc = sc.metricConfig.find((mc) => mc.name == metric)
if (result) { // If lowerIsBetter: Peak is still maximum value, no special case required
result.alert = (mc.alert > result.alert) ? mc.alert : result.alert
result.caution = (mc.caution > result.caution) ? mc.caution : result.caution
result.normal = (mc.normal > result.normal) ? mc.normal : result.normal
result.peak = (mc.peak > result.peak) ? mc.peak : result.peak
} else {
if (mc) result = {...mc};
}
}
}
return result
} }
return false;
} }
export function convert2uplot(canvasData) { export function convert2uplot(canvasData) {
@ -413,14 +454,14 @@ export function binsFromFootprint(weights, scope, values, numBins) {
} }
export function transformDataForRoofline(flopsAny, memBw) { // Uses Metric Objects: {series:[{},{},...], timestep:60, name:$NAME} export function transformDataForRoofline(flopsAny, memBw) { // Uses Metric Objects: {series:[{},{},...], timestep:60, name:$NAME}
const nodes = flopsAny.series.length
const timesteps = flopsAny.series[0].data.length
/* c will contain values from 0 to 1 representing the time */ /* c will contain values from 0 to 1 representing the time */
let data = null let data = null
const x = [], y = [], c = [] const x = [], y = [], c = []
if (flopsAny && memBw) { if (flopsAny && memBw) {
const nodes = flopsAny.series.length
const timesteps = flopsAny.series[0].data.length
for (let i = 0; i < nodes; i++) { for (let i = 0; i < nodes; i++) {
const flopsData = flopsAny.series[i].data const flopsData = flopsAny.series[i].data
const memBwData = memBw.series[i].data const memBwData = memBw.series[i].data
@ -446,7 +487,7 @@ export function transformDataForRoofline(flopsAny, memBw) { // Uses Metric Objec
// Return something to be plotted. The argument shall be the result of the // Return something to be plotted. The argument shall be the result of the
// `nodeMetrics` GraphQL query. // `nodeMetrics` GraphQL query.
// Remove "hardcoded" here or deemed necessary? // Hardcoded metric names required for correct render
export function transformPerNodeDataForRoofline(nodes) { export function transformPerNodeDataForRoofline(nodes) {
let data = null let data = null
const x = [], y = [] const x = [], y = []

View File

@ -1,3 +1,11 @@
<!--
@component Navbar component; renders in app navigation links as received from upstream
Properties:
- `clusters [String]`: List of cluster names
- `links [Object]`: Pre-filtered link objects based on user auth
-->
<script> <script>
import { import {
Icon, Icon,
@ -8,8 +16,8 @@
DropdownItem, DropdownItem,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
export let clusters; // array of names export let clusters;
export let links; // array of nav links export let links;
</script> </script>
{#each links as item} {#each links as item}

View File

@ -1,3 +1,13 @@
<!--
@component Navbar component; renders in app resource links and user dropdown
Properties:
- `username String!`: Empty string if auth. is disabled, otherwise the username as string
- `authlevel Number`: The current users authentication level
- `roles [Number]`: Enum containing available roles
- `screenSize Number`: The current window size, will trigger different render variants
-->
<script> <script>
import { import {
Icon, Icon,
@ -12,10 +22,10 @@
Col, Col,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
export let username; // empty string if auth. is disabled, otherwise the username as string export let username;
export let authlevel; // Integer export let authlevel;
export let roles; // Role Enum-Like export let roles;
export let screenSize; // screensize export let screenSize;
</script> </script>
<Nav navbar> <Nav navbar>

View File

@ -1,31 +1,39 @@
<!--
@component Metric plot wrapper with user scope selection; used in job detail view
Properties:
- `job Object`: The GQL job object
- `metricName String`: The metrics name
- `metricUnit Object`: The metrics GQL unit object
- `nativeScope String`: The metrics native scope
- `scopes [String]`: The scopes returned for this metric
- `width Number`: Nested plot width
- `rawData [Object]`: Metric data for all scopes returned for this metric
- `isShared Bool?`: If this job used shared resources; will adapt threshold indicators accordingly in downstream plots [Default: false]
-->
<script> <script>
import { getContext, createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import Timeseries from "./plots/MetricPlot.svelte";
import { import {
InputGroup, InputGroup,
InputGroupText, InputGroupText,
Spinner, Spinner,
Card, Card,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { fetchMetrics, minScope } from "./utils"; import { minScope } from "../generic/utils";
import Timeseries from "../generic/plots/MetricPlot.svelte";
export let job; export let job;
export let metricName; export let metricName;
export let metricUnit;
export let nativeScope;
export let scopes; export let scopes;
export let width; export let width;
export let rawData; export let rawData;
export let isShared = false; export let isShared = false;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const cluster = getContext("clusters").find( const unit = (metricUnit?.prefix ? metricUnit.prefix : "") + (metricUnit?.base ? metricUnit.base : "")
(cluster) => cluster.name == job.cluster,
);
const subCluster = cluster.subClusters.find(
(subCluster) => subCluster.name == job.subCluster,
);
const metricConfig = cluster.metricConfig.find(
(metricConfig) => metricConfig.name == metricName,
);
let selectedHost = null, let selectedHost = null,
plot, plot,
@ -49,44 +57,12 @@
(series) => selectedHost == null || series.hostname == selectedHost, (series) => selectedHost == null || series.hostname == selectedHost,
); );
let from = null, $: if (selectedScope == "load-all") dispatch("load-all");
to = null;
export function setTimeRange(f, t) {
(from = f), (to = t);
}
$: if (plot != null) plot.setTimeRange(from, to);
export async function loadMore() {
fetching = true;
let response = await fetchMetrics(job, [metricName], ["core"]);
fetching = false;
if (response.error) {
error = response.error;
return;
}
for (let jm of response.data.jobMetrics) {
if (jm.scope != "node") {
scopes = [...scopes, jm.scope];
rawData.push(jm.metric);
statsSeries = rawData.map((data) => data?.statisticsSeries ? data.statisticsSeries : null)
selectedScope = jm.scope;
selectedScopeIndex = scopes.findIndex((s) => s == jm.scope);
dispatch("more-loaded", jm);
}
}
}
$: if (selectedScope == "load-more") loadMore();
</script> </script>
<InputGroup> <InputGroup>
<InputGroupText style="min-width: 150px;"> <InputGroupText style="min-width: 150px;">
{metricName} ({(metricConfig?.unit?.prefix {metricName} ({unit})
? metricConfig.unit.prefix
: "") + (metricConfig?.unit?.base ? metricConfig.unit.base : "")})
</InputGroupText> </InputGroupText>
<select class="form-select" bind:value={selectedScope}> <select class="form-select" bind:value={selectedScope}>
{#each availableScopes as scope, index} {#each availableScopes as scope, index}
@ -95,8 +71,8 @@
<option value={scope + '-stat'}>stats series ({scope})</option> <option value={scope + '-stat'}>stats series ({scope})</option>
{/if} {/if}
{/each} {/each}
{#if availableScopes.length == 1 && metricConfig?.scope != "node"} {#if availableScopes.length == 1 && nativeScope != "node"}
<option value={"load-more"}>Load more...</option> <option value={"load-all"}>Load all...</option>
{/if} {/if}
</select> </select>
{#if job.resources.length > 1} {#if job.resources.length > 1}
@ -118,8 +94,8 @@
bind:this={plot} bind:this={plot}
{width} {width}
height={300} height={300}
{cluster} cluster={job.cluster}
{subCluster} subCluster={job.subCluster}
timestep={data.timestep} timestep={data.timestep}
scope={selectedScope} scope={selectedScope}
metric={metricName} metric={metricName}
@ -132,8 +108,8 @@
bind:this={plot} bind:this={plot}
{width} {width}
height={300} height={300}
{cluster} cluster={job.cluster}
{subCluster} subCluster={job.subCluster}
timestep={data.timestep} timestep={data.timestep}
scope={selectedScope} scope={selectedScope}
metric={metricName} metric={metricName}

View File

@ -1,3 +1,11 @@
<!--
@component Job-View subcomponent; display table of metric data statistics with selectable scopes
Properties:
- `job Object`: The job object
- `jobMetrics [Object]`: The jobs metricdata
-->
<script> <script>
import { getContext } from "svelte"; import { getContext } from "svelte";
import { import {
@ -7,9 +15,9 @@
InputGroupText, InputGroupText,
Icon, Icon,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import MetricSelection from "./MetricSelection.svelte"; import { maxScope } from "../generic/utils.js";
import StatsTableEntry from "./StatsTableEntry.svelte"; import StatsTableEntry from "./StatsTableEntry.svelte";
import { maxScope } from "./utils.js"; import MetricSelection from "../generic/select/MetricSelection.svelte";
export let job; export let job;
export let jobMetrics; export let jobMetrics;
@ -52,7 +60,7 @@
}; };
} }
export function sortBy(metric, stat) { function sortBy(metric, stat) {
let s = sorting[metric][stat]; let s = sorting[metric][stat];
if (s.active) { if (s.active) {
s.dir = s.dir == "up" ? "down" : "up"; s.dir = s.dir == "up" ? "down" : "up";
@ -74,10 +82,6 @@
return s.dir != "up" ? s1[stat] - s2[stat] : s2[stat] - s1[stat]; return s.dir != "up" ? s1[stat] - s2[stat] : s2[stat] - s1[stat];
}); });
} }
export function moreLoaded(jobMetric) {
jobMetrics = [...jobMetrics, jobMetric];
}
</script> </script>
<Table> <Table>
@ -85,7 +89,6 @@
<tr> <tr>
<th> <th>
<Button outline on:click={() => (isMetricSelectionOpen = true)}> <Button outline on:click={() => (isMetricSelectionOpen = true)}>
<!-- log to click ', console.log(isMetricSelectionOpen)' -->
Metrics Metrics
</Button> </Button>
</th> </th>

View File

@ -1,3 +1,13 @@
<!--
@component Job-View subcomponent; Single Statistics entry component fpr statstable
Properties:
- `host String`: The hostname (== node)
- `metric String`: The metric name
- `scope String`: The selected scope
- `jobMetrics [Object]`: The jobs metricdata
-->
<script> <script>
import { Icon } from "@sveltestrap/sveltestrap"; import { Icon } from "@sveltestrap/sveltestrap";

View File

@ -1,3 +1,10 @@
<!--
@component Job View Subcomponent; allows management of job tags by deletion or new entries
Properties:
- `job Object`: The job object
- `jobTags [Number]`: The array of currently designated tags
-->
<script> <script>
import { getContext } from "svelte"; import { getContext } from "svelte";
import { gql, getContextClient, mutationStore } from "@urql/svelte"; import { gql, getContextClient, mutationStore } from "@urql/svelte";
@ -13,8 +20,8 @@
ModalFooter, ModalFooter,
Alert, Alert,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { fuzzySearchTags } from "./utils.js"; import { fuzzySearchTags } from "../generic/utils.js";
import Tag from "./Tag.svelte"; import Tag from "../generic/helper/Tag.svelte";
export let job; export let job;
export let jobTags = job.tags; export let jobTags = job.tags;
@ -107,7 +114,6 @@
addTagToJob(res.data.createTag); addTagToJob(res.data.createTag);
} else if (res.fetching === false && res.error) { } else if (res.fetching === false && res.error) {
throw res.error; throw res.error;
// console.log('Error on subscription: ' + res.error)
} }
}); });
} }
@ -120,7 +126,6 @@
pendingChange = false; pendingChange = false;
} else if (res.fetching === false && res.error) { } else if (res.fetching === false && res.error) {
throw res.error; throw res.error;
// console.log('Error on subscription: ' + res.error)
} }
}); });
} }
@ -134,7 +139,6 @@
pendingChange = false; pendingChange = false;
} else if (res.fetching === false && res.error) { } else if (res.fetching === false && res.error) {
throw res.error; throw res.error;
// console.log('Error on subscription: ' + res.error)
} }
}, },
); );

View File

@ -9,8 +9,6 @@
<script> <script>
const jobInfos = { const jobInfos = {
id: "{{ .Infos.id }}", id: "{{ .Infos.id }}",
jobId: "{{ .Infos.jobId }}",
clusterId: "{{ .Infos.clusterId }}"
}; };
const clusterCockpitConfig = {{ .Config }}; const clusterCockpitConfig = {{ .Config }};
const authlevel = {{ .User.GetAuthLevel }}; const authlevel = {{ .User.GetAuthLevel }};