Merge branch 'feat/565-add-metric-tooltip' into metric-store-tickets

Resolve conflicts in the generated GraphQL code by regenerating it against
the merged schema. cc-lib v2.13.0 adds Tooltip to schema.MetricConfig and
schema.GlobalMetricListItem, so gqlgen now binds the tooltip field directly
and the hand-written globalMetricListItem/metricConfig resolvers introduced
on the tooltip branch are no longer needed.

Also migrate to the cc-lib v2.13.0 metric container types, which changed
from bare maps to structs carrying array-valued metric groups:

  schema.JobData          map -> {Metrics, Groups}
  schema.ScopedJobStats   map -> {Metrics, Groups}
  job.Statistics          map -> schema.JobStatisticsSet{Metrics, Groups}

Callers index .Metrics, return the zero struct instead of nil, and
deepCopy/DecodeJobStats now also carry the Groups payload through.
archive.GetStatistics returns the full JobStatisticsSet so group
statistics survive the round trip.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-27 10:22:22 +02:00
co-authored by Claude Opus 5
35 changed files with 411 additions and 212 deletions
+2
View File
@@ -143,6 +143,7 @@ type MetricConfig {
alert: Float! alert: Float!
lowerIsBetter: Boolean lowerIsBetter: Boolean
subClusters: [SubClusterConfig!]! subClusters: [SubClusterConfig!]!
tooltip: String
} }
type Tag { type Tag {
@@ -306,6 +307,7 @@ type GlobalMetricListItem {
unit: Unit! unit: Unit!
scope: MetricScope! scope: MetricScope!
footprint: String footprint: String
tooltip: String
availability: [ClusterSupport!]! availability: [ClusterSupport!]!
} }
+6 -6
View File
@@ -196,8 +196,8 @@ func cleanup() {
func TestRestApi(t *testing.T) { func TestRestApi(t *testing.T) {
restapi := setup(t) restapi := setup(t)
t.Cleanup(cleanup) t.Cleanup(cleanup)
testData := schema.JobData{ testData := schema.JobData{Metrics: map[string]schema.ScopedMetrics{
"load_one": map[schema.MetricScope]*schema.JobMetric{ "load_one": {
schema.MetricScopeNode: { schema.MetricScopeNode: {
Unit: schema.Unit{Base: "load"}, Unit: schema.Unit{Base: "load"},
Timestep: 60, Timestep: 60,
@@ -210,7 +210,7 @@ func TestRestApi(t *testing.T) {
}, },
}, },
}, },
} }}
metricstore.TestLoadDataCallback = func(job *schema.Job, metrics []string, scopes []schema.MetricScope, ctx context.Context, resolution int) (schema.JobData, error) { metricstore.TestLoadDataCallback = func(job *schema.Job, metrics []string, scopes []schema.MetricScope, ctx context.Context, resolution int) (schema.JobData, error) {
return testData, nil return testData, nil
@@ -497,8 +497,8 @@ func TestStopJobWithReusedJobId(t *testing.T) {
restapi := setup(t) restapi := setup(t)
t.Cleanup(cleanup) t.Cleanup(cleanup)
testData := schema.JobData{ testData := schema.JobData{Metrics: map[string]schema.ScopedMetrics{
"load_one": map[schema.MetricScope]*schema.JobMetric{ "load_one": {
schema.MetricScopeNode: { schema.MetricScopeNode: {
Unit: schema.Unit{Base: "load"}, Unit: schema.Unit{Base: "load"},
Timestep: 60, Timestep: 60,
@@ -511,7 +511,7 @@ func TestStopJobWithReusedJobId(t *testing.T) {
}, },
}, },
}, },
} }}
metricstore.TestLoadDataCallback = func(job *schema.Job, metrics []string, scopes []schema.MetricScope, ctx context.Context, resolution int) (schema.JobData, error) { metricstore.TestLoadDataCallback = func(job *schema.Job, metrics []string, scopes []schema.MetricScope, ctx context.Context, resolution int) (schema.JobData, error) {
return testData, nil return testData, nil
+1 -1
View File
@@ -412,7 +412,7 @@ func (api *RestAPI) getJobByID(rw http.ResponseWriter, r *http.Request) {
} }
res := []*JobMetricWithName{} res := []*JobMetricWithName{}
for name, md := range data { for name, md := range data.Metrics {
for scope, metric := range md { for scope, metric := range md {
res = append(res, &JobMetricWithName{ res = append(res, &JobMetricWithName{
Name: name, Name: name,
+3 -3
View File
@@ -531,8 +531,8 @@ func TestNatsHandleStopJob(t *testing.T) {
}, },
} }
testData := schema.JobData{ testData := schema.JobData{Metrics: map[string]schema.ScopedMetrics{
"load_one": map[schema.MetricScope]*schema.JobMetric{ "load_one": {
schema.MetricScopeNode: { schema.MetricScopeNode: {
Unit: schema.Unit{Base: "load"}, Unit: schema.Unit{Base: "load"},
Timestep: 60, Timestep: 60,
@@ -545,7 +545,7 @@ func TestNatsHandleStopJob(t *testing.T) {
}, },
}, },
}, },
} }}
metricstore.TestLoadDataCallback = func(job *schema.Job, metrics []string, scopes []schema.MetricScope, ctx context.Context, resolution int) (schema.JobData, error) { metricstore.TestLoadDataCallback = func(job *schema.Job, metrics []string, scopes []schema.MetricScope, ctx context.Context, resolution int) (schema.JobData, error) {
return testData, nil return testData, nil
+3 -3
View File
@@ -65,9 +65,9 @@ func ArchiveJob(job *schema.Job, ctx context.Context) (*schema.Job, error) {
return nil, err return nil, err
} }
job.Statistics = make(map[string]schema.JobStatistics) job.Statistics = schema.JobStatisticsSet{Metrics: make(map[string]schema.JobStatistics, len(jobData.Metrics))}
for metric, data := range jobData { for metric, data := range jobData.Metrics {
avg, min, max := 0.0, math.MaxFloat32, -math.MaxFloat32 avg, min, max := 0.0, math.MaxFloat32, -math.MaxFloat32
nodeData, ok := data["node"] nodeData, ok := data["node"]
if !ok { if !ok {
@@ -82,7 +82,7 @@ func ArchiveJob(job *schema.Job, ctx context.Context) (*schema.Job, error) {
} }
// Round AVG Result to 2 Digits // Round AVG Result to 2 Digits
job.Statistics[metric] = schema.JobStatistics{ job.Statistics.Metrics[metric] = schema.JobStatistics{
Unit: schema.Unit{ Unit: schema.Unit{
Prefix: archive.GetMetricConfig(job.Cluster, metric).Unit.Prefix, Prefix: archive.GetMetricConfig(job.Cluster, metric).Unit.Prefix,
Base: archive.GetMetricConfig(job.Cluster, metric).Unit.Base, Base: archive.GetMetricConfig(job.Cluster, metric).Unit.Base,
+76
View File
@@ -100,6 +100,7 @@ type ComplexityRoot struct {
Footprint func(childComplexity int) int Footprint func(childComplexity int) int
Name func(childComplexity int) int Name func(childComplexity int) int
Scope func(childComplexity int) int Scope func(childComplexity int) int
Tooltip func(childComplexity int) int
Unit func(childComplexity int) int Unit func(childComplexity int) int
} }
@@ -219,6 +220,7 @@ type ComplexityRoot struct {
Scope func(childComplexity int) int Scope func(childComplexity int) int
SubClusters func(childComplexity int) int SubClusters func(childComplexity int) int
Timestep func(childComplexity int) int Timestep func(childComplexity int) int
Tooltip func(childComplexity int) int
Unit func(childComplexity int) int Unit func(childComplexity int) int
} }
@@ -698,6 +700,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
} }
return e.ComplexityRoot.GlobalMetricListItem.Scope(childComplexity), true return e.ComplexityRoot.GlobalMetricListItem.Scope(childComplexity), true
case "GlobalMetricListItem.tooltip":
if e.ComplexityRoot.GlobalMetricListItem.Tooltip == nil {
break
}
return e.ComplexityRoot.GlobalMetricListItem.Tooltip(childComplexity), true
case "GlobalMetricListItem.unit": case "GlobalMetricListItem.unit":
if e.ComplexityRoot.GlobalMetricListItem.Unit == nil { if e.ComplexityRoot.GlobalMetricListItem.Unit == nil {
break break
@@ -1225,6 +1233,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
} }
return e.ComplexityRoot.MetricConfig.Timestep(childComplexity), true return e.ComplexityRoot.MetricConfig.Timestep(childComplexity), true
case "MetricConfig.tooltip":
if e.ComplexityRoot.MetricConfig.Tooltip == nil {
break
}
return e.ComplexityRoot.MetricConfig.Tooltip(childComplexity), true
case "MetricConfig.unit": case "MetricConfig.unit":
if e.ComplexityRoot.MetricConfig.Unit == nil { if e.ComplexityRoot.MetricConfig.Unit == nil {
break break
@@ -2426,6 +2440,7 @@ type MetricConfig {
alert: Float! alert: Float!
lowerIsBetter: Boolean lowerIsBetter: Boolean
subClusters: [SubClusterConfig!]! subClusters: [SubClusterConfig!]!
tooltip: String
} }
type Tag { type Tag {
@@ -2589,6 +2604,7 @@ type GlobalMetricListItem {
unit: Unit! unit: Unit!
scope: MetricScope! scope: MetricScope!
footprint: String footprint: String
tooltip: String
availability: [ClusterSupport!]! availability: [ClusterSupport!]!
} }
@@ -2973,6 +2989,8 @@ func (ec *executionContext) childFields_GlobalMetricListItem(ctx context.Context
return ec.fieldContext_GlobalMetricListItem_scope(ctx, field) return ec.fieldContext_GlobalMetricListItem_scope(ctx, field)
case "footprint": case "footprint":
return ec.fieldContext_GlobalMetricListItem_footprint(ctx, field) return ec.fieldContext_GlobalMetricListItem_footprint(ctx, field)
case "tooltip":
return ec.fieldContext_GlobalMetricListItem_tooltip(ctx, field)
case "availability": case "availability":
return ec.fieldContext_GlobalMetricListItem_availability(ctx, field) return ec.fieldContext_GlobalMetricListItem_availability(ctx, field)
} }
@@ -3203,6 +3221,8 @@ func (ec *executionContext) childFields_MetricConfig(ctx context.Context, field
return ec.fieldContext_MetricConfig_lowerIsBetter(ctx, field) return ec.fieldContext_MetricConfig_lowerIsBetter(ctx, field)
case "subClusters": case "subClusters":
return ec.fieldContext_MetricConfig_subClusters(ctx, field) return ec.fieldContext_MetricConfig_subClusters(ctx, field)
case "tooltip":
return ec.fieldContext_MetricConfig_tooltip(ctx, field)
} }
return nil, fmt.Errorf("no field named %q was found under type MetricConfig", field.Name) return nil, fmt.Errorf("no field named %q was found under type MetricConfig", field.Name)
} }
@@ -5187,6 +5207,29 @@ func (ec *executionContext) fieldContext_GlobalMetricListItem_footprint(_ contex
return graphql.NewScalarFieldContext("GlobalMetricListItem", field, false, false, errors.New("field of type String does not have child fields")) return graphql.NewScalarFieldContext("GlobalMetricListItem", field, false, false, errors.New("field of type String does not have child fields"))
} }
func (ec *executionContext) _GlobalMetricListItem_tooltip(ctx context.Context, field graphql.CollectedField, obj *schema.GlobalMetricListItem) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return ec.fieldContext_GlobalMetricListItem_tooltip(ctx, field)
},
func(ctx context.Context) (any, error) {
return obj.Tooltip, nil
},
nil,
func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler {
return ec.marshalOString2string(ctx, selections, v)
},
true,
false,
)
}
func (ec *executionContext) fieldContext_GlobalMetricListItem_tooltip(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
return graphql.NewScalarFieldContext("GlobalMetricListItem", field, false, false, errors.New("field of type String does not have child fields"))
}
func (ec *executionContext) _GlobalMetricListItem_availability(ctx context.Context, field graphql.CollectedField, obj *schema.GlobalMetricListItem) (ret graphql.Marshaler) { func (ec *executionContext) _GlobalMetricListItem_availability(ctx context.Context, field graphql.CollectedField, obj *schema.GlobalMetricListItem) (ret graphql.Marshaler) {
return graphql.ResolveField( return graphql.ResolveField(
ctx, ctx,
@@ -7377,6 +7420,29 @@ func (ec *executionContext) fieldContext_MetricConfig_subClusters(_ context.Cont
return fc, nil return fc, nil
} }
func (ec *executionContext) _MetricConfig_tooltip(ctx context.Context, field graphql.CollectedField, obj *schema.MetricConfig) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return ec.fieldContext_MetricConfig_tooltip(ctx, field)
},
func(ctx context.Context) (any, error) {
return obj.Tooltip, nil
},
nil,
func(ctx context.Context, selections ast.SelectionSet, v string) graphql.Marshaler {
return ec.marshalOString2string(ctx, selections, v)
},
true,
false,
)
}
func (ec *executionContext) fieldContext_MetricConfig_tooltip(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
return graphql.NewScalarFieldContext("MetricConfig", field, false, false, errors.New("field of type String does not have child fields"))
}
func (ec *executionContext) _MetricFootprints_metric(ctx context.Context, field graphql.CollectedField, obj *model.MetricFootprints) (ret graphql.Marshaler) { func (ec *executionContext) _MetricFootprints_metric(ctx context.Context, field graphql.CollectedField, obj *model.MetricFootprints) (ret graphql.Marshaler) {
return graphql.ResolveField( return graphql.ResolveField(
ctx, ctx,
@@ -13311,6 +13377,11 @@ func (ec *executionContext) _GlobalMetricListItem(ctx context.Context, sel ast.S
if out.Values[i] == graphql.RequiredNull { if out.Values[i] == graphql.RequiredNull {
out.Invalids++ out.Invalids++
} }
case "tooltip":
out.Values[i] = ec._GlobalMetricListItem_tooltip(ctx, field, obj)
if out.Values[i] == graphql.RequiredNull {
out.Invalids++
}
case "availability": case "availability":
out.Values[i] = ec._GlobalMetricListItem_availability(ctx, field, obj) out.Values[i] = ec._GlobalMetricListItem_availability(ctx, field, obj)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
@@ -14340,6 +14411,11 @@ func (ec *executionContext) _MetricConfig(ctx context.Context, sel ast.Selection
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
out.Invalids++ out.Invalids++
} }
case "tooltip":
out.Values[i] = ec._MetricConfig_tooltip(ctx, field, obj)
if out.Values[i] == graphql.RequiredNull {
out.Invalids++
}
default: default:
panic("unknown field " + strconv.Quote(field.Name)) panic("unknown field " + strconv.Quote(field.Name))
} }
+3 -3
View File
@@ -528,7 +528,7 @@ func (r *queryResolver) JobMetrics(ctx context.Context, id string, metrics []str
} }
res := []*model.JobMetricWithName{} res := []*model.JobMetricWithName{}
for name, md := range data { for name, md := range data.Metrics {
for scope, metric := range md { for scope, metric := range md {
res = append(res, &model.JobMetricWithName{ res = append(res, &model.JobMetricWithName{
Name: name, Name: name,
@@ -581,7 +581,7 @@ func (r *queryResolver) ScopedJobStats(ctx context.Context, id string, metrics [
} }
res := make([]*model.NamedStatsWithScope, 0) res := make([]*model.NamedStatsWithScope, 0)
for name, scoped := range data { for name, scoped := range data.Metrics {
for scope, stats := range scoped { for scope, stats := range scoped {
mdlStats := make([]*model.ScopedStats, 0) mdlStats := make([]*model.ScopedStats, 0)
@@ -939,7 +939,7 @@ func (r *queryResolver) NodeMetricsList(ctx context.Context, cluster string, sub
cclog.Warnf("error in nodeMetrics resolver: %s", err) cclog.Warnf("error in nodeMetrics resolver: %s", err)
} }
for metric, scopedMetrics := range data[hostname] { for metric, scopedMetrics := range data[hostname].Metrics {
for scope, scopedMetric := range scopedMetrics { for scope, scopedMetric := range scopedMetrics {
host.Metrics = append(host.Metrics, &model.JobMetricWithName{ host.Metrics = append(host.Metrics, &model.JobMetricWithName{
Name: metric, Name: metric,
+1 -1
View File
@@ -61,7 +61,7 @@ func (r *queryResolver) rooflineHeatmap(
return nil, err return nil, err
} }
flops_, membw_ := jobdata["flops_any"], jobdata["mem_bw"] flops_, membw_ := jobdata.Metrics["flops_any"], jobdata.Metrics["mem_bw"]
if flops_ == nil && membw_ == nil { if flops_ == nil && membw_ == nil {
cclog.Warnf("rooflineHeatmap(): 'flops_any' or 'mem_bw' missing for job %d", *job.ID) cclog.Warnf("rooflineHeatmap(): 'flops_any' or 'mem_bw' missing for job %d", *job.ID)
continue continue
+1 -1
View File
@@ -290,7 +290,7 @@ func SanityChecks(job *schema.Job) error {
// //
// TODO: Either implement the metric normalization or remove this dead code. // TODO: Either implement the metric normalization or remove this dead code.
func checkJobData(d *schema.JobData) error { func checkJobData(d *schema.JobData) error {
for _, scopes := range *d { for _, scopes := range d.Metrics {
// var newUnit schema.Unit // var newUnit schema.Unit
// TODO Add node scope if missing // TODO Add node scope if missing
for _, metric := range scopes { for _, metric := range scopes {
+38 -17
View File
@@ -117,7 +117,7 @@ func LoadData(job *schema.Job,
jd, err = ms.LoadData(job, metrics, scopes, ctx, resolution) jd, err = ms.LoadData(job, metrics, scopes, ctx, resolution)
if err != nil { if err != nil {
if len(jd) != 0 { if len(jd.Metrics) != 0 {
cclog.Warnf("partial error loading metrics from store for job %d (user: %s, project: %s, cluster: %s-%s): %s", cclog.Warnf("partial error loading metrics from store for job %d (user: %s, project: %s, cluster: %s-%s): %s",
job.JobID, job.User, job.Project, job.Cluster, job.SubCluster, err.Error()) job.JobID, job.User, job.Project, job.Cluster, job.SubCluster, err.Error())
} else { } else {
@@ -144,7 +144,7 @@ func LoadData(job *schema.Job,
if rfErr != nil { if rfErr != nil {
return rfErr, 0, 0 return rfErr, 0, 0
} }
for _, v := range jd { for _, v := range jd.Metrics {
for _, v_ := range v { for _, v_ := range v {
timestep := int64(0) timestep := int64(0)
for i := 0; i < len(v_.Series); i += 1 { for i := 0; i < len(v_.Series); i += 1 {
@@ -160,17 +160,20 @@ func LoadData(job *schema.Job,
// Filter job data to only include requested metrics and scopes, avoiding unnecessary data transfer. // Filter job data to only include requested metrics and scopes, avoiding unnecessary data transfer.
if metrics != nil || scopes != nil { if metrics != nil || scopes != nil {
if metrics == nil { if metrics == nil {
metrics = make([]string, 0, len(jd)) metrics = make([]string, 0, len(jd.Metrics))
for k := range jd { for k := range jd.Metrics {
metrics = append(metrics, k) metrics = append(metrics, k)
} }
} }
res := schema.JobData{} res := schema.JobData{
Metrics: make(map[string]schema.ScopedMetrics, len(metrics)),
Groups: jd.Groups,
}
for _, metric := range metrics { for _, metric := range metrics {
if perscope, ok := jd[metric]; ok { if perscope, ok := jd.Metrics[metric]; ok {
if len(perscope) > 1 { if len(perscope) > 1 {
subset := make(map[schema.MetricScope]*schema.JobMetric) subset := make(schema.ScopedMetrics)
for _, scope := range scopes { for _, scope := range scopes {
if jm, ok := perscope[scope]; ok { if jm, ok := perscope[scope]; ok {
subset[scope] = jm subset[scope] = jm
@@ -182,7 +185,7 @@ func LoadData(job *schema.Job,
} }
} }
res[metric] = perscope res.Metrics[metric] = perscope
} }
} }
jd = res jd = res
@@ -199,7 +202,7 @@ func LoadData(job *schema.Job,
// instead of overwhelming the UI with individual node lines. Note that newly calculated // instead of overwhelming the UI with individual node lines. Note that newly calculated
// statistics use min/median/max, while archived statistics may use min/mean/max. // statistics use min/median/max, while archived statistics may use min/mean/max.
const maxSeriesSize int = 8 const maxSeriesSize int = 8
for _, scopes := range jd { for _, scopes := range jd.Metrics {
for _, jm := range scopes { for _, jm := range scopes {
if jm.StatisticsSeries != nil || len(jm.Series) < maxSeriesSize { if jm.StatisticsSeries != nil || len(jm.Series) < maxSeriesSize {
continue continue
@@ -229,7 +232,7 @@ func LoadData(job *schema.Job,
if err, ok := data.(error); ok { if err, ok := data.(error); ok {
cclog.Errorf("error in cached dataset for job %d: %s", job.JobID, err.Error()) cclog.Errorf("error in cached dataset for job %d: %s", job.JobID, err.Error())
return nil, err return schema.JobData{}, err
} }
return data.(schema.JobData), nil return data.(schema.JobData), nil
@@ -296,14 +299,14 @@ func LoadScopedJobStats(
if err != nil { if err != nil {
cclog.Errorf("failed to access metricDataRepo for cluster %s-%s: %s", cclog.Errorf("failed to access metricDataRepo for cluster %s-%s: %s",
job.Cluster, job.SubCluster, err.Error()) job.Cluster, job.SubCluster, err.Error())
return nil, err return schema.ScopedJobStats{}, err
} }
scopedStats, err := ms.LoadScopedStats(job, metrics, scopes, ctx) scopedStats, err := ms.LoadScopedStats(job, metrics, scopes, ctx)
if err != nil { if err != nil {
cclog.Warnf("failed to load scoped statistics from metric store for job %d (user: %s, project: %s, cluster: %s-%s): %s", cclog.Warnf("failed to load scoped statistics from metric store for job %d (user: %s, project: %s, cluster: %s-%s): %s",
job.JobID, job.User, job.Project, job.Cluster, job.SubCluster, err.Error()) job.JobID, job.User, job.Project, job.Cluster, job.SubCluster, err.Error())
return nil, err return schema.ScopedJobStats{}, err
} }
// Round Resulting Stat Values // Round Resulting Stat Values
@@ -451,7 +454,7 @@ func LoadNodeListData(
// Statistics are calculated as min/median/max. // Statistics are calculated as min/median/max.
const maxSeriesSize int = 8 const maxSeriesSize int = 8
for _, jd := range data { for _, jd := range data {
for _, scopes := range jd { for _, scopes := range jd.Metrics {
for _, jm := range scopes { for _, jm := range scopes {
if jm.StatisticsSeries != nil || len(jm.Series) < maxSeriesSize { if jm.StatisticsSeries != nil || len(jm.Series) < maxSeriesSize {
continue continue
@@ -472,14 +475,32 @@ func LoadNodeListData(
// archived data (e.g., during resampling). This ensures the cached archive data remains // archived data (e.g., during resampling). This ensures the cached archive data remains
// immutable while allowing per-request transformations. // immutable while allowing per-request transformations.
func deepCopy(source schema.JobData) schema.JobData { func deepCopy(source schema.JobData) schema.JobData {
result := make(schema.JobData, len(source)) result := schema.JobData{Metrics: copyScopedMetrics(source.Metrics)}
for _, group := range source.Groups {
copied := schema.MetricGroup{Key: group.Key}
for _, inst := range group.Instances {
copied.Instances = append(copied.Instances, schema.MetricGroupInstance{
Name: inst.Name,
Type: inst.Type,
Metrics: copyScopedMetrics(inst.Metrics),
})
}
result.Groups = append(result.Groups, copied)
}
return result
}
func copyScopedMetrics(source map[string]schema.ScopedMetrics) map[string]schema.ScopedMetrics {
result := make(map[string]schema.ScopedMetrics, len(source))
for metricName, scopeMap := range source { for metricName, scopeMap := range source {
result[metricName] = make(map[schema.MetricScope]*schema.JobMetric, len(scopeMap)) scopes := make(schema.ScopedMetrics, len(scopeMap))
for scope, jobMetric := range scopeMap { for scope, jobMetric := range scopeMap {
result[metricName][scope] = copyJobMetric(jobMetric) scopes[scope] = copyJobMetric(jobMetric)
} }
result[metricName] = scopes
} }
return result return result
+22 -22
View File
@@ -13,7 +13,7 @@ import (
func TestDeepCopy(t *testing.T) { func TestDeepCopy(t *testing.T) {
nodeId := "0" nodeId := "0"
original := schema.JobData{ original := schema.JobData{Metrics: map[string]schema.ScopedMetrics{
"cpu_load": { "cpu_load": {
schema.MetricScopeNode: &schema.JobMetric{ schema.MetricScopeNode: &schema.JobMetric{
Timestep: 60, Timestep: 60,
@@ -42,42 +42,42 @@ func TestDeepCopy(t *testing.T) {
}, },
}, },
}, },
} }}
copied := deepCopy(original) copied := deepCopy(original)
original["cpu_load"][schema.MetricScopeNode].Series[0].Data[0] = 999.0 original.Metrics["cpu_load"][schema.MetricScopeNode].Series[0].Data[0] = 999.0
original["cpu_load"][schema.MetricScopeNode].StatisticsSeries.Min[0] = 888.0 original.Metrics["cpu_load"][schema.MetricScopeNode].StatisticsSeries.Min[0] = 888.0
original["cpu_load"][schema.MetricScopeNode].StatisticsSeries.Percentiles[25][0] = 777.0 original.Metrics["cpu_load"][schema.MetricScopeNode].StatisticsSeries.Percentiles[25][0] = 777.0
if copied["cpu_load"][schema.MetricScopeNode].Series[0].Data[0] != 1.0 { if copied.Metrics["cpu_load"][schema.MetricScopeNode].Series[0].Data[0] != 1.0 {
t.Errorf("Series data was not deeply copied: got %v, want 1.0", t.Errorf("Series data was not deeply copied: got %v, want 1.0",
copied["cpu_load"][schema.MetricScopeNode].Series[0].Data[0]) copied.Metrics["cpu_load"][schema.MetricScopeNode].Series[0].Data[0])
} }
if copied["cpu_load"][schema.MetricScopeNode].StatisticsSeries.Min[0] != 1.0 { if copied.Metrics["cpu_load"][schema.MetricScopeNode].StatisticsSeries.Min[0] != 1.0 {
t.Errorf("StatisticsSeries was not deeply copied: got %v, want 1.0", t.Errorf("StatisticsSeries was not deeply copied: got %v, want 1.0",
copied["cpu_load"][schema.MetricScopeNode].StatisticsSeries.Min[0]) copied.Metrics["cpu_load"][schema.MetricScopeNode].StatisticsSeries.Min[0])
} }
if copied["cpu_load"][schema.MetricScopeNode].StatisticsSeries.Percentiles[25][0] != 1.5 { if copied.Metrics["cpu_load"][schema.MetricScopeNode].StatisticsSeries.Percentiles[25][0] != 1.5 {
t.Errorf("Percentiles was not deeply copied: got %v, want 1.5", t.Errorf("Percentiles was not deeply copied: got %v, want 1.5",
copied["cpu_load"][schema.MetricScopeNode].StatisticsSeries.Percentiles[25][0]) copied.Metrics["cpu_load"][schema.MetricScopeNode].StatisticsSeries.Percentiles[25][0])
} }
if copied["cpu_load"][schema.MetricScopeNode].Timestep != 60 { if copied.Metrics["cpu_load"][schema.MetricScopeNode].Timestep != 60 {
t.Errorf("Timestep not copied correctly: got %v, want 60", t.Errorf("Timestep not copied correctly: got %v, want 60",
copied["cpu_load"][schema.MetricScopeNode].Timestep) copied.Metrics["cpu_load"][schema.MetricScopeNode].Timestep)
} }
if copied["cpu_load"][schema.MetricScopeNode].Series[0].Hostname != "node001" { if copied.Metrics["cpu_load"][schema.MetricScopeNode].Series[0].Hostname != "node001" {
t.Errorf("Hostname not copied correctly: got %v, want node001", t.Errorf("Hostname not copied correctly: got %v, want node001",
copied["cpu_load"][schema.MetricScopeNode].Series[0].Hostname) copied.Metrics["cpu_load"][schema.MetricScopeNode].Series[0].Hostname)
} }
} }
func TestDeepCopyNilStatisticsSeries(t *testing.T) { func TestDeepCopyNilStatisticsSeries(t *testing.T) {
original := schema.JobData{ original := schema.JobData{Metrics: map[string]schema.ScopedMetrics{
"mem_used": { "mem_used": {
schema.MetricScopeNode: &schema.JobMetric{ schema.MetricScopeNode: &schema.JobMetric{
Timestep: 60, Timestep: 60,
@@ -90,18 +90,18 @@ func TestDeepCopyNilStatisticsSeries(t *testing.T) {
StatisticsSeries: nil, StatisticsSeries: nil,
}, },
}, },
} }}
copied := deepCopy(original) copied := deepCopy(original)
if copied["mem_used"][schema.MetricScopeNode].StatisticsSeries != nil { if copied.Metrics["mem_used"][schema.MetricScopeNode].StatisticsSeries != nil {
t.Errorf("StatisticsSeries should be nil, got %v", t.Errorf("StatisticsSeries should be nil, got %v",
copied["mem_used"][schema.MetricScopeNode].StatisticsSeries) copied.Metrics["mem_used"][schema.MetricScopeNode].StatisticsSeries)
} }
} }
func TestDeepCopyEmptyPercentiles(t *testing.T) { func TestDeepCopyEmptyPercentiles(t *testing.T) {
original := schema.JobData{ original := schema.JobData{Metrics: map[string]schema.ScopedMetrics{
"cpu_load": { "cpu_load": {
schema.MetricScopeNode: &schema.JobMetric{ schema.MetricScopeNode: &schema.JobMetric{
Timestep: 60, Timestep: 60,
@@ -115,11 +115,11 @@ func TestDeepCopyEmptyPercentiles(t *testing.T) {
}, },
}, },
}, },
} }}
copied := deepCopy(original) copied := deepCopy(original)
if copied["cpu_load"][schema.MetricScopeNode].StatisticsSeries.Percentiles != nil { if copied.Metrics["cpu_load"][schema.MetricScopeNode].StatisticsSeries.Percentiles != nil {
t.Errorf("Percentiles should be nil when source is nil/empty") t.Errorf("Percentiles should be nil when source is nil/empty")
} }
} }
+27 -27
View File
@@ -235,7 +235,7 @@ func (ccms *CCMetricStore) LoadData(
queries, assignedScope, err := ccms.buildQueries(job, metrics, scopes, resolution) queries, assignedScope, err := ccms.buildQueries(job, metrics, scopes, resolution)
if err != nil { if err != nil {
cclog.Errorf("Error while building queries for jobId %d, Metrics %v, Scopes %v: %s", job.JobID, metrics, scopes, err.Error()) cclog.Errorf("Error while building queries for jobId %d, Metrics %v, Scopes %v: %s", job.JobID, metrics, scopes, err.Error())
return nil, err return schema.JobData{}, err
} }
// Verify assignment is correct - log any inconsistencies for debugging // Verify assignment is correct - log any inconsistencies for debugging
@@ -256,11 +256,11 @@ func (ccms *CCMetricStore) LoadData(
resBody, err := ccms.doRequest(ctx, &req) resBody, err := ccms.doRequest(ctx, &req)
if err != nil { if err != nil {
cclog.Errorf("Error while performing request for job %d: %s", job.JobID, err.Error()) cclog.Errorf("Error while performing request for job %d: %s", job.JobID, err.Error())
return nil, err return schema.JobData{}, err
} }
var errors []string var errors []string
jobData := make(schema.JobData) jobData := schema.JobData{Metrics: make(map[string]schema.ScopedMetrics)}
// Add safety check for potential index out of range errors // Add safety check for potential index out of range errors
if len(resBody.Results) != len(req.Queries) || len(assignedScope) != len(req.Queries) { if len(resBody.Results) != len(req.Queries) || len(assignedScope) != len(req.Queries) {
@@ -285,8 +285,8 @@ func (ccms *CCMetricStore) LoadData(
continue continue
} }
if _, ok := jobData[metric]; !ok { if _, ok := jobData.Metrics[metric]; !ok {
jobData[metric] = make(map[schema.MetricScope]*schema.JobMetric) jobData.Metrics[metric] = make(schema.ScopedMetrics)
} }
res := mc.Timestep res := mc.Timestep
@@ -294,14 +294,14 @@ func (ccms *CCMetricStore) LoadData(
res = row[0].Resolution res = row[0].Resolution
} }
jobMetric, ok := jobData[metric][scope] jobMetric, ok := jobData.Metrics[metric][scope]
if !ok { if !ok {
jobMetric = &schema.JobMetric{ jobMetric = &schema.JobMetric{
Unit: mc.Unit, Unit: mc.Unit,
Timestep: res, Timestep: res,
Series: make([]schema.Series, 0), Series: make([]schema.Series, 0),
} }
jobData[metric][scope] = jobMetric jobData.Metrics[metric][scope] = jobMetric
} }
for ndx, res := range row { for ndx, res := range row {
@@ -329,9 +329,9 @@ func (ccms *CCMetricStore) LoadData(
// So that one can later check len(jobData): // So that one can later check len(jobData):
if len(jobMetric.Series) == 0 { if len(jobMetric.Series) == 0 {
delete(jobData[metric], scope) delete(jobData.Metrics[metric], scope)
if len(jobData[metric]) == 0 { if len(jobData.Metrics[metric]) == 0 {
delete(jobData, metric) delete(jobData.Metrics, metric)
} }
} }
} }
@@ -426,7 +426,7 @@ func (ccms *CCMetricStore) LoadScopedStats(
queries, assignedScope, err := ccms.buildQueries(job, metrics, scopes, 0) queries, assignedScope, err := ccms.buildQueries(job, metrics, scopes, 0)
if err != nil { if err != nil {
cclog.Errorf("Error while building queries for jobId %d, Metrics %v, Scopes %v: %s", job.JobID, metrics, scopes, err.Error()) cclog.Errorf("Error while building queries for jobId %d, Metrics %v, Scopes %v: %s", job.JobID, metrics, scopes, err.Error())
return nil, err return schema.ScopedJobStats{}, err
} }
req := APIQueryRequest{ req := APIQueryRequest{
@@ -441,23 +441,23 @@ func (ccms *CCMetricStore) LoadScopedStats(
resBody, err := ccms.doRequest(ctx, &req) resBody, err := ccms.doRequest(ctx, &req)
if err != nil { if err != nil {
cclog.Errorf("Error while performing request for job %d: %s", job.JobID, err.Error()) cclog.Errorf("Error while performing request for job %d: %s", job.JobID, err.Error())
return nil, err return schema.ScopedJobStats{}, err
} }
var errors []string var errors []string
scopedJobStats := make(schema.ScopedJobStats) scopedJobStats := schema.ScopedJobStats{Metrics: make(map[string]schema.ScopedMetricStats)}
for i, row := range resBody.Results { for i, row := range resBody.Results {
query := req.Queries[i] query := req.Queries[i]
metric := query.Metric metric := query.Metric
scope := assignedScope[i] scope := assignedScope[i]
if _, ok := scopedJobStats[metric]; !ok { if _, ok := scopedJobStats.Metrics[metric]; !ok {
scopedJobStats[metric] = make(map[schema.MetricScope][]*schema.ScopedStats) scopedJobStats.Metrics[metric] = make(schema.ScopedMetricStats)
} }
if _, ok := scopedJobStats[metric][scope]; !ok { if _, ok := scopedJobStats.Metrics[metric][scope]; !ok {
scopedJobStats[metric][scope] = make([]*schema.ScopedStats, 0) scopedJobStats.Metrics[metric][scope] = make([]*schema.ScopedStats, 0)
} }
for ndx, res := range row { for ndx, res := range row {
@@ -471,7 +471,7 @@ func (ccms *CCMetricStore) LoadScopedStats(
ms.SanitizeStats(&res.Avg, &res.Min, &res.Max) ms.SanitizeStats(&res.Avg, &res.Min, &res.Max)
scopedJobStats[metric][scope] = append(scopedJobStats[metric][scope], &schema.ScopedStats{ scopedJobStats.Metrics[metric][scope] = append(scopedJobStats.Metrics[metric][scope], &schema.ScopedStats{
Hostname: query.Hostname, Hostname: query.Hostname,
ID: id, ID: id,
Data: &schema.MetricStatistics{ Data: &schema.MetricStatistics{
@@ -483,10 +483,10 @@ func (ccms *CCMetricStore) LoadScopedStats(
} }
// So that one can later check len(scopedJobStats[metric][scope]): Remove from map if empty // So that one can later check len(scopedJobStats[metric][scope]): Remove from map if empty
if len(scopedJobStats[metric][scope]) == 0 { if len(scopedJobStats.Metrics[metric][scope]) == 0 {
delete(scopedJobStats[metric], scope) delete(scopedJobStats.Metrics[metric], scope)
if len(scopedJobStats[metric]) == 0 { if len(scopedJobStats.Metrics[metric]) == 0 {
delete(scopedJobStats, metric) delete(scopedJobStats.Metrics, metric)
} }
} }
} }
@@ -691,14 +691,14 @@ func (ccms *CCMetricStore) LoadNodeListData(
// Init Nested Map Data Structures If Not Found // Init Nested Map Data Structures If Not Found
hostData, ok := data[query.Hostname] hostData, ok := data[query.Hostname]
if !ok { if !ok {
hostData = make(schema.JobData) hostData = schema.JobData{Metrics: make(map[string]schema.ScopedMetrics)}
data[query.Hostname] = hostData data[query.Hostname] = hostData
} }
metricData, ok := hostData[metric] metricData, ok := hostData.Metrics[metric]
if !ok { if !ok {
metricData = make(map[schema.MetricScope]*schema.JobMetric) metricData = make(schema.ScopedMetrics)
data[query.Hostname][metric] = metricData hostData.Metrics[metric] = metricData
} }
scopeData, ok := metricData[scope] scopeData, ok := metricData[scope]
@@ -708,7 +708,7 @@ func (ccms *CCMetricStore) LoadNodeListData(
Timestep: res, Timestep: res,
Series: make([]schema.Series, 0), Series: make([]schema.Series, 0),
} }
data[query.Hostname][metric][scope] = scopeData metricData[scope] = scopeData
} }
for ndx, res := range row { for ndx, res := range row {
+1 -1
View File
@@ -403,7 +403,7 @@ func (r *JobRepository) JobsStats(
// //
// Returns the requested statistic value or 0.0 if not found. // Returns the requested statistic value or 0.0 if not found.
func LoadJobStat(job *schema.Job, metric string, statType string) float64 { func LoadJobStat(job *schema.Job, metric string, statType string) float64 {
if stats, ok := job.Statistics[metric]; ok { if stats, ok := job.Statistics.Metrics[metric]; ok {
switch statType { switch statType {
case "avg": case "avg":
return stats.Avg return stats.Avg
+2 -2
View File
@@ -107,7 +107,7 @@ type JobClassTagger struct {
// repo provides access to job database operations // repo provides access to job database operations
repo JobRepository repo JobRepository
// getStatistics retrieves job statistics for analysis // getStatistics retrieves job statistics for analysis
getStatistics func(job *schema.Job) (map[string]schema.JobStatistics, error) getStatistics func(job *schema.Job) (schema.JobStatisticsSet, error)
// getMetricConfig retrieves metric configuration (limits) for a cluster // getMetricConfig retrieves metric configuration (limits) for a cluster
getMetricConfig func(cluster, subCluster string) map[string]*schema.Metric getMetricConfig func(cluster, subCluster string) map[string]*schema.Metric
} }
@@ -361,7 +361,7 @@ func (t *JobClassTagger) Match(job *schema.Job) {
// add metrics to env // add metrics to env
skipRule := false skipRule := false
for _, m := range ri.metrics { for _, m := range ri.metrics {
stats, ok := jobStats[m] stats, ok := jobStats.Metrics[m]
if !ok { if !ok {
cclog.Debugf("job classification: missing metric '%s' for rule %s on job %d", m, tag, job.JobID) cclog.Debugf("job classification: missing metric '%s' for rule %s on job %d", m, tag, job.JobID)
skipRule = true skipRule = true
+6 -6
View File
@@ -64,10 +64,10 @@ func TestClassifyJobMatch(t *testing.T) {
parameters: make(map[string]any), parameters: make(map[string]any),
tagType: "jobClass", tagType: "jobClass",
repo: mockRepo, repo: mockRepo,
getStatistics: func(job *schema.Job) (map[string]schema.JobStatistics, error) { getStatistics: func(job *schema.Job) (schema.JobStatisticsSet, error) {
return map[string]schema.JobStatistics{ return schema.JobStatisticsSet{Metrics: map[string]schema.JobStatistics{
"flops_any": {Min: 0, Max: 200, Avg: 150}, "flops_any": {Min: 0, Max: 200, Avg: 150},
}, nil }}, nil
}, },
getMetricConfig: func(cluster, subCluster string) map[string]*schema.Metric { getMetricConfig: func(cluster, subCluster string) map[string]*schema.Metric {
return map[string]*schema.Metric{ return map[string]*schema.Metric{
@@ -120,10 +120,10 @@ func TestMatch_NoMatch(t *testing.T) {
parameters: make(map[string]any), parameters: make(map[string]any),
tagType: "jobClass", tagType: "jobClass",
repo: mockRepo, repo: mockRepo,
getStatistics: func(job *schema.Job) (map[string]schema.JobStatistics, error) { getStatistics: func(job *schema.Job) (schema.JobStatisticsSet, error) {
return map[string]schema.JobStatistics{ return schema.JobStatisticsSet{Metrics: map[string]schema.JobStatistics{
"flops_any": {Min: 0, Max: 50, Avg: 20}, // Avg 20 < 100 "flops_any": {Min: 0, Max: 50, Avg: 20}, // Avg 20 < 100
}, nil }}, nil
}, },
getMetricConfig: func(cluster, subCluster string) map[string]*schema.Metric { getMetricConfig: func(cluster, subCluster string) map[string]*schema.Metric {
return map[string]*schema.Metric{ return map[string]*schema.Metric{
@@ -80,7 +80,7 @@ func RegisterFootprintWorker() {
continue continue
} }
job.Statistics = make(map[string]schema.JobStatistics) job.Statistics = schema.JobStatisticsSet{Metrics: make(map[string]schema.JobStatistics, len(allMetrics))}
for _, metric := range allMetrics { for _, metric := range allMetrics {
avg, min, max := 0.0, 0.0, 0.0 avg, min, max := 0.0, 0.0, 0.0
@@ -98,7 +98,7 @@ func RegisterFootprintWorker() {
} }
// Add values rounded to 2 digits: repo.LoadStats may return unrounded // Add values rounded to 2 digits: repo.LoadStats may return unrounded
job.Statistics[metric] = schema.JobStatistics{ job.Statistics.Metrics[metric] = schema.JobStatistics{
Unit: schema.Unit{ Unit: schema.Unit{
Prefix: archive.GetMetricConfig(job.Cluster, metric).Unit.Prefix, Prefix: archive.GetMetricConfig(job.Cluster, metric).Unit.Prefix,
Base: archive.GetMetricConfig(job.Cluster, metric).Unit.Base, Base: archive.GetMetricConfig(job.Cluster, metric).Unit.Base,
+7 -7
View File
@@ -299,7 +299,7 @@ func LoadAveragesFromArchive(
} }
for i, m := range metrics { for i, m := range metrics {
if stat, ok := metaFile.Statistics[m]; ok { if stat, ok := metaFile.Statistics.Metrics[m]; ok {
data[i] = append(data[i], schema.Float(stat.Avg)) data[i] = append(data[i], schema.Float(stat.Avg))
} else { } else {
data[i] = append(data[i], schema.NaN) data[i] = append(data[i], schema.NaN)
@@ -323,7 +323,7 @@ func LoadStatsFromArchive(
} }
for _, m := range metrics { for _, m := range metrics {
stat, ok := metaFile.Statistics[m] stat, ok := metaFile.Statistics.Metrics[m]
if !ok { if !ok {
data[m] = schema.MetricStatistics{Min: 0.0, Avg: 0.0, Max: 0.0} data[m] = schema.MetricStatistics{Min: 0.0, Avg: 0.0, Max: 0.0}
continue continue
@@ -349,19 +349,19 @@ func LoadScopedStatsFromArchive(
data, err := ar.LoadJobStats(job) data, err := ar.LoadJobStats(job)
if err != nil { if err != nil {
cclog.Errorf("Error while loading job stats from archiveBackend: %s", err.Error()) cclog.Errorf("Error while loading job stats from archiveBackend: %s", err.Error())
return nil, err return schema.ScopedJobStats{}, err
} }
return data, nil return data, nil
} }
// GetStatistics returns all metric statistics for a job. // GetStatistics returns all metric statistics for a job, including the
// Returns a map of metric names to their job-level statistics. // array-valued statistics groups (e.g. filesystems).
func GetStatistics(job *schema.Job) (map[string]schema.JobStatistics, error) { func GetStatistics(job *schema.Job) (schema.JobStatisticsSet, error) {
metaFile, err := ar.LoadJobMeta(job) metaFile, err := ar.LoadJobMeta(job)
if err != nil { if err != nil {
cclog.Errorf("Error while loading job metadata from archiveBackend: %s", err.Error()) cclog.Errorf("Error while loading job metadata from archiveBackend: %s", err.Error())
return nil, err return schema.JobStatisticsSet{}, err
} }
return metaFile.Statistics, nil return metaFile.Statistics, nil
+7 -2
View File
@@ -63,8 +63,11 @@ func initClusterConfig() error {
if _, ok := metricLookup[mc.Name]; !ok { if _, ok := metricLookup[mc.Name]; !ok {
metricLookup[mc.Name] = schema.GlobalMetricListItem{ metricLookup[mc.Name] = schema.GlobalMetricListItem{
Name: mc.Name, Scope: mc.Scope, Unit: mc.Unit, Footprint: mc.Footprint, Name: mc.Name, Scope: mc.Scope, Unit: mc.Unit, Footprint: mc.Footprint, Tooltip: mc.Tooltip,
} }
} else if item := metricLookup[mc.Name]; item.Tooltip == "" && mc.Tooltip != "" {
item.Tooltip = mc.Tooltip
metricLookup[mc.Name] = item
} }
availability := schema.ClusterSupport{Cluster: cluster.Name} availability := schema.ClusterSupport{Cluster: cluster.Name}
@@ -139,8 +142,10 @@ func initClusterConfig() error {
userItem, ok := userMetricLookup[mc.Name] userItem, ok := userMetricLookup[mc.Name]
if !ok { if !ok {
userItem = schema.GlobalMetricListItem{ userItem = schema.GlobalMetricListItem{
Name: mc.Name, Scope: mc.Scope, Unit: mc.Unit, Footprint: mc.Footprint, Name: mc.Name, Scope: mc.Scope, Unit: mc.Unit, Footprint: mc.Footprint, Tooltip: mc.Tooltip,
} }
} else if userItem.Tooltip == "" && mc.Tooltip != "" {
userItem.Tooltip = mc.Tooltip
} }
userItem.Availability = append(userItem.Availability, userAvailability) userItem.Availability = append(userItem.Availability, userAvailability)
userMetricLookup[mc.Name] = userItem userMetricLookup[mc.Name] = userItem
+6 -6
View File
@@ -93,7 +93,7 @@ func loadJobData(filename string, isCompressed bool) (schema.JobData, error) {
f, err := os.Open(filename) f, err := os.Open(filename)
if err != nil { if err != nil {
cclog.Errorf("fsBackend LoadJobData()- %v", err) cclog.Errorf("fsBackend LoadJobData()- %v", err)
return nil, err return schema.JobData{}, err
} }
defer f.Close() defer f.Close()
@@ -101,7 +101,7 @@ func loadJobData(filename string, isCompressed bool) (schema.JobData, error) {
r, err := gzip.NewReader(f) r, err := gzip.NewReader(f)
if err != nil { if err != nil {
cclog.Errorf(" %v", err) cclog.Errorf(" %v", err)
return nil, err return schema.JobData{}, err
} }
defer r.Close() defer r.Close()
@@ -126,7 +126,7 @@ func loadJobStats(filename string, isCompressed bool) (schema.ScopedJobStats, er
f, err := os.Open(filename) f, err := os.Open(filename)
if err != nil { if err != nil {
cclog.Errorf("fsBackend LoadJobStats()- %v", err) cclog.Errorf("fsBackend LoadJobStats()- %v", err)
return nil, err return schema.ScopedJobStats{}, err
} }
defer f.Close() defer f.Close()
@@ -134,13 +134,13 @@ func loadJobStats(filename string, isCompressed bool) (schema.ScopedJobStats, er
r, err := gzip.NewReader(f) r, err := gzip.NewReader(f)
if err != nil { if err != nil {
cclog.Errorf(" %v", err) cclog.Errorf(" %v", err)
return nil, err return schema.ScopedJobStats{}, err
} }
defer r.Close() defer r.Close()
if config.Keys.Validate { if config.Keys.Validate {
if err := schema.Validate(schema.Data, r); err != nil { if err := schema.Validate(schema.Data, r); err != nil {
return nil, fmt.Errorf("validate job data: %v", err) return schema.ScopedJobStats{}, fmt.Errorf("validate job data: %v", err)
} }
} }
@@ -148,7 +148,7 @@ func loadJobStats(filename string, isCompressed bool) (schema.ScopedJobStats, er
} else { } else {
if config.Keys.Validate { if config.Keys.Validate {
if err := schema.Validate(schema.Data, bufio.NewReader(f)); err != nil { if err := schema.Validate(schema.Data, bufio.NewReader(f)); err != nil {
return nil, fmt.Errorf("validate job data: %v", err) return schema.ScopedJobStats{}, fmt.Errorf("validate job data: %v", err)
} }
} }
return DecodeJobStats(bufio.NewReader(f), filename) return DecodeJobStats(bufio.NewReader(f), filename)
+1 -1
View File
@@ -129,7 +129,7 @@ func TestLoadJobData(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
for _, scopes := range data { for _, scopes := range data.Metrics {
// fmt.Printf("Metric name: %s\n", name) // fmt.Printf("Metric name: %s\n", name)
if _, exists := scopes[schema.MetricScopeNode]; !exists { if _, exists := scopes[schema.MetricScopeNode]; !exists {
+46 -26
View File
@@ -27,7 +27,7 @@ func DecodeJobData(r io.Reader, k string) (schema.JobData, error) {
if err, ok := data.(error); ok { if err, ok := data.(error); ok {
cclog.Warn("Error in decoded job data set") cclog.Warn("Error in decoded job data set")
return nil, err return schema.JobData{}, err
} }
return data.(schema.JobData), nil return data.(schema.JobData), nil
@@ -35,39 +35,59 @@ func DecodeJobData(r io.Reader, k string) (schema.JobData, error) {
func DecodeJobStats(r io.Reader, k string) (schema.ScopedJobStats, error) { func DecodeJobStats(r io.Reader, k string) (schema.ScopedJobStats, error) {
jobData, err := DecodeJobData(r, k) jobData, err := DecodeJobData(r, k)
if err != nil {
return schema.ScopedJobStats{}, err
}
// Convert schema.JobData to schema.ScopedJobStats // Convert schema.JobData to schema.ScopedJobStats
if jobData != nil { scopedJobStats := schema.ScopedJobStats{
scopedJobStats := make(schema.ScopedJobStats) Metrics: scopedStatsFromMetrics(jobData.Metrics),
for metric, metricData := range jobData {
if _, ok := scopedJobStats[metric]; !ok {
scopedJobStats[metric] = make(map[schema.MetricScope][]*schema.ScopedStats)
} }
for scope, jobMetric := range metricData { for _, group := range jobData.Groups {
if _, ok := scopedJobStats[metric][scope]; !ok { statsGroup := schema.ScopedStatsGroup{Key: group.Key}
scopedJobStats[metric][scope] = make([]*schema.ScopedStats, 0) for _, inst := range group.Instances {
} statsGroup.Instances = append(statsGroup.Instances, schema.ScopedStatsGroupInstance{
Name: inst.Name,
for _, series := range jobMetric.Series { Type: inst.Type,
scopedJobStats[metric][scope] = append(scopedJobStats[metric][scope], &schema.ScopedStats{ Metrics: scopedStatsFromMetrics(inst.Metrics),
Hostname: series.Hostname,
ID: series.ID,
Data: &series.Statistics,
}) })
} }
scopedJobStats.Groups = append(scopedJobStats.Groups, statsGroup)
}
// So that one can later check len(scopedJobStats[metric][scope]): Remove from map if empty
if len(scopedJobStats[metric][scope]) == 0 {
delete(scopedJobStats[metric], scope)
if len(scopedJobStats[metric]) == 0 {
delete(scopedJobStats, metric)
}
}
}
}
return scopedJobStats, nil return scopedJobStats, nil
} }
return nil, err
// scopedStatsFromMetrics reduces the full time series of every metric/scope to
// the per-series statistics. Scopes without any series are dropped so that
// callers can rely on len(stats[metric][scope]) being non-zero when present.
func scopedStatsFromMetrics(metrics map[string]schema.ScopedMetrics) map[string]schema.ScopedMetricStats {
stats := make(map[string]schema.ScopedMetricStats, len(metrics))
for metric, metricData := range metrics {
scoped := make(schema.ScopedMetricStats, len(metricData))
for scope, jobMetric := range metricData {
if len(jobMetric.Series) == 0 {
continue
}
series := make([]*schema.ScopedStats, 0, len(jobMetric.Series))
for i := range jobMetric.Series {
series = append(series, &schema.ScopedStats{
Hostname: jobMetric.Series[i].Hostname,
ID: jobMetric.Series[i].ID,
Data: &jobMetric.Series[i].Statistics,
})
}
scoped[scope] = series
}
if len(scoped) > 0 {
stats[metric] = scoped
}
}
return stats
} }
func DecodeJobMeta(r io.Reader) (*schema.Job, error) { func DecodeJobMeta(r io.Reader) (*schema.Job, error) {
+1 -1
View File
@@ -23,7 +23,7 @@ func JobToParquetRow(meta *schema.Job, data *schema.JobData) (*ParquetJobRow, er
} }
var statisticsJSON []byte var statisticsJSON []byte
if meta.Statistics != nil { if len(meta.Statistics.Metrics) > 0 || len(meta.Statistics.Groups) > 0 {
statisticsJSON, err = json.Marshal(meta.Statistics) statisticsJSON, err = json.Marshal(meta.Statistics)
if err != nil { if err != nil {
return nil, fmt.Errorf("marshal statistics: %w", err) return nil, fmt.Errorf("marshal statistics: %w", err)
+13 -13
View File
@@ -32,9 +32,9 @@ func TestParquetRowToJob(t *testing.T) {
{Hostname: "node001", HWThreads: []int{0, 1, 2, 3}}, {Hostname: "node001", HWThreads: []int{0, 1, 2, 3}},
{Hostname: "node002", HWThreads: []int{4, 5, 6, 7}}, {Hostname: "node002", HWThreads: []int{4, 5, 6, 7}},
}, },
Statistics: map[string]schema.JobStatistics{ Statistics: schema.JobStatisticsSet{Metrics: map[string]schema.JobStatistics{
"cpu_load": {Avg: 50.0, Min: 10.0, Max: 90.0}, "cpu_load": {Avg: 50.0, Min: 10.0, Max: 90.0},
}, }},
Tags: []*schema.Tag{ Tags: []*schema.Tag{
{Type: "test", Name: "tag1"}, {Type: "test", Name: "tag1"},
}, },
@@ -49,7 +49,7 @@ func TestParquetRowToJob(t *testing.T) {
}, },
} }
data := &schema.JobData{ data := &schema.JobData{Metrics: map[string]schema.ScopedMetrics{
"cpu_load": { "cpu_load": {
schema.MetricScopeNode: &schema.JobMetric{ schema.MetricScopeNode: &schema.JobMetric{
Unit: schema.Unit{Base: ""}, Unit: schema.Unit{Base: ""},
@@ -62,7 +62,7 @@ func TestParquetRowToJob(t *testing.T) {
}, },
}, },
}, },
} }}
// Convert to parquet row // Convert to parquet row
row, err := JobToParquetRow(meta, data) row, err := JobToParquetRow(meta, data)
@@ -134,10 +134,10 @@ func TestParquetRowToJob(t *testing.T) {
t.Errorf("Resources[0].HWThreads len = %d, want 4", len(gotMeta.Resources[0].HWThreads)) t.Errorf("Resources[0].HWThreads len = %d, want 4", len(gotMeta.Resources[0].HWThreads))
} }
if len(gotMeta.Statistics) != 1 { if len(gotMeta.Statistics.Metrics) != 1 {
t.Fatalf("Statistics len = %d, want 1", len(gotMeta.Statistics)) t.Fatalf("Statistics len = %d, want 1", len(gotMeta.Statistics.Metrics))
} }
if stat, ok := gotMeta.Statistics["cpu_load"]; !ok { if stat, ok := gotMeta.Statistics.Metrics["cpu_load"]; !ok {
t.Error("Statistics missing cpu_load") t.Error("Statistics missing cpu_load")
} else if stat.Avg != 50.0 { } else if stat.Avg != 50.0 {
t.Errorf("Statistics[cpu_load].Avg = %f, want 50.0", stat.Avg) t.Errorf("Statistics[cpu_load].Avg = %f, want 50.0", stat.Avg)
@@ -163,7 +163,7 @@ func TestParquetRowToJob(t *testing.T) {
if gotData == nil { if gotData == nil {
t.Fatal("JobData is nil") t.Fatal("JobData is nil")
} }
cpuLoad, ok := (*gotData)["cpu_load"] cpuLoad, ok := gotData.Metrics["cpu_load"]
if !ok { if !ok {
t.Fatal("JobData missing cpu_load") t.Fatal("JobData missing cpu_load")
} }
@@ -201,7 +201,7 @@ func TestParquetRowToJobNilOptionalFields(t *testing.T) {
}, },
} }
data := &schema.JobData{ data := &schema.JobData{Metrics: map[string]schema.ScopedMetrics{
"cpu_load": { "cpu_load": {
schema.MetricScopeNode: &schema.JobMetric{ schema.MetricScopeNode: &schema.JobMetric{
Timestep: 60, Timestep: 60,
@@ -210,7 +210,7 @@ func TestParquetRowToJobNilOptionalFields(t *testing.T) {
}, },
}, },
}, },
} }}
row, err := JobToParquetRow(meta, data) row, err := JobToParquetRow(meta, data)
if err != nil { if err != nil {
@@ -228,8 +228,8 @@ func TestParquetRowToJobNilOptionalFields(t *testing.T) {
if gotMeta.Tags != nil { if gotMeta.Tags != nil {
t.Errorf("Tags should be nil, got %v", gotMeta.Tags) t.Errorf("Tags should be nil, got %v", gotMeta.Tags)
} }
if gotMeta.Statistics != nil { if len(gotMeta.Statistics.Metrics) != 0 || len(gotMeta.Statistics.Groups) != 0 {
t.Errorf("Statistics should be nil, got %v", gotMeta.Statistics) t.Errorf("Statistics should be empty, got %v", gotMeta.Statistics)
} }
if gotMeta.MetaData != nil { if gotMeta.MetaData != nil {
t.Errorf("MetaData should be nil, got %v", gotMeta.MetaData) t.Errorf("MetaData should be nil, got %v", gotMeta.MetaData)
@@ -299,7 +299,7 @@ func TestRoundTripThroughParquetFile(t *testing.T) {
if gotData == nil { if gotData == nil {
t.Fatal("JobData is nil") t.Fatal("JobData is nil")
} }
if _, ok := (*gotData)["cpu_load"]; !ok { if _, ok := gotData.Metrics["cpu_load"]; !ok {
t.Error("JobData missing cpu_load") t.Error("JobData missing cpu_load")
} }
} }
+3 -3
View File
@@ -57,7 +57,7 @@ func makeTestJob(jobID int64) (*schema.Job, *schema.JobData) {
}, },
} }
data := schema.JobData{ data := schema.JobData{Metrics: map[string]schema.ScopedMetrics{
"cpu_load": { "cpu_load": {
schema.MetricScopeNode: &schema.JobMetric{ schema.MetricScopeNode: &schema.JobMetric{
Unit: schema.Unit{Base: ""}, Unit: schema.Unit{Base: ""},
@@ -70,7 +70,7 @@ func makeTestJob(jobID int64) (*schema.Job, *schema.JobData) {
}, },
}, },
}, },
} }}
return meta, &data return meta, &data
} }
@@ -132,7 +132,7 @@ func TestJobToParquetRowConversion(t *testing.T) {
if err := json.Unmarshal(decompressed, &jobData); err != nil { if err := json.Unmarshal(decompressed, &jobData); err != nil {
t.Fatalf("unmarshal metric data: %v", err) t.Fatalf("unmarshal metric data: %v", err)
} }
if _, ok := jobData["cpu_load"]; !ok { if _, ok := jobData.Metrics["cpu_load"]; !ok {
t.Error("metric data missing cpu_load key") t.Error("metric data missing cpu_load key")
} }
} }
+6 -6
View File
@@ -330,7 +330,7 @@ func (s3a *S3Archive) LoadJobData(job *schema.Job) (schema.JobData, error) {
}) })
if err != nil { if err != nil {
cclog.Errorf("S3Archive LoadJobData() > GetObject error: %v", err) cclog.Errorf("S3Archive LoadJobData() > GetObject error: %v", err)
return nil, err return schema.JobData{}, err
} }
defer result.Body.Close() defer result.Body.Close()
@@ -349,7 +349,7 @@ func (s3a *S3Archive) LoadJobData(job *schema.Job) (schema.JobData, error) {
r, err := gzip.NewReader(result.Body) r, err := gzip.NewReader(result.Body)
if err != nil { if err != nil {
cclog.Errorf("S3Archive LoadJobData() > gzip error: %v", err) cclog.Errorf("S3Archive LoadJobData() > gzip error: %v", err)
return nil, err return schema.JobData{}, err
} }
defer r.Close() defer r.Close()
@@ -381,14 +381,14 @@ func (s3a *S3Archive) LoadJobStats(job *schema.Job) (schema.ScopedJobStats, erro
}) })
if err != nil { if err != nil {
cclog.Errorf("S3Archive LoadJobStats() > GetObject error: %v", err) cclog.Errorf("S3Archive LoadJobStats() > GetObject error: %v", err)
return nil, err return schema.ScopedJobStats{}, err
} }
defer result.Body.Close() defer result.Body.Close()
if config.Keys.Validate { if config.Keys.Validate {
b, _ := io.ReadAll(result.Body) b, _ := io.ReadAll(result.Body)
if err := schema.Validate(schema.Data, bytes.NewReader(b)); err != nil { if err := schema.Validate(schema.Data, bytes.NewReader(b)); err != nil {
return nil, fmt.Errorf("validate job data: %v", err) return schema.ScopedJobStats{}, fmt.Errorf("validate job data: %v", err)
} }
return DecodeJobStats(bytes.NewReader(b), key) return DecodeJobStats(bytes.NewReader(b), key)
} }
@@ -400,14 +400,14 @@ func (s3a *S3Archive) LoadJobStats(job *schema.Job) (schema.ScopedJobStats, erro
r, err := gzip.NewReader(result.Body) r, err := gzip.NewReader(result.Body)
if err != nil { if err != nil {
cclog.Errorf("S3Archive LoadJobStats() > gzip error: %v", err) cclog.Errorf("S3Archive LoadJobStats() > gzip error: %v", err)
return nil, err return schema.ScopedJobStats{}, err
} }
defer r.Close() defer r.Close()
if config.Keys.Validate { if config.Keys.Validate {
b, _ := io.ReadAll(r) b, _ := io.ReadAll(r)
if err := schema.Validate(schema.Data, bytes.NewReader(b)); err != nil { if err := schema.Validate(schema.Data, bytes.NewReader(b)); err != nil {
return nil, fmt.Errorf("validate job data: %v", err) return schema.ScopedJobStats{}, fmt.Errorf("validate job data: %v", err)
} }
return DecodeJobStats(bytes.NewReader(b), keyGz) return DecodeJobStats(bytes.NewReader(b), keyGz)
} }
+5 -5
View File
@@ -252,7 +252,7 @@ func (sa *SqliteArchive) LoadJobData(job *schema.Job) (schema.JobData, error) {
job.JobID, job.Cluster, job.StartTime).Scan(&dataBlob, &compressed) job.JobID, job.Cluster, job.StartTime).Scan(&dataBlob, &compressed)
if err != nil { if err != nil {
cclog.Errorf("SqliteArchive LoadJobData() > query error: %v", err) cclog.Errorf("SqliteArchive LoadJobData() > query error: %v", err)
return nil, err return schema.JobData{}, err
} }
key := fmt.Sprintf("%s:%d:%d", job.Cluster, job.JobID, job.StartTime) key := fmt.Sprintf("%s:%d:%d", job.Cluster, job.JobID, job.StartTime)
@@ -261,7 +261,7 @@ func (sa *SqliteArchive) LoadJobData(job *schema.Job) (schema.JobData, error) {
gzipReader, err := gzip.NewReader(reader) gzipReader, err := gzip.NewReader(reader)
if err != nil { if err != nil {
cclog.Errorf("SqliteArchive LoadJobData() > gzip error: %v", err) cclog.Errorf("SqliteArchive LoadJobData() > gzip error: %v", err)
return nil, err return schema.JobData{}, err
} }
defer gzipReader.Close() defer gzipReader.Close()
reader = gzipReader reader = gzipReader
@@ -285,7 +285,7 @@ func (sa *SqliteArchive) LoadJobStats(job *schema.Job) (schema.ScopedJobStats, e
job.JobID, job.Cluster, job.StartTime).Scan(&dataBlob, &compressed) job.JobID, job.Cluster, job.StartTime).Scan(&dataBlob, &compressed)
if err != nil { if err != nil {
cclog.Errorf("SqliteArchive LoadJobStats() > query error: %v", err) cclog.Errorf("SqliteArchive LoadJobStats() > query error: %v", err)
return nil, err return schema.ScopedJobStats{}, err
} }
key := fmt.Sprintf("%s:%d:%d", job.Cluster, job.JobID, job.StartTime) key := fmt.Sprintf("%s:%d:%d", job.Cluster, job.JobID, job.StartTime)
@@ -294,7 +294,7 @@ func (sa *SqliteArchive) LoadJobStats(job *schema.Job) (schema.ScopedJobStats, e
gzipReader, err := gzip.NewReader(reader) gzipReader, err := gzip.NewReader(reader)
if err != nil { if err != nil {
cclog.Errorf("SqliteArchive LoadJobStats() > gzip error: %v", err) cclog.Errorf("SqliteArchive LoadJobStats() > gzip error: %v", err)
return nil, err return schema.ScopedJobStats{}, err
} }
defer gzipReader.Close() defer gzipReader.Close()
reader = gzipReader reader = gzipReader
@@ -303,7 +303,7 @@ func (sa *SqliteArchive) LoadJobStats(job *schema.Job) (schema.ScopedJobStats, e
if config.Keys.Validate { if config.Keys.Validate {
data, _ := io.ReadAll(reader) data, _ := io.ReadAll(reader)
if err := schema.Validate(schema.Data, bytes.NewReader(data)); err != nil { if err := schema.Validate(schema.Data, bytes.NewReader(data)); err != nil {
return nil, fmt.Errorf("validate job data: %v", err) return schema.ScopedJobStats{}, fmt.Errorf("validate job data: %v", err)
} }
return DecodeJobStats(bytes.NewReader(data), key) return DecodeJobStats(bytes.NewReader(data), key)
} }
+29 -29
View File
@@ -89,7 +89,7 @@ func (ccms *InternalMetricStore) LoadData(
queries, assignedScope, err := buildQueries(job, metrics, scopes, int64(resolution)) queries, assignedScope, err := buildQueries(job, metrics, scopes, int64(resolution))
if err != nil { if err != nil {
cclog.Errorf("Error while building queries for jobId %d, Metrics %v, Scopes %v: %s", job.JobID, metrics, scopes, err.Error()) cclog.Errorf("Error while building queries for jobId %d, Metrics %v, Scopes %v: %s", job.JobID, metrics, scopes, err.Error())
return nil, err return schema.JobData{}, err
} }
// Verify assignment is correct - log any inconsistencies for debugging // Verify assignment is correct - log any inconsistencies for debugging
@@ -110,11 +110,11 @@ func (ccms *InternalMetricStore) LoadData(
resBody, err := FetchData(req) resBody, err := FetchData(req)
if err != nil { if err != nil {
cclog.Errorf("Error while fetching data : %s", err.Error()) cclog.Errorf("Error while fetching data : %s", err.Error())
return nil, err return schema.JobData{}, err
} }
var errors []string var errors []string
jobData := make(schema.JobData) jobData := schema.JobData{Metrics: make(map[string]schema.ScopedMetrics)}
// Add safety check for potential index out of range errors // Add safety check for potential index out of range errors
if len(resBody.Results) != len(req.Queries) || len(assignedScope) != len(req.Queries) { if len(resBody.Results) != len(req.Queries) || len(assignedScope) != len(req.Queries) {
@@ -139,8 +139,8 @@ func (ccms *InternalMetricStore) LoadData(
continue continue
} }
if _, ok := jobData[metric]; !ok { if _, ok := jobData.Metrics[metric]; !ok {
jobData[metric] = make(map[schema.MetricScope]*schema.JobMetric) jobData.Metrics[metric] = make(schema.ScopedMetrics)
} }
res := mc.Timestep res := mc.Timestep
@@ -148,14 +148,14 @@ func (ccms *InternalMetricStore) LoadData(
res = int(row[0].Resolution) res = int(row[0].Resolution)
} }
jobMetric, ok := jobData[metric][scope] jobMetric, ok := jobData.Metrics[metric][scope]
if !ok { if !ok {
jobMetric = &schema.JobMetric{ jobMetric = &schema.JobMetric{
Unit: mc.Unit, Unit: mc.Unit,
Timestep: res, Timestep: res,
Series: make([]schema.Series, 0), Series: make([]schema.Series, 0),
} }
jobData[metric][scope] = jobMetric jobData.Metrics[metric][scope] = jobMetric
} }
for ndx, res := range row { for ndx, res := range row {
@@ -181,11 +181,11 @@ func (ccms *InternalMetricStore) LoadData(
}) })
} }
// So that one can later check len(jobData): // So that one can later check len(jobData.Metrics):
if len(jobMetric.Series) == 0 { if len(jobMetric.Series) == 0 {
delete(jobData[metric], scope) delete(jobData.Metrics[metric], scope)
if len(jobData[metric]) == 0 { if len(jobData.Metrics[metric]) == 0 {
delete(jobData, metric) delete(jobData.Metrics, metric)
} }
} }
} }
@@ -407,7 +407,7 @@ func (ccms *InternalMetricStore) LoadScopedStats(
queries, assignedScope, err := buildQueries(job, metrics, scopes, 0) queries, assignedScope, err := buildQueries(job, metrics, scopes, 0)
if err != nil { if err != nil {
cclog.Errorf("Error while building queries for jobId %d, Metrics %v, Scopes %v: %s", job.JobID, metrics, scopes, err.Error()) cclog.Errorf("Error while building queries for jobId %d, Metrics %v, Scopes %v: %s", job.JobID, metrics, scopes, err.Error())
return nil, err return schema.ScopedJobStats{}, err
} }
req := APIQueryRequest{ req := APIQueryRequest{
@@ -422,11 +422,11 @@ func (ccms *InternalMetricStore) LoadScopedStats(
resBody, err := FetchData(req) resBody, err := FetchData(req)
if err != nil { if err != nil {
cclog.Errorf("Error while fetching data : %s", err.Error()) cclog.Errorf("Error while fetching data : %s", err.Error())
return nil, err return schema.ScopedJobStats{}, err
} }
var errors []string var errors []string
scopedJobStats := make(schema.ScopedJobStats) scopedJobStats := schema.ScopedJobStats{Metrics: make(map[string]schema.ScopedMetricStats)}
for i, row := range resBody.Results { for i, row := range resBody.Results {
if len(row) == 0 { if len(row) == 0 {
@@ -437,12 +437,12 @@ func (ccms *InternalMetricStore) LoadScopedStats(
metric := query.Metric metric := query.Metric
scope := assignedScope[i] scope := assignedScope[i]
if _, ok := scopedJobStats[metric]; !ok { if _, ok := scopedJobStats.Metrics[metric]; !ok {
scopedJobStats[metric] = make(map[schema.MetricScope][]*schema.ScopedStats) scopedJobStats.Metrics[metric] = make(schema.ScopedMetricStats)
} }
if _, ok := scopedJobStats[metric][scope]; !ok { if _, ok := scopedJobStats.Metrics[metric][scope]; !ok {
scopedJobStats[metric][scope] = make([]*schema.ScopedStats, 0) scopedJobStats.Metrics[metric][scope] = make([]*schema.ScopedStats, 0)
} }
for ndx, res := range row { for ndx, res := range row {
@@ -456,7 +456,7 @@ func (ccms *InternalMetricStore) LoadScopedStats(
SanitizeStats(&res.Avg, &res.Min, &res.Max) SanitizeStats(&res.Avg, &res.Min, &res.Max)
scopedJobStats[metric][scope] = append(scopedJobStats[metric][scope], &schema.ScopedStats{ scopedJobStats.Metrics[metric][scope] = append(scopedJobStats.Metrics[metric][scope], &schema.ScopedStats{
Hostname: query.Hostname, Hostname: query.Hostname,
ID: id, ID: id,
Data: &schema.MetricStatistics{ Data: &schema.MetricStatistics{
@@ -467,11 +467,11 @@ func (ccms *InternalMetricStore) LoadScopedStats(
}) })
} }
// So that one can later check len(scopedJobStats[metric][scope]): Remove from map if empty // So that one can later check len(scopedJobStats.Metrics[metric][scope]): Remove from map if empty
if len(scopedJobStats[metric][scope]) == 0 { if len(scopedJobStats.Metrics[metric][scope]) == 0 {
delete(scopedJobStats[metric], scope) delete(scopedJobStats.Metrics[metric], scope)
if len(scopedJobStats[metric]) == 0 { if len(scopedJobStats.Metrics[metric]) == 0 {
delete(scopedJobStats, metric) delete(scopedJobStats.Metrics, metric)
} }
} }
} }
@@ -697,14 +697,14 @@ func (ccms *InternalMetricStore) LoadNodeListData(
// Init Nested Map Data Structures If Not Found // Init Nested Map Data Structures If Not Found
hostData, ok := data[query.Hostname] hostData, ok := data[query.Hostname]
if !ok { if !ok {
hostData = make(schema.JobData) hostData = schema.JobData{Metrics: make(map[string]schema.ScopedMetrics)}
data[query.Hostname] = hostData data[query.Hostname] = hostData
} }
metricData, ok := hostData[metric] metricData, ok := hostData.Metrics[metric]
if !ok { if !ok {
metricData = make(map[schema.MetricScope]*schema.JobMetric) metricData = make(schema.ScopedMetrics)
data[query.Hostname][metric] = metricData hostData.Metrics[metric] = metricData
} }
scopeData, ok := metricData[scope] scopeData, ok := metricData[scope]
@@ -714,7 +714,7 @@ func (ccms *InternalMetricStore) LoadNodeListData(
Timestep: res, Timestep: res,
Series: make([]schema.Series, 0), Series: make([]schema.Series, 0),
} }
data[query.Hostname][metric][scope] = scopeData metricData[scope] = scopeData
} }
for ndx, res := range row { for ndx, res := range row {
+2 -2
View File
@@ -231,9 +231,9 @@ func TestImportDataIntegrity(t *testing.T) {
} }
// Verify metric data exists // Verify metric data exists
if len(srcData) != len(dstData) { if len(srcData.Metrics) != len(dstData.Metrics) {
t.Errorf("Metric count mismatch for job %d: expected %d, got %d", t.Errorf("Metric count mismatch for job %d: expected %d, got %d",
srcJob.Meta.JobID, len(srcData), len(dstData)) srcJob.Meta.JobID, len(srcData.Metrics), len(dstData.Metrics))
} }
verifiedJobs++ verifiedJobs++
+10
View File
@@ -70,3 +70,13 @@ footer {
margin: 0rem 0.8rem; margin: 0rem 0.8rem;
white-space: nowrap; white-space: nowrap;
} }
/* Fix: prevent Sveltestrap collapseIn from leaving the navbar invisible
when prefers-reduced-motion causes transition-duration = 0ms and Svelte
skips calling tick(), leaving the node stuck in .collapsing (height:0). */
@media (prefers-reduced-motion: reduce) {
.navbar-collapse.collapsing {
height: auto !important;
overflow: visible !important;
}
}
+4
View File
@@ -60,6 +60,8 @@
let presetProject = $derived(filterPresets?.project ? filterPresets.project : ""); let presetProject = $derived(filterPresets?.project ? filterPresets.project : "");
let selectedCluster = $derived(filterPresets?.cluster ? filterPresets.cluster : null); let selectedCluster = $derived(filterPresets?.cluster ? filterPresets.cluster : null);
let selectedSubCluster = $derived(filterPresets?.partition ? filterPresets.partition : null); let selectedSubCluster = $derived(filterPresets?.partition ? filterPresets.partition : null);
const maxClusters = $derived($initq?.data?.clusters?.length || 0);
const maxSubClusters = $derived($initq?.data?.clusters?.find((c) => c.name == selectedCluster)?.subClusters?.length || 0);
let metrics = $derived.by(() => { let metrics = $derived.by(() => {
if (thisInit && ccconfig) { if (thisInit && ccconfig) {
if (selectedCluster) { if (selectedCluster) {
@@ -243,6 +245,8 @@
presetMetrics={metrics} presetMetrics={metrics}
cluster={selectedCluster} cluster={selectedCluster}
subCluster={selectedSubCluster} subCluster={selectedSubCluster}
{maxClusters}
{maxSubClusters}
configName="metricConfig_jobListMetrics" configName="metricConfig_jobListMetrics"
footprintSelect footprintSelect
{globalMetrics} {globalMetrics}
+3 -1
View File
@@ -63,8 +63,9 @@
let pendingHostnameFilter = $state(""); let pendingHostnameFilter = $state("");
let isMetricsSelectionOpen = $state(false); let isMetricsSelectionOpen = $state(false);
/* Derived Init Return */ /* Derived Init Returns */
const thisInit = $derived($initq?.data ? true : false); const thisInit = $derived($initq?.data ? true : false);
const maxSubClusters = $derived($initq?.data?.clusters?.find((c) => c.name == cluster)?.subClusters?.length || 0);
/* Derived States */ /* Derived States */
const ccconfig = $derived(thisInit ? getContext("cc-config") : null); const ccconfig = $derived(thisInit ? getContext("cc-config") : null);
@@ -268,6 +269,7 @@
{cluster} {cluster}
{subCluster} {subCluster}
{globalMetrics} {globalMetrics}
maxSubClusters={subCluster? null: maxSubClusters}
configName="nodeList_selectedMetrics" configName="nodeList_selectedMetrics"
applyMetrics={(newMetrics) => applyMetrics={(newMetrics) =>
selectedMetrics = [...newMetrics] selectedMetrics = [...newMetrics]
+4
View File
@@ -90,6 +90,8 @@
const shortDuration = $derived(ccconfig?.jobList_hideShortRunningJobs); const shortDuration = $derived(ccconfig?.jobList_hideShortRunningJobs);
let selectedCluster = $derived(filterPresets?.cluster ? filterPresets.cluster : null); let selectedCluster = $derived(filterPresets?.cluster ? filterPresets.cluster : null);
let selectedSubCluster = $derived(filterPresets?.partition ? filterPresets.partition : null); let selectedSubCluster = $derived(filterPresets?.partition ? filterPresets.partition : null);
const maxClusters = $derived($initq?.data?.clusters?.length || 0);
const maxSubClusters = $derived($initq?.data?.clusters?.find((c) => c.name == selectedCluster)?.subClusters?.length || 0);
let metrics = $derived.by(() => { let metrics = $derived.by(() => {
if (thisInit && ccconfig) { if (thisInit && ccconfig) {
if (selectedCluster) { if (selectedCluster) {
@@ -531,6 +533,8 @@
presetMetrics={metrics} presetMetrics={metrics}
cluster={selectedCluster} cluster={selectedCluster}
subCluster={selectedSubCluster} subCluster={selectedSubCluster}
{maxClusters}
{maxSubClusters}
configName="metricConfig_jobListMetrics" configName="metricConfig_jobListMetrics"
footprintSelect footprintSelect
{globalMetrics} {globalMetrics}
@@ -22,6 +22,8 @@
ModalFooter, ModalFooter,
Button, Button,
ListGroup, ListGroup,
Icon,
Tooltip
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { gql, getContextClient, mutationStore } from "@urql/svelte"; import { gql, getContextClient, mutationStore } from "@urql/svelte";
@@ -33,6 +35,8 @@
presetMetrics = [], presetMetrics = [],
cluster = null, cluster = null,
subCluster = null, subCluster = null,
maxClusters = null,
maxSubClusters = null,
footprintSelect = false, footprintSelect = false,
configName, configName,
globalMetrics, globalMetrics,
@@ -86,20 +90,45 @@
return availableMetrics; return availableMetrics;
} }
function printAvailabilityCount(metric, cluster) {
const avail = globalMetrics.find((gm) => gm.name === metric)?.availability
if (avail) {
if (!cluster) {
return `${avail.length} / ${maxClusters} Cluster`
} else {
const subAvail = avail.find((av) => av.cluster === cluster)?.subClusters
if (subAvail) {
return `${subAvail.length} / ${maxSubClusters} SubCluster`
} else {
return `0 / ${maxSubClusters} SubCluster`
}
}
}
return `0 / ${maxClusters} Cluster`
}
function printAvailability(metric, cluster) { function printAvailability(metric, cluster) {
const avail = globalMetrics.find((gm) => gm.name === metric)?.availability const avail = globalMetrics.find((gm) => gm.name === metric)?.availability
if (avail) { if (avail) {
if (!cluster) { if (!cluster) {
return avail.map((av) => av.cluster).join(', ') return avail.map((av) => av.cluster)
} else { } else {
const subAvail = avail.find((av) => av.cluster === cluster)?.subClusters const subAvail = avail.find((av) => av.cluster === cluster)?.subClusters
if (subAvail) { if (subAvail) {
return subAvail.join(', ') return subAvail
} else { } else {
return `Not available for ${cluster}` return [`Not available for ${cluster}`]
} }
} }
} }
return [`Not available for ${cluster}`]
}
function printTooltip(metric) {
const toolt = globalMetrics.find((gm) => gm.name === metric)?.tooltip
if (toolt) {
return toolt
}
return "" return ""
} }
@@ -172,7 +201,7 @@
</script> </script>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}> <Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>Configure columns (Metric availability shown)</ModalHeader> <ModalHeader>Configure columns</ModalHeader>
<ModalBody> <ModalBody>
<ListGroup> <ListGroup>
{#if footprintSelect} {#if footprintSelect}
@@ -213,9 +242,34 @@
/> />
{/if} {/if}
{metric} {metric}
<span style="float: right; text-align: justify;"> {#if maxClusters !== null || maxSubClusters !== null}
{printAvailability(metric, cluster)} <span style="float: right;" class="ms-1">
<Button id={`${metric}-avail-info`} outline color="secondary" size="sm" class="ml-2">
<b>{ printAvailabilityCount(metric, cluster) }</b>
</Button>
<Tooltip target={`${metric}-avail-info`} placement="right">
<b>Availability</b>
<ul style="text-align: left; padding-left: 1.0rem; margin-bottom: 0.25rem;">
{#each printAvailability(metric, cluster) as avail}
<li>{avail}</li>
{/each}
</ul>
</Tooltip>
</span> </span>
{/if}
{#if printTooltip(metric) !== ""}
<span style="float: right;">
<Button id={`${metric}-kind-info`} outline color="secondary" size="sm" class="ml-2">
<Icon name="info-square" />
</Button>
<Tooltip target={`${metric}-kind-info`} placement="right">
<b>Information</b>
<p style="text-align: left; margin-bottom: 0.25rem;">
{ printTooltip(metric) }
</p>
</Tooltip>
</span>
{/if}
</li> </li>
{/each} {/each}
</ListGroup> </ListGroup>
+1
View File
@@ -81,6 +81,7 @@ export function init(extraInitQuery = "") {
name name
scope scope
footprint footprint
tooltip
unit { base, prefix } unit { base, prefix }
availability { cluster, subClusters } availability { cluster, subClusters }
} }
+1 -1
View File
@@ -41,7 +41,7 @@
<DropdownToggle nav caret class="dropdown-item py-1 px-2"> <DropdownToggle nav caret class="dropdown-item py-1 px-2">
{cn} {cn}
</DropdownToggle> </DropdownToggle>
<DropdownMenu> <DropdownMenu style="max-height:75vh; overflow-y: auto;">
<DropdownItem class="py-1 px-2" <DropdownItem class="py-1 px-2"
href={item.href + cn} href={item.href + cn}
> >