mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-04-22 11:21:42 +02:00
Merge pull request #284 from ClusterCockpit/Refactor-job-footprint
Refactor job footprint
This commit is contained in:
commit
5603c41900
@ -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!]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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")
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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"`
|
||||||
|
@ -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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
@ -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}
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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}
|
||||||
|
@ -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> <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} <!-- 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>
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
@ -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";
|
||||||
|
@ -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)}/>
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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";
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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
|
||||||
});
|
});
|
||||||
|
@ -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
|
||||||
});
|
});
|
||||||
|
@ -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
|
||||||
});
|
});
|
||||||
|
@ -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>
|
|
@ -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>
|
@ -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 {
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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;
|
@ -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>
|
@ -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,
|
@ -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>
|
95
web/frontend/src/generic/filters/Stats.svelte
Normal file
95
web/frontend/src/generic/filters/Stats.svelte
Normal 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>
|
@ -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>
|
264
web/frontend/src/generic/helper/JobFootprint.svelte
Normal file
264
web/frontend/src/generic/helper/JobFootprint.svelte
Normal 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> <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} <!-- 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>
|
||||||
|
<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}
|
||||||
|
</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>
|
@ -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>
|
@ -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)
|
@ -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
|
||||||
});
|
});
|
@ -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;
|
@ -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}
|
@ -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));
|
@ -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;
|
||||||
|
|
@ -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}
|
@ -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 = [
|
@ -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.
|
@ -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!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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>
|
@ -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>
|
@ -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)
|
||||||
-->
|
-->
|
@ -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)}>
|
@ -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}
|
@ -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
|
@ -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}
|
@ -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);
|
@ -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 = []
|
@ -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}
|
@ -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>
|
@ -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}
|
@ -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>
|
@ -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";
|
||||||
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
@ -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 }};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user