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!
lowerIsBetter: Boolean
subClusters: [SubClusterConfig!]!
tooltip: String
}
type Tag {
@@ -306,6 +307,7 @@ type GlobalMetricListItem {
unit: Unit!
scope: MetricScope!
footprint: String
tooltip: String
availability: [ClusterSupport!]!
}
+6 -6
View File
@@ -196,8 +196,8 @@ func cleanup() {
func TestRestApi(t *testing.T) {
restapi := setup(t)
t.Cleanup(cleanup)
testData := schema.JobData{
"load_one": map[schema.MetricScope]*schema.JobMetric{
testData := schema.JobData{Metrics: map[string]schema.ScopedMetrics{
"load_one": {
schema.MetricScopeNode: {
Unit: schema.Unit{Base: "load"},
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) {
return testData, nil
@@ -497,8 +497,8 @@ func TestStopJobWithReusedJobId(t *testing.T) {
restapi := setup(t)
t.Cleanup(cleanup)
testData := schema.JobData{
"load_one": map[schema.MetricScope]*schema.JobMetric{
testData := schema.JobData{Metrics: map[string]schema.ScopedMetrics{
"load_one": {
schema.MetricScopeNode: {
Unit: schema.Unit{Base: "load"},
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) {
return testData, nil
+1 -1
View File
@@ -412,7 +412,7 @@ func (api *RestAPI) getJobByID(rw http.ResponseWriter, r *http.Request) {
}
res := []*JobMetricWithName{}
for name, md := range data {
for name, md := range data.Metrics {
for scope, metric := range md {
res = append(res, &JobMetricWithName{
Name: name,
+3 -3
View File
@@ -531,8 +531,8 @@ func TestNatsHandleStopJob(t *testing.T) {
},
}
testData := schema.JobData{
"load_one": map[schema.MetricScope]*schema.JobMetric{
testData := schema.JobData{Metrics: map[string]schema.ScopedMetrics{
"load_one": {
schema.MetricScopeNode: {
Unit: schema.Unit{Base: "load"},
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) {
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
}
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
nodeData, ok := data["node"]
if !ok {
@@ -82,7 +82,7 @@ func ArchiveJob(job *schema.Job, ctx context.Context) (*schema.Job, error) {
}
// Round AVG Result to 2 Digits
job.Statistics[metric] = schema.JobStatistics{
job.Statistics.Metrics[metric] = schema.JobStatistics{
Unit: schema.Unit{
Prefix: archive.GetMetricConfig(job.Cluster, metric).Unit.Prefix,
Base: archive.GetMetricConfig(job.Cluster, metric).Unit.Base,
+76
View File
@@ -100,6 +100,7 @@ type ComplexityRoot struct {
Footprint func(childComplexity int) int
Name func(childComplexity int) int
Scope func(childComplexity int) int
Tooltip func(childComplexity int) int
Unit func(childComplexity int) int
}
@@ -219,6 +220,7 @@ type ComplexityRoot struct {
Scope func(childComplexity int) int
SubClusters func(childComplexity int) int
Timestep func(childComplexity int) int
Tooltip 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
case "GlobalMetricListItem.tooltip":
if e.ComplexityRoot.GlobalMetricListItem.Tooltip == nil {
break
}
return e.ComplexityRoot.GlobalMetricListItem.Tooltip(childComplexity), true
case "GlobalMetricListItem.unit":
if e.ComplexityRoot.GlobalMetricListItem.Unit == nil {
break
@@ -1225,6 +1233,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
}
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":
if e.ComplexityRoot.MetricConfig.Unit == nil {
break
@@ -2426,6 +2440,7 @@ type MetricConfig {
alert: Float!
lowerIsBetter: Boolean
subClusters: [SubClusterConfig!]!
tooltip: String
}
type Tag {
@@ -2589,6 +2604,7 @@ type GlobalMetricListItem {
unit: Unit!
scope: MetricScope!
footprint: String
tooltip: String
availability: [ClusterSupport!]!
}
@@ -2973,6 +2989,8 @@ func (ec *executionContext) childFields_GlobalMetricListItem(ctx context.Context
return ec.fieldContext_GlobalMetricListItem_scope(ctx, field)
case "footprint":
return ec.fieldContext_GlobalMetricListItem_footprint(ctx, field)
case "tooltip":
return ec.fieldContext_GlobalMetricListItem_tooltip(ctx, field)
case "availability":
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)
case "subClusters":
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)
}
@@ -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"))
}
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) {
return graphql.ResolveField(
ctx,
@@ -7377,6 +7420,29 @@ func (ec *executionContext) fieldContext_MetricConfig_subClusters(_ context.Cont
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) {
return graphql.ResolveField(
ctx,
@@ -13311,6 +13377,11 @@ func (ec *executionContext) _GlobalMetricListItem(ctx context.Context, sel ast.S
if out.Values[i] == graphql.RequiredNull {
out.Invalids++
}
case "tooltip":
out.Values[i] = ec._GlobalMetricListItem_tooltip(ctx, field, obj)
if out.Values[i] == graphql.RequiredNull {
out.Invalids++
}
case "availability":
out.Values[i] = ec._GlobalMetricListItem_availability(ctx, field, obj)
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 {
out.Invalids++
}
case "tooltip":
out.Values[i] = ec._MetricConfig_tooltip(ctx, field, obj)
if out.Values[i] == graphql.RequiredNull {
out.Invalids++
}
default:
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{}
for name, md := range data {
for name, md := range data.Metrics {
for scope, metric := range md {
res = append(res, &model.JobMetricWithName{
Name: name,
@@ -581,7 +581,7 @@ func (r *queryResolver) ScopedJobStats(ctx context.Context, id string, metrics [
}
res := make([]*model.NamedStatsWithScope, 0)
for name, scoped := range data {
for name, scoped := range data.Metrics {
for scope, stats := range scoped {
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)
}
for metric, scopedMetrics := range data[hostname] {
for metric, scopedMetrics := range data[hostname].Metrics {
for scope, scopedMetric := range scopedMetrics {
host.Metrics = append(host.Metrics, &model.JobMetricWithName{
Name: metric,
+1 -1
View File
@@ -61,7 +61,7 @@ func (r *queryResolver) rooflineHeatmap(
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 {
cclog.Warnf("rooflineHeatmap(): 'flops_any' or 'mem_bw' missing for job %d", *job.ID)
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.
func checkJobData(d *schema.JobData) error {
for _, scopes := range *d {
for _, scopes := range d.Metrics {
// var newUnit schema.Unit
// TODO Add node scope if missing
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)
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",
job.JobID, job.User, job.Project, job.Cluster, job.SubCluster, err.Error())
} else {
@@ -144,7 +144,7 @@ func LoadData(job *schema.Job,
if rfErr != nil {
return rfErr, 0, 0
}
for _, v := range jd {
for _, v := range jd.Metrics {
for _, v_ := range v {
timestep := int64(0)
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.
if metrics != nil || scopes != nil {
if metrics == nil {
metrics = make([]string, 0, len(jd))
for k := range jd {
metrics = make([]string, 0, len(jd.Metrics))
for k := range jd.Metrics {
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 {
if perscope, ok := jd[metric]; ok {
if perscope, ok := jd.Metrics[metric]; ok {
if len(perscope) > 1 {
subset := make(map[schema.MetricScope]*schema.JobMetric)
subset := make(schema.ScopedMetrics)
for _, scope := range scopes {
if jm, ok := perscope[scope]; ok {
subset[scope] = jm
@@ -182,7 +185,7 @@ func LoadData(job *schema.Job,
}
}
res[metric] = perscope
res.Metrics[metric] = perscope
}
}
jd = res
@@ -199,7 +202,7 @@ func LoadData(job *schema.Job,
// 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.
const maxSeriesSize int = 8
for _, scopes := range jd {
for _, scopes := range jd.Metrics {
for _, jm := range scopes {
if jm.StatisticsSeries != nil || len(jm.Series) < maxSeriesSize {
continue
@@ -229,7 +232,7 @@ func LoadData(job *schema.Job,
if err, ok := data.(error); ok {
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
@@ -296,14 +299,14 @@ func LoadScopedJobStats(
if err != nil {
cclog.Errorf("failed to access metricDataRepo for cluster %s-%s: %s",
job.Cluster, job.SubCluster, err.Error())
return nil, err
return schema.ScopedJobStats{}, err
}
scopedStats, err := ms.LoadScopedStats(job, metrics, scopes, ctx)
if err != nil {
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())
return nil, err
return schema.ScopedJobStats{}, err
}
// Round Resulting Stat Values
@@ -451,7 +454,7 @@ func LoadNodeListData(
// Statistics are calculated as min/median/max.
const maxSeriesSize int = 8
for _, jd := range data {
for _, scopes := range jd {
for _, scopes := range jd.Metrics {
for _, jm := range scopes {
if jm.StatisticsSeries != nil || len(jm.Series) < maxSeriesSize {
continue
@@ -472,14 +475,32 @@ func LoadNodeListData(
// archived data (e.g., during resampling). This ensures the cached archive data remains
// immutable while allowing per-request transformations.
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 {
result[metricName] = make(map[schema.MetricScope]*schema.JobMetric, len(scopeMap))
scopes := make(schema.ScopedMetrics, len(scopeMap))
for scope, jobMetric := range scopeMap {
result[metricName][scope] = copyJobMetric(jobMetric)
scopes[scope] = copyJobMetric(jobMetric)
}
result[metricName] = scopes
}
return result
+22 -22
View File
@@ -13,7 +13,7 @@ import (
func TestDeepCopy(t *testing.T) {
nodeId := "0"
original := schema.JobData{
original := schema.JobData{Metrics: map[string]schema.ScopedMetrics{
"cpu_load": {
schema.MetricScopeNode: &schema.JobMetric{
Timestep: 60,
@@ -42,42 +42,42 @@ func TestDeepCopy(t *testing.T) {
},
},
},
}
}}
copied := deepCopy(original)
original["cpu_load"][schema.MetricScopeNode].Series[0].Data[0] = 999.0
original["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].Series[0].Data[0] = 999.0
original.Metrics["cpu_load"][schema.MetricScopeNode].StatisticsSeries.Min[0] = 888.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",
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",
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",
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",
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",
copied["cpu_load"][schema.MetricScopeNode].Series[0].Hostname)
copied.Metrics["cpu_load"][schema.MetricScopeNode].Series[0].Hostname)
}
}
func TestDeepCopyNilStatisticsSeries(t *testing.T) {
original := schema.JobData{
original := schema.JobData{Metrics: map[string]schema.ScopedMetrics{
"mem_used": {
schema.MetricScopeNode: &schema.JobMetric{
Timestep: 60,
@@ -90,18 +90,18 @@ func TestDeepCopyNilStatisticsSeries(t *testing.T) {
StatisticsSeries: nil,
},
},
}
}}
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",
copied["mem_used"][schema.MetricScopeNode].StatisticsSeries)
copied.Metrics["mem_used"][schema.MetricScopeNode].StatisticsSeries)
}
}
func TestDeepCopyEmptyPercentiles(t *testing.T) {
original := schema.JobData{
original := schema.JobData{Metrics: map[string]schema.ScopedMetrics{
"cpu_load": {
schema.MetricScopeNode: &schema.JobMetric{
Timestep: 60,
@@ -115,11 +115,11 @@ func TestDeepCopyEmptyPercentiles(t *testing.T) {
},
},
},
}
}}
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")
}
}
+27 -27
View File
@@ -235,7 +235,7 @@ func (ccms *CCMetricStore) LoadData(
queries, assignedScope, err := ccms.buildQueries(job, metrics, scopes, resolution)
if err != nil {
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
@@ -256,11 +256,11 @@ func (ccms *CCMetricStore) LoadData(
resBody, err := ccms.doRequest(ctx, &req)
if err != nil {
cclog.Errorf("Error while performing request for job %d: %s", job.JobID, err.Error())
return nil, err
return schema.JobData{}, err
}
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
if len(resBody.Results) != len(req.Queries) || len(assignedScope) != len(req.Queries) {
@@ -285,8 +285,8 @@ func (ccms *CCMetricStore) LoadData(
continue
}
if _, ok := jobData[metric]; !ok {
jobData[metric] = make(map[schema.MetricScope]*schema.JobMetric)
if _, ok := jobData.Metrics[metric]; !ok {
jobData.Metrics[metric] = make(schema.ScopedMetrics)
}
res := mc.Timestep
@@ -294,14 +294,14 @@ func (ccms *CCMetricStore) LoadData(
res = row[0].Resolution
}
jobMetric, ok := jobData[metric][scope]
jobMetric, ok := jobData.Metrics[metric][scope]
if !ok {
jobMetric = &schema.JobMetric{
Unit: mc.Unit,
Timestep: res,
Series: make([]schema.Series, 0),
}
jobData[metric][scope] = jobMetric
jobData.Metrics[metric][scope] = jobMetric
}
for ndx, res := range row {
@@ -329,9 +329,9 @@ func (ccms *CCMetricStore) LoadData(
// So that one can later check len(jobData):
if len(jobMetric.Series) == 0 {
delete(jobData[metric], scope)
if len(jobData[metric]) == 0 {
delete(jobData, metric)
delete(jobData.Metrics[metric], scope)
if len(jobData.Metrics[metric]) == 0 {
delete(jobData.Metrics, metric)
}
}
}
@@ -426,7 +426,7 @@ func (ccms *CCMetricStore) LoadScopedStats(
queries, assignedScope, err := ccms.buildQueries(job, metrics, scopes, 0)
if err != nil {
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{
@@ -441,23 +441,23 @@ func (ccms *CCMetricStore) LoadScopedStats(
resBody, err := ccms.doRequest(ctx, &req)
if err != nil {
cclog.Errorf("Error while performing request for job %d: %s", job.JobID, err.Error())
return nil, err
return schema.ScopedJobStats{}, err
}
var errors []string
scopedJobStats := make(schema.ScopedJobStats)
scopedJobStats := schema.ScopedJobStats{Metrics: make(map[string]schema.ScopedMetricStats)}
for i, row := range resBody.Results {
query := req.Queries[i]
metric := query.Metric
scope := assignedScope[i]
if _, ok := scopedJobStats[metric]; !ok {
scopedJobStats[metric] = make(map[schema.MetricScope][]*schema.ScopedStats)
if _, ok := scopedJobStats.Metrics[metric]; !ok {
scopedJobStats.Metrics[metric] = make(schema.ScopedMetricStats)
}
if _, ok := scopedJobStats[metric][scope]; !ok {
scopedJobStats[metric][scope] = make([]*schema.ScopedStats, 0)
if _, ok := scopedJobStats.Metrics[metric][scope]; !ok {
scopedJobStats.Metrics[metric][scope] = make([]*schema.ScopedStats, 0)
}
for ndx, res := range row {
@@ -471,7 +471,7 @@ func (ccms *CCMetricStore) LoadScopedStats(
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,
ID: id,
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
if len(scopedJobStats[metric][scope]) == 0 {
delete(scopedJobStats[metric], scope)
if len(scopedJobStats[metric]) == 0 {
delete(scopedJobStats, metric)
if len(scopedJobStats.Metrics[metric][scope]) == 0 {
delete(scopedJobStats.Metrics[metric], scope)
if len(scopedJobStats.Metrics[metric]) == 0 {
delete(scopedJobStats.Metrics, metric)
}
}
}
@@ -691,14 +691,14 @@ func (ccms *CCMetricStore) LoadNodeListData(
// Init Nested Map Data Structures If Not Found
hostData, ok := data[query.Hostname]
if !ok {
hostData = make(schema.JobData)
hostData = schema.JobData{Metrics: make(map[string]schema.ScopedMetrics)}
data[query.Hostname] = hostData
}
metricData, ok := hostData[metric]
metricData, ok := hostData.Metrics[metric]
if !ok {
metricData = make(map[schema.MetricScope]*schema.JobMetric)
data[query.Hostname][metric] = metricData
metricData = make(schema.ScopedMetrics)
hostData.Metrics[metric] = metricData
}
scopeData, ok := metricData[scope]
@@ -708,7 +708,7 @@ func (ccms *CCMetricStore) LoadNodeListData(
Timestep: res,
Series: make([]schema.Series, 0),
}
data[query.Hostname][metric][scope] = scopeData
metricData[scope] = scopeData
}
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.
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 {
case "avg":
return stats.Avg
+2 -2
View File
@@ -107,7 +107,7 @@ type JobClassTagger struct {
// repo provides access to job database operations
repo JobRepository
// 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 func(cluster, subCluster string) map[string]*schema.Metric
}
@@ -361,7 +361,7 @@ func (t *JobClassTagger) Match(job *schema.Job) {
// add metrics to env
skipRule := false
for _, m := range ri.metrics {
stats, ok := jobStats[m]
stats, ok := jobStats.Metrics[m]
if !ok {
cclog.Debugf("job classification: missing metric '%s' for rule %s on job %d", m, tag, job.JobID)
skipRule = true
+6 -6
View File
@@ -64,10 +64,10 @@ func TestClassifyJobMatch(t *testing.T) {
parameters: make(map[string]any),
tagType: "jobClass",
repo: mockRepo,
getStatistics: func(job *schema.Job) (map[string]schema.JobStatistics, error) {
return map[string]schema.JobStatistics{
getStatistics: func(job *schema.Job) (schema.JobStatisticsSet, error) {
return schema.JobStatisticsSet{Metrics: map[string]schema.JobStatistics{
"flops_any": {Min: 0, Max: 200, Avg: 150},
}, nil
}}, nil
},
getMetricConfig: func(cluster, subCluster string) map[string]*schema.Metric {
return map[string]*schema.Metric{
@@ -120,10 +120,10 @@ func TestMatch_NoMatch(t *testing.T) {
parameters: make(map[string]any),
tagType: "jobClass",
repo: mockRepo,
getStatistics: func(job *schema.Job) (map[string]schema.JobStatistics, error) {
return map[string]schema.JobStatistics{
getStatistics: func(job *schema.Job) (schema.JobStatisticsSet, error) {
return schema.JobStatisticsSet{Metrics: map[string]schema.JobStatistics{
"flops_any": {Min: 0, Max: 50, Avg: 20}, // Avg 20 < 100
}, nil
}}, nil
},
getMetricConfig: func(cluster, subCluster string) map[string]*schema.Metric {
return map[string]*schema.Metric{
@@ -80,7 +80,7 @@ func RegisterFootprintWorker() {
continue
}
job.Statistics = make(map[string]schema.JobStatistics)
job.Statistics = schema.JobStatisticsSet{Metrics: make(map[string]schema.JobStatistics, len(allMetrics))}
for _, metric := range allMetrics {
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
job.Statistics[metric] = schema.JobStatistics{
job.Statistics.Metrics[metric] = schema.JobStatistics{
Unit: schema.Unit{
Prefix: archive.GetMetricConfig(job.Cluster, metric).Unit.Prefix,
Base: archive.GetMetricConfig(job.Cluster, metric).Unit.Base,
+7 -7
View File
@@ -299,7 +299,7 @@ func LoadAveragesFromArchive(
}
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))
} else {
data[i] = append(data[i], schema.NaN)
@@ -323,7 +323,7 @@ func LoadStatsFromArchive(
}
for _, m := range metrics {
stat, ok := metaFile.Statistics[m]
stat, ok := metaFile.Statistics.Metrics[m]
if !ok {
data[m] = schema.MetricStatistics{Min: 0.0, Avg: 0.0, Max: 0.0}
continue
@@ -349,19 +349,19 @@ func LoadScopedStatsFromArchive(
data, err := ar.LoadJobStats(job)
if err != nil {
cclog.Errorf("Error while loading job stats from archiveBackend: %s", err.Error())
return nil, err
return schema.ScopedJobStats{}, err
}
return data, nil
}
// GetStatistics returns all metric statistics for a job.
// Returns a map of metric names to their job-level statistics.
func GetStatistics(job *schema.Job) (map[string]schema.JobStatistics, error) {
// GetStatistics returns all metric statistics for a job, including the
// array-valued statistics groups (e.g. filesystems).
func GetStatistics(job *schema.Job) (schema.JobStatisticsSet, error) {
metaFile, err := ar.LoadJobMeta(job)
if err != nil {
cclog.Errorf("Error while loading job metadata from archiveBackend: %s", err.Error())
return nil, err
return schema.JobStatisticsSet{}, err
}
return metaFile.Statistics, nil
+7 -2
View File
@@ -63,8 +63,11 @@ func initClusterConfig() error {
if _, ok := metricLookup[mc.Name]; !ok {
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}
@@ -139,8 +142,10 @@ func initClusterConfig() error {
userItem, ok := userMetricLookup[mc.Name]
if !ok {
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)
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)
if err != nil {
cclog.Errorf("fsBackend LoadJobData()- %v", err)
return nil, err
return schema.JobData{}, err
}
defer f.Close()
@@ -101,7 +101,7 @@ func loadJobData(filename string, isCompressed bool) (schema.JobData, error) {
r, err := gzip.NewReader(f)
if err != nil {
cclog.Errorf(" %v", err)
return nil, err
return schema.JobData{}, err
}
defer r.Close()
@@ -126,7 +126,7 @@ func loadJobStats(filename string, isCompressed bool) (schema.ScopedJobStats, er
f, err := os.Open(filename)
if err != nil {
cclog.Errorf("fsBackend LoadJobStats()- %v", err)
return nil, err
return schema.ScopedJobStats{}, err
}
defer f.Close()
@@ -134,13 +134,13 @@ func loadJobStats(filename string, isCompressed bool) (schema.ScopedJobStats, er
r, err := gzip.NewReader(f)
if err != nil {
cclog.Errorf(" %v", err)
return nil, err
return schema.ScopedJobStats{}, err
}
defer r.Close()
if config.Keys.Validate {
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 {
if config.Keys.Validate {
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)
+1 -1
View File
@@ -129,7 +129,7 @@ func TestLoadJobData(t *testing.T) {
t.Fatal(err)
}
for _, scopes := range data {
for _, scopes := range data.Metrics {
// fmt.Printf("Metric name: %s\n", name)
if _, exists := scopes[schema.MetricScopeNode]; !exists {
+53 -33
View File
@@ -27,7 +27,7 @@ func DecodeJobData(r io.Reader, k string) (schema.JobData, error) {
if err, ok := data.(error); ok {
cclog.Warn("Error in decoded job data set")
return nil, err
return schema.JobData{}, err
}
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) {
jobData, err := DecodeJobData(r, k)
// Convert schema.JobData to schema.ScopedJobStats
if jobData != nil {
scopedJobStats := make(schema.ScopedJobStats)
for metric, metricData := range jobData {
if _, ok := scopedJobStats[metric]; !ok {
scopedJobStats[metric] = make(map[schema.MetricScope][]*schema.ScopedStats)
}
for scope, jobMetric := range metricData {
if _, ok := scopedJobStats[metric][scope]; !ok {
scopedJobStats[metric][scope] = make([]*schema.ScopedStats, 0)
}
for _, series := range jobMetric.Series {
scopedJobStats[metric][scope] = append(scopedJobStats[metric][scope], &schema.ScopedStats{
Hostname: series.Hostname,
ID: series.ID,
Data: &series.Statistics,
})
}
// 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
if err != nil {
return schema.ScopedJobStats{}, err
}
return nil, err
// Convert schema.JobData to schema.ScopedJobStats
scopedJobStats := schema.ScopedJobStats{
Metrics: scopedStatsFromMetrics(jobData.Metrics),
}
for _, group := range jobData.Groups {
statsGroup := schema.ScopedStatsGroup{Key: group.Key}
for _, inst := range group.Instances {
statsGroup.Instances = append(statsGroup.Instances, schema.ScopedStatsGroupInstance{
Name: inst.Name,
Type: inst.Type,
Metrics: scopedStatsFromMetrics(inst.Metrics),
})
}
scopedJobStats.Groups = append(scopedJobStats.Groups, statsGroup)
}
return scopedJobStats, nil
}
// 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) {
+1 -1
View File
@@ -23,7 +23,7 @@ func JobToParquetRow(meta *schema.Job, data *schema.JobData) (*ParquetJobRow, er
}
var statisticsJSON []byte
if meta.Statistics != nil {
if len(meta.Statistics.Metrics) > 0 || len(meta.Statistics.Groups) > 0 {
statisticsJSON, err = json.Marshal(meta.Statistics)
if err != nil {
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: "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},
},
}},
Tags: []*schema.Tag{
{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": {
schema.MetricScopeNode: &schema.JobMetric{
Unit: schema.Unit{Base: ""},
@@ -62,7 +62,7 @@ func TestParquetRowToJob(t *testing.T) {
},
},
},
}
}}
// Convert to parquet row
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))
}
if len(gotMeta.Statistics) != 1 {
t.Fatalf("Statistics len = %d, want 1", len(gotMeta.Statistics))
if len(gotMeta.Statistics.Metrics) != 1 {
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")
} else if stat.Avg != 50.0 {
t.Errorf("Statistics[cpu_load].Avg = %f, want 50.0", stat.Avg)
@@ -163,7 +163,7 @@ func TestParquetRowToJob(t *testing.T) {
if gotData == nil {
t.Fatal("JobData is nil")
}
cpuLoad, ok := (*gotData)["cpu_load"]
cpuLoad, ok := gotData.Metrics["cpu_load"]
if !ok {
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": {
schema.MetricScopeNode: &schema.JobMetric{
Timestep: 60,
@@ -210,7 +210,7 @@ func TestParquetRowToJobNilOptionalFields(t *testing.T) {
},
},
},
}
}}
row, err := JobToParquetRow(meta, data)
if err != nil {
@@ -228,8 +228,8 @@ func TestParquetRowToJobNilOptionalFields(t *testing.T) {
if gotMeta.Tags != nil {
t.Errorf("Tags should be nil, got %v", gotMeta.Tags)
}
if gotMeta.Statistics != nil {
t.Errorf("Statistics should be nil, got %v", gotMeta.Statistics)
if len(gotMeta.Statistics.Metrics) != 0 || len(gotMeta.Statistics.Groups) != 0 {
t.Errorf("Statistics should be empty, got %v", gotMeta.Statistics)
}
if gotMeta.MetaData != nil {
t.Errorf("MetaData should be nil, got %v", gotMeta.MetaData)
@@ -299,7 +299,7 @@ func TestRoundTripThroughParquetFile(t *testing.T) {
if gotData == 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")
}
}
+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": {
schema.MetricScopeNode: &schema.JobMetric{
Unit: schema.Unit{Base: ""},
@@ -70,7 +70,7 @@ func makeTestJob(jobID int64) (*schema.Job, *schema.JobData) {
},
},
},
}
}}
return meta, &data
}
@@ -132,7 +132,7 @@ func TestJobToParquetRowConversion(t *testing.T) {
if err := json.Unmarshal(decompressed, &jobData); err != nil {
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")
}
}
+6 -6
View File
@@ -330,7 +330,7 @@ func (s3a *S3Archive) LoadJobData(job *schema.Job) (schema.JobData, error) {
})
if err != nil {
cclog.Errorf("S3Archive LoadJobData() > GetObject error: %v", err)
return nil, err
return schema.JobData{}, err
}
defer result.Body.Close()
@@ -349,7 +349,7 @@ func (s3a *S3Archive) LoadJobData(job *schema.Job) (schema.JobData, error) {
r, err := gzip.NewReader(result.Body)
if err != nil {
cclog.Errorf("S3Archive LoadJobData() > gzip error: %v", err)
return nil, err
return schema.JobData{}, err
}
defer r.Close()
@@ -381,14 +381,14 @@ func (s3a *S3Archive) LoadJobStats(job *schema.Job) (schema.ScopedJobStats, erro
})
if err != nil {
cclog.Errorf("S3Archive LoadJobStats() > GetObject error: %v", err)
return nil, err
return schema.ScopedJobStats{}, err
}
defer result.Body.Close()
if config.Keys.Validate {
b, _ := io.ReadAll(result.Body)
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)
}
@@ -400,14 +400,14 @@ func (s3a *S3Archive) LoadJobStats(job *schema.Job) (schema.ScopedJobStats, erro
r, err := gzip.NewReader(result.Body)
if err != nil {
cclog.Errorf("S3Archive LoadJobStats() > gzip error: %v", err)
return nil, err
return schema.ScopedJobStats{}, err
}
defer r.Close()
if config.Keys.Validate {
b, _ := io.ReadAll(r)
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)
}
+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)
if err != nil {
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)
@@ -261,7 +261,7 @@ func (sa *SqliteArchive) LoadJobData(job *schema.Job) (schema.JobData, error) {
gzipReader, err := gzip.NewReader(reader)
if err != nil {
cclog.Errorf("SqliteArchive LoadJobData() > gzip error: %v", err)
return nil, err
return schema.JobData{}, err
}
defer gzipReader.Close()
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)
if err != nil {
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)
@@ -294,7 +294,7 @@ func (sa *SqliteArchive) LoadJobStats(job *schema.Job) (schema.ScopedJobStats, e
gzipReader, err := gzip.NewReader(reader)
if err != nil {
cclog.Errorf("SqliteArchive LoadJobStats() > gzip error: %v", err)
return nil, err
return schema.ScopedJobStats{}, err
}
defer gzipReader.Close()
reader = gzipReader
@@ -303,7 +303,7 @@ func (sa *SqliteArchive) LoadJobStats(job *schema.Job) (schema.ScopedJobStats, e
if config.Keys.Validate {
data, _ := io.ReadAll(reader)
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)
}
+29 -29
View File
@@ -89,7 +89,7 @@ func (ccms *InternalMetricStore) LoadData(
queries, assignedScope, err := buildQueries(job, metrics, scopes, int64(resolution))
if err != nil {
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
@@ -110,11 +110,11 @@ func (ccms *InternalMetricStore) LoadData(
resBody, err := FetchData(req)
if err != nil {
cclog.Errorf("Error while fetching data : %s", err.Error())
return nil, err
return schema.JobData{}, err
}
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
if len(resBody.Results) != len(req.Queries) || len(assignedScope) != len(req.Queries) {
@@ -139,8 +139,8 @@ func (ccms *InternalMetricStore) LoadData(
continue
}
if _, ok := jobData[metric]; !ok {
jobData[metric] = make(map[schema.MetricScope]*schema.JobMetric)
if _, ok := jobData.Metrics[metric]; !ok {
jobData.Metrics[metric] = make(schema.ScopedMetrics)
}
res := mc.Timestep
@@ -148,14 +148,14 @@ func (ccms *InternalMetricStore) LoadData(
res = int(row[0].Resolution)
}
jobMetric, ok := jobData[metric][scope]
jobMetric, ok := jobData.Metrics[metric][scope]
if !ok {
jobMetric = &schema.JobMetric{
Unit: mc.Unit,
Timestep: res,
Series: make([]schema.Series, 0),
}
jobData[metric][scope] = jobMetric
jobData.Metrics[metric][scope] = jobMetric
}
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 {
delete(jobData[metric], scope)
if len(jobData[metric]) == 0 {
delete(jobData, metric)
delete(jobData.Metrics[metric], scope)
if len(jobData.Metrics[metric]) == 0 {
delete(jobData.Metrics, metric)
}
}
}
@@ -407,7 +407,7 @@ func (ccms *InternalMetricStore) LoadScopedStats(
queries, assignedScope, err := buildQueries(job, metrics, scopes, 0)
if err != nil {
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{
@@ -422,11 +422,11 @@ func (ccms *InternalMetricStore) LoadScopedStats(
resBody, err := FetchData(req)
if err != nil {
cclog.Errorf("Error while fetching data : %s", err.Error())
return nil, err
return schema.ScopedJobStats{}, err
}
var errors []string
scopedJobStats := make(schema.ScopedJobStats)
scopedJobStats := schema.ScopedJobStats{Metrics: make(map[string]schema.ScopedMetricStats)}
for i, row := range resBody.Results {
if len(row) == 0 {
@@ -437,12 +437,12 @@ func (ccms *InternalMetricStore) LoadScopedStats(
metric := query.Metric
scope := assignedScope[i]
if _, ok := scopedJobStats[metric]; !ok {
scopedJobStats[metric] = make(map[schema.MetricScope][]*schema.ScopedStats)
if _, ok := scopedJobStats.Metrics[metric]; !ok {
scopedJobStats.Metrics[metric] = make(schema.ScopedMetricStats)
}
if _, ok := scopedJobStats[metric][scope]; !ok {
scopedJobStats[metric][scope] = make([]*schema.ScopedStats, 0)
if _, ok := scopedJobStats.Metrics[metric][scope]; !ok {
scopedJobStats.Metrics[metric][scope] = make([]*schema.ScopedStats, 0)
}
for ndx, res := range row {
@@ -456,7 +456,7 @@ func (ccms *InternalMetricStore) LoadScopedStats(
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,
ID: id,
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
if len(scopedJobStats[metric][scope]) == 0 {
delete(scopedJobStats[metric], scope)
if len(scopedJobStats[metric]) == 0 {
delete(scopedJobStats, metric)
// So that one can later check len(scopedJobStats.Metrics[metric][scope]): Remove from map if empty
if len(scopedJobStats.Metrics[metric][scope]) == 0 {
delete(scopedJobStats.Metrics[metric], scope)
if len(scopedJobStats.Metrics[metric]) == 0 {
delete(scopedJobStats.Metrics, metric)
}
}
}
@@ -697,14 +697,14 @@ func (ccms *InternalMetricStore) LoadNodeListData(
// Init Nested Map Data Structures If Not Found
hostData, ok := data[query.Hostname]
if !ok {
hostData = make(schema.JobData)
hostData = schema.JobData{Metrics: make(map[string]schema.ScopedMetrics)}
data[query.Hostname] = hostData
}
metricData, ok := hostData[metric]
metricData, ok := hostData.Metrics[metric]
if !ok {
metricData = make(map[schema.MetricScope]*schema.JobMetric)
data[query.Hostname][metric] = metricData
metricData = make(schema.ScopedMetrics)
hostData.Metrics[metric] = metricData
}
scopeData, ok := metricData[scope]
@@ -714,7 +714,7 @@ func (ccms *InternalMetricStore) LoadNodeListData(
Timestep: res,
Series: make([]schema.Series, 0),
}
data[query.Hostname][metric][scope] = scopeData
metricData[scope] = scopeData
}
for ndx, res := range row {
+2 -2
View File
@@ -231,9 +231,9 @@ func TestImportDataIntegrity(t *testing.T) {
}
// 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",
srcJob.Meta.JobID, len(srcData), len(dstData))
srcJob.Meta.JobID, len(srcData.Metrics), len(dstData.Metrics))
}
verifiedJobs++
+10
View File
@@ -70,3 +70,13 @@ footer {
margin: 0rem 0.8rem;
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 selectedCluster = $derived(filterPresets?.cluster ? filterPresets.cluster : 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(() => {
if (thisInit && ccconfig) {
if (selectedCluster) {
@@ -243,6 +245,8 @@
presetMetrics={metrics}
cluster={selectedCluster}
subCluster={selectedSubCluster}
{maxClusters}
{maxSubClusters}
configName="metricConfig_jobListMetrics"
footprintSelect
{globalMetrics}
+3 -1
View File
@@ -63,8 +63,9 @@
let pendingHostnameFilter = $state("");
let isMetricsSelectionOpen = $state(false);
/* Derived Init Return */
/* Derived Init Returns */
const thisInit = $derived($initq?.data ? true : false);
const maxSubClusters = $derived($initq?.data?.clusters?.find((c) => c.name == cluster)?.subClusters?.length || 0);
/* Derived States */
const ccconfig = $derived(thisInit ? getContext("cc-config") : null);
@@ -268,6 +269,7 @@
{cluster}
{subCluster}
{globalMetrics}
maxSubClusters={subCluster? null: maxSubClusters}
configName="nodeList_selectedMetrics"
applyMetrics={(newMetrics) =>
selectedMetrics = [...newMetrics]
+4
View File
@@ -90,6 +90,8 @@
const shortDuration = $derived(ccconfig?.jobList_hideShortRunningJobs);
let selectedCluster = $derived(filterPresets?.cluster ? filterPresets.cluster : 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(() => {
if (thisInit && ccconfig) {
if (selectedCluster) {
@@ -531,6 +533,8 @@
presetMetrics={metrics}
cluster={selectedCluster}
subCluster={selectedSubCluster}
{maxClusters}
{maxSubClusters}
configName="metricConfig_jobListMetrics"
footprintSelect
{globalMetrics}
@@ -22,6 +22,8 @@
ModalFooter,
Button,
ListGroup,
Icon,
Tooltip
} from "@sveltestrap/sveltestrap";
import { gql, getContextClient, mutationStore } from "@urql/svelte";
@@ -33,6 +35,8 @@
presetMetrics = [],
cluster = null,
subCluster = null,
maxClusters = null,
maxSubClusters = null,
footprintSelect = false,
configName,
globalMetrics,
@@ -86,20 +90,45 @@
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) {
const avail = globalMetrics.find((gm) => gm.name === metric)?.availability
if (avail) {
if (!cluster) {
return avail.map((av) => av.cluster).join(', ')
return avail.map((av) => av.cluster)
} else {
const subAvail = avail.find((av) => av.cluster === cluster)?.subClusters
if (subAvail) {
return subAvail.join(', ')
return subAvail
} 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 ""
}
@@ -172,7 +201,7 @@
</script>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>Configure columns (Metric availability shown)</ModalHeader>
<ModalHeader>Configure columns</ModalHeader>
<ModalBody>
<ListGroup>
{#if footprintSelect}
@@ -213,9 +242,34 @@
/>
{/if}
{metric}
<span style="float: right; text-align: justify;">
{printAvailability(metric, cluster)}
</span>
{#if maxClusters !== null || maxSubClusters !== null}
<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>
{/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>
{/each}
</ListGroup>
+1
View File
@@ -81,6 +81,7 @@ export function init(extraInitQuery = "") {
name
scope
footprint
tooltip
unit { base, prefix }
availability { cluster, subClusters }
}
+1 -1
View File
@@ -41,7 +41,7 @@
<DropdownToggle nav caret class="dropdown-item py-1 px-2">
{cn}
</DropdownToggle>
<DropdownMenu>
<DropdownMenu style="max-height:75vh; overflow-y: auto;">
<DropdownItem class="py-1 px-2"
href={item.href + cn}
>