feat: display energy usage in job view

- optional emission constant config line added
This commit is contained in:
Christoph Kluge 2024-09-27 13:45:44 +02:00
parent f53fc088ec
commit 48225662b1
11 changed files with 611 additions and 10 deletions

View File

@ -18,6 +18,7 @@ type Job {
numNodes: Int! numNodes: Int!
numHWThreads: Int! numHWThreads: Int!
numAcc: Int! numAcc: Int!
energy: Float!
SMT: Int! SMT: Int!
exclusive: Int! exclusive: Int!
partition: String! partition: String!
@ -28,6 +29,7 @@ type Job {
resources: [Resource!]! resources: [Resource!]!
concurrentJobs: JobLinkResultList concurrentJobs: JobLinkResultList
footprint: [FootprintValue] footprint: [FootprintValue]
energyFootprint: [EnergyFootprintValue]
metaData: Any metaData: Any
userData: User userData: User
} }
@ -65,6 +67,12 @@ type FootprintValue {
value: Float! value: Float!
} }
type EnergyFootprintValue {
hardware: String!
metric: String!
value: Float!
}
type MetricValue { type MetricValue {
name: String name: String
unit: Unit! unit: Unit!

View File

@ -74,6 +74,12 @@ type ComplexityRoot struct {
Name func(childComplexity int) int Name func(childComplexity int) int
} }
EnergyFootprintValue struct {
Hardware func(childComplexity int) int
Metric func(childComplexity int) int
Value func(childComplexity int) int
}
FootprintValue struct { FootprintValue struct {
Name func(childComplexity int) int Name func(childComplexity int) int
Stat func(childComplexity int) int Stat func(childComplexity int) int
@ -108,6 +114,8 @@ type ComplexityRoot struct {
Cluster func(childComplexity int) int Cluster func(childComplexity int) int
ConcurrentJobs func(childComplexity int) int ConcurrentJobs func(childComplexity int) int
Duration func(childComplexity int) int Duration func(childComplexity int) int
Energy func(childComplexity int) int
EnergyFootprint func(childComplexity int) int
Exclusive func(childComplexity int) int Exclusive func(childComplexity int) int
Footprint func(childComplexity int) int Footprint func(childComplexity int) int
ID func(childComplexity int) int ID func(childComplexity int) int
@ -349,6 +357,7 @@ type JobResolver interface {
ConcurrentJobs(ctx context.Context, obj *schema.Job) (*model.JobLinkResultList, error) ConcurrentJobs(ctx context.Context, obj *schema.Job) (*model.JobLinkResultList, error)
Footprint(ctx context.Context, obj *schema.Job) ([]*model.FootprintValue, error) Footprint(ctx context.Context, obj *schema.Job) ([]*model.FootprintValue, error)
EnergyFootprint(ctx context.Context, obj *schema.Job) ([]*model.EnergyFootprintValue, error)
MetaData(ctx context.Context, obj *schema.Job) (any, error) MetaData(ctx context.Context, obj *schema.Job) (any, error)
UserData(ctx context.Context, obj *schema.Job) (*model.User, error) UserData(ctx context.Context, obj *schema.Job) (*model.User, error)
} }
@ -469,6 +478,27 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Count.Name(childComplexity), true return e.complexity.Count.Name(childComplexity), true
case "EnergyFootprintValue.hardware":
if e.complexity.EnergyFootprintValue.Hardware == nil {
break
}
return e.complexity.EnergyFootprintValue.Hardware(childComplexity), true
case "EnergyFootprintValue.metric":
if e.complexity.EnergyFootprintValue.Metric == nil {
break
}
return e.complexity.EnergyFootprintValue.Metric(childComplexity), true
case "EnergyFootprintValue.value":
if e.complexity.EnergyFootprintValue.Value == nil {
break
}
return e.complexity.EnergyFootprintValue.Value(childComplexity), true
case "FootprintValue.name": case "FootprintValue.name":
if e.complexity.FootprintValue.Name == nil { if e.complexity.FootprintValue.Name == nil {
break break
@ -595,6 +625,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Job.Duration(childComplexity), true return e.complexity.Job.Duration(childComplexity), true
case "Job.energy":
if e.complexity.Job.Energy == nil {
break
}
return e.complexity.Job.Energy(childComplexity), true
case "Job.energyFootprint":
if e.complexity.Job.EnergyFootprint == nil {
break
}
return e.complexity.Job.EnergyFootprint(childComplexity), true
case "Job.exclusive": case "Job.exclusive":
if e.complexity.Job.Exclusive == nil { if e.complexity.Job.Exclusive == nil {
break break
@ -1862,6 +1906,7 @@ type Job {
numNodes: Int! numNodes: Int!
numHWThreads: Int! numHWThreads: Int!
numAcc: Int! numAcc: Int!
energy: Float!
SMT: Int! SMT: Int!
exclusive: Int! exclusive: Int!
partition: String! partition: String!
@ -1872,6 +1917,7 @@ type Job {
resources: [Resource!]! resources: [Resource!]!
concurrentJobs: JobLinkResultList concurrentJobs: JobLinkResultList
footprint: [FootprintValue] footprint: [FootprintValue]
energyFootprint: [EnergyFootprintValue]
metaData: Any metaData: Any
userData: User userData: User
} }
@ -1909,6 +1955,12 @@ type FootprintValue {
value: Float! value: Float!
} }
type EnergyFootprintValue {
hardware: String!
metric: String!
value: Float!
}
type MetricValue { type MetricValue {
name: String name: String
unit: Unit! unit: Unit!
@ -3173,6 +3225,138 @@ func (ec *executionContext) fieldContext_Count_count(_ context.Context, field gr
return fc, nil return fc, nil
} }
func (ec *executionContext) _EnergyFootprintValue_hardware(ctx context.Context, field graphql.CollectedField, obj *model.EnergyFootprintValue) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_EnergyFootprintValue_hardware(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Hardware, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(string)
fc.Result = res
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_EnergyFootprintValue_hardware(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "EnergyFootprintValue",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _EnergyFootprintValue_metric(ctx context.Context, field graphql.CollectedField, obj *model.EnergyFootprintValue) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_EnergyFootprintValue_metric(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Metric, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(string)
fc.Result = res
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_EnergyFootprintValue_metric(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "EnergyFootprintValue",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _EnergyFootprintValue_value(ctx context.Context, field graphql.CollectedField, obj *model.EnergyFootprintValue) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_EnergyFootprintValue_value(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Value, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(float64)
fc.Result = res
return ec.marshalNFloat2float64(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_EnergyFootprintValue_value(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "EnergyFootprintValue",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type Float does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _FootprintValue_name(ctx context.Context, field graphql.CollectedField, obj *model.FootprintValue) (ret graphql.Marshaler) { func (ec *executionContext) _FootprintValue_name(ctx context.Context, field graphql.CollectedField, obj *model.FootprintValue) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_FootprintValue_name(ctx, field) fc, err := ec.fieldContext_FootprintValue_name(ctx, field)
if err != nil { if err != nil {
@ -4340,6 +4524,50 @@ func (ec *executionContext) fieldContext_Job_numAcc(_ context.Context, field gra
return fc, nil return fc, nil
} }
func (ec *executionContext) _Job_energy(ctx context.Context, field graphql.CollectedField, obj *schema.Job) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Job_energy(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Energy, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(float64)
fc.Result = res
return ec.marshalNFloat2float64(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Job_energy(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Job",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type Float does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Job_SMT(ctx context.Context, field graphql.CollectedField, obj *schema.Job) (ret graphql.Marshaler) { func (ec *executionContext) _Job_SMT(ctx context.Context, field graphql.CollectedField, obj *schema.Job) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Job_SMT(ctx, field) fc, err := ec.fieldContext_Job_SMT(ctx, field)
if err != nil { if err != nil {
@ -4810,6 +5038,55 @@ func (ec *executionContext) fieldContext_Job_footprint(_ context.Context, field
return fc, nil return fc, nil
} }
func (ec *executionContext) _Job_energyFootprint(ctx context.Context, field graphql.CollectedField, obj *schema.Job) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Job_energyFootprint(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Job().EnergyFootprint(rctx, obj)
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.([]*model.EnergyFootprintValue)
fc.Result = res
return ec.marshalOEnergyFootprintValue2ᚕᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐEnergyFootprintValue(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Job_energyFootprint(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Job",
Field: field,
IsMethod: true,
IsResolver: true,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "hardware":
return ec.fieldContext_EnergyFootprintValue_hardware(ctx, field)
case "metric":
return ec.fieldContext_EnergyFootprintValue_metric(ctx, field)
case "value":
return ec.fieldContext_EnergyFootprintValue_value(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type EnergyFootprintValue", field.Name)
},
}
return fc, nil
}
func (ec *executionContext) _Job_metaData(ctx context.Context, field graphql.CollectedField, obj *schema.Job) (ret graphql.Marshaler) { func (ec *executionContext) _Job_metaData(ctx context.Context, field graphql.CollectedField, obj *schema.Job) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Job_metaData(ctx, field) fc, err := ec.fieldContext_Job_metaData(ctx, field)
if err != nil { if err != nil {
@ -5518,6 +5795,8 @@ func (ec *executionContext) fieldContext_JobResultList_items(_ context.Context,
return ec.fieldContext_Job_numHWThreads(ctx, field) return ec.fieldContext_Job_numHWThreads(ctx, field)
case "numAcc": case "numAcc":
return ec.fieldContext_Job_numAcc(ctx, field) return ec.fieldContext_Job_numAcc(ctx, field)
case "energy":
return ec.fieldContext_Job_energy(ctx, field)
case "SMT": case "SMT":
return ec.fieldContext_Job_SMT(ctx, field) return ec.fieldContext_Job_SMT(ctx, field)
case "exclusive": case "exclusive":
@ -5538,6 +5817,8 @@ func (ec *executionContext) fieldContext_JobResultList_items(_ context.Context,
return ec.fieldContext_Job_concurrentJobs(ctx, field) return ec.fieldContext_Job_concurrentJobs(ctx, field)
case "footprint": case "footprint":
return ec.fieldContext_Job_footprint(ctx, field) return ec.fieldContext_Job_footprint(ctx, field)
case "energyFootprint":
return ec.fieldContext_Job_energyFootprint(ctx, field)
case "metaData": case "metaData":
return ec.fieldContext_Job_metaData(ctx, field) return ec.fieldContext_Job_metaData(ctx, field)
case "userData": case "userData":
@ -8480,6 +8761,8 @@ func (ec *executionContext) fieldContext_Query_job(ctx context.Context, field gr
return ec.fieldContext_Job_numHWThreads(ctx, field) return ec.fieldContext_Job_numHWThreads(ctx, field)
case "numAcc": case "numAcc":
return ec.fieldContext_Job_numAcc(ctx, field) return ec.fieldContext_Job_numAcc(ctx, field)
case "energy":
return ec.fieldContext_Job_energy(ctx, field)
case "SMT": case "SMT":
return ec.fieldContext_Job_SMT(ctx, field) return ec.fieldContext_Job_SMT(ctx, field)
case "exclusive": case "exclusive":
@ -8500,6 +8783,8 @@ func (ec *executionContext) fieldContext_Query_job(ctx context.Context, field gr
return ec.fieldContext_Job_concurrentJobs(ctx, field) return ec.fieldContext_Job_concurrentJobs(ctx, field)
case "footprint": case "footprint":
return ec.fieldContext_Job_footprint(ctx, field) return ec.fieldContext_Job_footprint(ctx, field)
case "energyFootprint":
return ec.fieldContext_Job_energyFootprint(ctx, field)
case "metaData": case "metaData":
return ec.fieldContext_Job_metaData(ctx, field) return ec.fieldContext_Job_metaData(ctx, field)
case "userData": case "userData":
@ -13740,6 +14025,55 @@ func (ec *executionContext) _Count(ctx context.Context, sel ast.SelectionSet, ob
return out return out
} }
var energyFootprintValueImplementors = []string{"EnergyFootprintValue"}
func (ec *executionContext) _EnergyFootprintValue(ctx context.Context, sel ast.SelectionSet, obj *model.EnergyFootprintValue) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, energyFootprintValueImplementors)
out := graphql.NewFieldSet(fields)
deferred := make(map[string]*graphql.FieldSet)
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("EnergyFootprintValue")
case "hardware":
out.Values[i] = ec._EnergyFootprintValue_hardware(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "metric":
out.Values[i] = ec._EnergyFootprintValue_metric(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "value":
out.Values[i] = ec._EnergyFootprintValue_value(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch(ctx)
if out.Invalids > 0 {
return graphql.Null
}
atomic.AddInt32(&ec.deferred, int32(len(deferred)))
for label, dfs := range deferred {
ec.processDeferredGroup(graphql.DeferredGroup{
Label: label,
Path: graphql.GetPath(ctx),
FieldSet: dfs,
Context: ctx,
})
}
return out
}
var footprintValueImplementors = []string{"FootprintValue"} var footprintValueImplementors = []string{"FootprintValue"}
func (ec *executionContext) _FootprintValue(ctx context.Context, sel ast.SelectionSet, obj *model.FootprintValue) graphql.Marshaler { func (ec *executionContext) _FootprintValue(ctx context.Context, sel ast.SelectionSet, obj *model.FootprintValue) graphql.Marshaler {
@ -14048,6 +14382,11 @@ func (ec *executionContext) _Job(ctx context.Context, sel ast.SelectionSet, obj
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1) atomic.AddUint32(&out.Invalids, 1)
} }
case "energy":
out.Values[i] = ec._Job_energy(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1)
}
case "SMT": case "SMT":
out.Values[i] = ec._Job_SMT(ctx, field, obj) out.Values[i] = ec._Job_SMT(ctx, field, obj)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
@ -14184,6 +14523,39 @@ func (ec *executionContext) _Job(ctx context.Context, sel ast.SelectionSet, obj
continue continue
} }
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
case "energyFootprint":
field := field
innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
}
}()
res = ec._Job_energyFootprint(ctx, field, obj)
return res
}
if field.Deferrable != nil {
dfs, ok := deferred[field.Deferrable.Label]
di := 0
if ok {
dfs.AddField(field)
di = len(dfs.Values) - 1
} else {
dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
deferred[field.Deferrable.Label] = dfs
}
dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
return innerFunc(ctx, dfs)
})
// don't run the out.Concurrently() call below
out.Values[i] = graphql.Null
continue
}
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
case "metaData": case "metaData":
field := field field := field
@ -18120,6 +18492,54 @@ func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast
return res return res
} }
func (ec *executionContext) marshalOEnergyFootprintValue2ᚕᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐEnergyFootprintValue(ctx context.Context, sel ast.SelectionSet, v []*model.EnergyFootprintValue) graphql.Marshaler {
if v == nil {
return graphql.Null
}
ret := make(graphql.Array, len(v))
var wg sync.WaitGroup
isLen1 := len(v) == 1
if !isLen1 {
wg.Add(len(v))
}
for i := range v {
i := i
fc := &graphql.FieldContext{
Index: &i,
Result: &v[i],
}
ctx := graphql.WithFieldContext(ctx, fc)
f := func(i int) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = nil
}
}()
if !isLen1 {
defer wg.Done()
}
ret[i] = ec.marshalOEnergyFootprintValue2ᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐEnergyFootprintValue(ctx, sel, v[i])
}
if isLen1 {
f(i)
} else {
go f(i)
}
}
wg.Wait()
return ret
}
func (ec *executionContext) marshalOEnergyFootprintValue2ᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐEnergyFootprintValue(ctx context.Context, sel ast.SelectionSet, v *model.EnergyFootprintValue) graphql.Marshaler {
if v == nil {
return graphql.Null
}
return ec._EnergyFootprintValue(ctx, sel, v)
}
func (ec *executionContext) unmarshalOFloat2float64(ctx context.Context, v interface{}) (float64, error) { func (ec *executionContext) unmarshalOFloat2float64(ctx context.Context, v interface{}) (float64, error) {
res, err := graphql.UnmarshalFloatContext(ctx, v) res, err := graphql.UnmarshalFloatContext(ctx, v)
return res, graphql.ErrorOnPath(ctx, err) return res, graphql.ErrorOnPath(ctx, err)

View File

@ -16,6 +16,12 @@ type Count struct {
Count int `json:"count"` Count int `json:"count"`
} }
type EnergyFootprintValue struct {
Hardware string `json:"hardware"`
Metric string `json:"metric"`
Value float64 `json:"value"`
}
type FloatRange struct { type FloatRange struct {
From float64 `json:"from"` From float64 `json:"from"`
To float64 `json:"to"` To float64 `json:"to"`

View File

@ -8,6 +8,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"regexp"
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
@ -71,6 +72,45 @@ func (r *jobResolver) Footprint(ctx context.Context, obj *schema.Job) ([]*model.
return res, err return res, err
} }
// EnergyFootprint is the resolver for the energyFootprint field.
func (r *jobResolver) EnergyFootprint(ctx context.Context, obj *schema.Job) ([]*model.EnergyFootprintValue, error) {
rawEnergyFootprint, err := r.Repo.FetchEnergyFootprint(obj)
if err != nil {
log.Warn("Error while fetching job energy footprint data")
return nil, err
}
res := []*model.EnergyFootprintValue{}
for name, value := range rawEnergyFootprint {
// Suboptimal: Nearly hardcoded metric name expectations
matchCpu := regexp.MustCompile(`cpu|Cpu|CPU`)
matchAcc := regexp.MustCompile(`acc|Acc|ACC`)
matchMem := regexp.MustCompile(`mem|Mem|MEM`)
matchCore := regexp.MustCompile(`core|Core|CORE`)
hwType := ""
switch test := name; { // NOtice ';' for var declaration
case matchCpu.MatchString(test):
hwType = "CPU"
case matchAcc.MatchString(test):
hwType = "Accelerator"
case matchMem.MatchString(test):
hwType = "Memory"
case matchCore.MatchString(test):
hwType = "Core"
default:
hwType = "Hardware"
}
res = append(res, &model.EnergyFootprintValue{
Hardware: hwType,
Metric: name,
Value: value,
})
}
return res, err
}
// MetaData is the resolver for the metaData field. // MetaData is the resolver for the metaData field.
func (r *jobResolver) MetaData(ctx context.Context, obj *schema.Job) (any, error) { func (r *jobResolver) MetaData(ctx context.Context, obj *schema.Job) (any, error) {
return r.Repo.FetchMetadata(obj) return r.Repo.FetchMetadata(obj)

View File

@ -9,6 +9,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"math"
"strconv" "strconv"
"sync" "sync"
"time" "time"
@ -52,7 +53,7 @@ func GetJobRepository() *JobRepository {
var jobColumns []string = []string{ var jobColumns []string = []string{
"job.id", "job.job_id", "job.user", "job.project", "job.cluster", "job.subcluster", "job.start_time", "job.partition", "job.array_job_id", "job.id", "job.job_id", "job.user", "job.project", "job.cluster", "job.subcluster", "job.start_time", "job.partition", "job.array_job_id",
"job.num_nodes", "job.num_hwthreads", "job.num_acc", "job.exclusive", "job.monitoring_status", "job.smt", "job.job_state", "job.num_nodes", "job.num_hwthreads", "job.num_acc", "job.exclusive", "job.monitoring_status", "job.smt", "job.job_state",
"job.duration", "job.walltime", "job.resources", "job.footprint", // "job.meta_data", "job.duration", "job.walltime", "job.resources", "job.footprint", "job.energy",
} }
func scanJob(row interface{ Scan(...interface{}) error }) (*schema.Job, error) { func scanJob(row interface{ Scan(...interface{}) error }) (*schema.Job, error) {
@ -61,7 +62,7 @@ func scanJob(row interface{ Scan(...interface{}) error }) (*schema.Job, error) {
if err := row.Scan( if err := row.Scan(
&job.ID, &job.JobID, &job.User, &job.Project, &job.Cluster, &job.SubCluster, &job.StartTimeUnix, &job.Partition, &job.ArrayJobId, &job.ID, &job.JobID, &job.User, &job.Project, &job.Cluster, &job.SubCluster, &job.StartTimeUnix, &job.Partition, &job.ArrayJobId,
&job.NumNodes, &job.NumHWThreads, &job.NumAcc, &job.Exclusive, &job.MonitoringStatus, &job.SMT, &job.State, &job.NumNodes, &job.NumHWThreads, &job.NumAcc, &job.Exclusive, &job.MonitoringStatus, &job.SMT, &job.State,
&job.Duration, &job.Walltime, &job.RawResources, &job.RawFootprint /*&job.RawMetaData*/); err != nil { &job.Duration, &job.Walltime, &job.RawResources, &job.RawFootprint, &job.Energy); err != nil {
log.Warnf("Error while scanning rows (Job): %v", err) log.Warnf("Error while scanning rows (Job): %v", err)
return nil, err return nil, err
} }
@ -245,6 +246,34 @@ func (r *JobRepository) FetchFootprint(job *schema.Job) (map[string]float64, err
return job.Footprint, nil return job.Footprint, nil
} }
func (r *JobRepository) FetchEnergyFootprint(job *schema.Job) (map[string]float64, error) {
start := time.Now()
cachekey := fmt.Sprintf("energyFootprint:%d", job.ID)
if cached := r.cache.Get(cachekey, nil); cached != nil {
job.EnergyFootprint = cached.(map[string]float64)
return job.EnergyFootprint, nil
}
if err := sq.Select("job.energy_footprint").From("job").Where("job.id = ?", job.ID).
RunWith(r.stmtCache).QueryRow().Scan(&job.RawEnergyFootprint); err != nil {
log.Warn("Error while scanning for job energy_footprint")
return nil, err
}
if len(job.RawEnergyFootprint) == 0 {
return nil, nil
}
if err := json.Unmarshal(job.RawEnergyFootprint, &job.EnergyFootprint); err != nil {
log.Warn("Error while unmarshaling raw energy footprint json")
return nil, err
}
r.cache.Put(cachekey, job.EnergyFootprint, len(job.EnergyFootprint), 24*time.Hour)
log.Debugf("Timer FetchEnergyFootprint %s", time.Since(start))
return job.EnergyFootprint, nil
}
func (r *JobRepository) DeleteJobsBefore(startTime int64) (int, error) { func (r *JobRepository) DeleteJobsBefore(startTime int64) (int, error) {
var cnt int var cnt int
q := sq.Select("count(*)").From("job").Where("job.start_time < ?", startTime) q := sq.Select("count(*)").From("job").Where("job.start_time < ?", startTime)
@ -562,6 +591,7 @@ func (r *JobRepository) UpdateEnergy(
stmt sq.UpdateBuilder, stmt sq.UpdateBuilder,
jobMeta *schema.JobMeta, jobMeta *schema.JobMeta,
) (sq.UpdateBuilder, error) { ) (sq.UpdateBuilder, error) {
/* Note: Only Called for Running Jobs during Intermediate Update or on Archiving */
sc, err := archive.GetSubCluster(jobMeta.Cluster, jobMeta.SubCluster) sc, err := archive.GetSubCluster(jobMeta.Cluster, jobMeta.SubCluster)
if err != nil { if err != nil {
log.Errorf("cannot get subcluster: %s", err.Error()) log.Errorf("cannot get subcluster: %s", err.Error())
@ -572,13 +602,17 @@ func (r *JobRepository) UpdateEnergy(
var energy float64 var energy float64
for _, fp := range sc.EnergyFootprint { for _, fp := range sc.EnergyFootprint {
if i, err := archive.MetricIndex(sc.MetricConfig, fp); err != nil { if i, err := archive.MetricIndex(sc.MetricConfig, fp); err == nil {
// FIXME: Check for unit conversions // FIXME: Check for unit conversions
// Energy: Watts * Time // Power: Energy / Time -> Correct labelling here?
if sc.MetricConfig[i].Energy == "power" { if sc.MetricConfig[i].Energy == "power" {
energy = LoadJobStat(jobMeta, fp, "avg") * float64(jobMeta.Duration) // Unit: ( W * s ) / 3600 = Wh ; Rounded to 2 nearest digits
energy = math.Round(((LoadJobStat(jobMeta, fp, "avg")*float64(jobMeta.Duration))/3600)*100) / 100
} else if sc.MetricConfig[i].Energy == "energy" { } else if sc.MetricConfig[i].Energy == "energy" {
// This assumes the metric is of aggregation type sum // This assumes the metric is of aggregation type sum
} }
} else {
log.Warnf("Error while collecting energy metric %s for job, DB ID '%v', return '0.0'", fp, jobMeta.ID)
} }
energyFootprint[fp] = energy energyFootprint[fp] = energy
@ -592,13 +626,14 @@ func (r *JobRepository) UpdateEnergy(
return stmt, err return stmt, err
} }
return stmt.Set("energy_footprint", rawFootprint).Set("energy", totalEnergy), nil return stmt.Set("energy_footprint", rawFootprint).Set("energy", (math.Round(totalEnergy*100) / 100)), nil
} }
func (r *JobRepository) UpdateFootprint( func (r *JobRepository) UpdateFootprint(
stmt sq.UpdateBuilder, stmt sq.UpdateBuilder,
jobMeta *schema.JobMeta, jobMeta *schema.JobMeta,
) (sq.UpdateBuilder, error) { ) (sq.UpdateBuilder, error) {
/* Note: Only Called for Running Jobs during Intermediate Update or on Archiving */
sc, err := archive.GetSubCluster(jobMeta.Cluster, jobMeta.SubCluster) sc, err := archive.GetSubCluster(jobMeta.Cluster, jobMeta.SubCluster)
if err != nil { if err != nil {
log.Errorf("cannot get subcluster: %s", err.Error()) log.Errorf("cannot get subcluster: %s", err.Error())

View File

@ -78,6 +78,9 @@ func setupHomeRoute(i InfoType, r *http.Request) InfoType {
func setupJobRoute(i InfoType, r *http.Request) InfoType { func setupJobRoute(i InfoType, r *http.Request) InfoType {
i["id"] = mux.Vars(r)["id"] i["id"] = mux.Vars(r)["id"]
if config.Keys.EmissionConstant != 0 {
i["emission"] = config.Keys.EmissionConstant
}
return i return i
} }

View File

@ -75,7 +75,6 @@ func initClusterConfig() error {
if !cfg.Remove { if !cfg.Remove {
availability.SubClusters = append(availability.SubClusters, sc.Name) availability.SubClusters = append(availability.SubClusters, sc.Name)
newMetric.Peak = cfg.Peak newMetric.Peak = cfg.Peak
newMetric.Peak = cfg.Peak
newMetric.Normal = cfg.Normal newMetric.Normal = cfg.Normal
newMetric.Caution = cfg.Caution newMetric.Caution = cfg.Caution
newMetric.Alert = cfg.Alert newMetric.Alert = cfg.Alert
@ -229,5 +228,5 @@ func MetricIndex(mc []schema.MetricConfig, name string) (int, error) {
} }
} }
return 0, fmt.Errorf("Unknown metric name %s", name) return 0, fmt.Errorf("unknown metric name %s", name)
} }

View File

@ -154,4 +154,8 @@ type ProgramConfig struct {
// Array of Clusters // Array of Clusters
Clusters []*ClusterConfig `json:"clusters"` Clusters []*ClusterConfig `json:"clusters"`
// Energy Mix CO2 Emission Constant [g/kWh]
// If entered, displays estimated CO2 emission for job based on jobs totalEnergy
EmissionConstant int `json:"emission-constant"`
} }

View File

@ -36,6 +36,7 @@
import Metric from "./job/Metric.svelte"; import Metric from "./job/Metric.svelte";
import StatsTable from "./job/StatsTable.svelte"; import StatsTable from "./job/StatsTable.svelte";
import JobSummary from "./job/JobSummary.svelte"; import JobSummary from "./job/JobSummary.svelte";
import EnergySummary from "./job/EnergySummary.svelte";
import ConcurrentJobs from "./generic/helper/ConcurrentJobs.svelte"; import ConcurrentJobs from "./generic/helper/ConcurrentJobs.svelte";
import PlotTable from "./generic/PlotTable.svelte"; import PlotTable from "./generic/PlotTable.svelte";
import Roofline from "./generic/plots/Roofline.svelte"; import Roofline from "./generic/plots/Roofline.svelte";
@ -70,7 +71,7 @@
const { query: initq } = init(` const { query: initq } = init(`
job(id: "${dbid}") { job(id: "${dbid}") {
id, jobId, user, project, cluster, startTime, id, jobId, user, project, cluster, startTime,
duration, numNodes, numHWThreads, numAcc, duration, numNodes, numHWThreads, numAcc, energy,
SMT, exclusive, partition, subCluster, arrayJobId, SMT, exclusive, partition, subCluster, arrayJobId,
monitoringStatus, state, walltime, monitoringStatus, state, walltime,
tags { id, type, scope, name }, tags { id, type, scope, name },
@ -78,7 +79,8 @@
metaData, metaData,
userData { name, email }, userData { name, email },
concurrentJobs { items { id, jobId }, count, listQuery }, concurrentJobs { items { id, jobId }, count, listQuery },
footprint { name, stat, value } footprint { name, stat, value },
energyFootprint { hardware, metric, value }
} }
`); `);
@ -308,6 +310,14 @@
</Col> </Col>
</Row> </Row>
{#if $initq?.data}
<Row class="mb-3">
<Col>
<EnergySummary job={$initq.data.job}/>
</Col>
</Row>
{/if}
<Card class="mb-3"> <Card class="mb-3">
<CardBody> <CardBody>
<Row class="mb-2"> <Row class="mb-2">

View File

@ -11,6 +11,7 @@ new Job({
}, },
context: new Map([ context: new Map([
['cc-config', clusterCockpitConfig], ['cc-config', clusterCockpitConfig],
['resampling', resampleConfig] ['resampling', resampleConfig],
['emission', emission]
]) ])
}) })

View File

@ -0,0 +1,75 @@
<!--
@component Energy Summary component; Displays job.footprint data as bars in relation to thresholds, as polar plot, and summariziong comment
Properties:
- `job Object`: The GQL job object
-->
<script>
import {
getContext
} from "svelte";
import {
Card,
CardBody,
Tooltip,
Row,
Col,
} from "@sveltestrap/sveltestrap";
import { round } from "mathjs";
export let job;
const carbonPerkWh = getContext("emission");
let carbonMass;
$: if (carbonPerkWh) {
// (( Wh / 1000 )* g/kWh) / 1000 = kg || Rounded to 2 Digits via [ round(x * 100) / 100 ]
carbonMass = round( (((job?.energy ? job.energy : 0.0) / 1000 ) * carbonPerkWh) / 10 ) / 100;
}
</script>
<Card>
<CardBody>
<Row>
{#each job.energyFootprint as efp}
<Col class="text-center" id={`energy-footprint-${job.jobId}-${efp.hardware}`}>
<div class="cursor-help mr-2"><b>{efp.hardware}:</b> {efp.value} Wh (<i>{efp.metric}</i>)</div>
</Col>
<Tooltip
target={`energy-footprint-${job.jobId}-${efp.hardware}`}
placement="top"
>Estimated energy consumption based on metric {efp.metric} and job runtime.
</Tooltip>
{/each}
<Col class="text-center" id={`energy-footprint-${job.jobId}-total`}>
<div class="cursor-help"><b>Total Energy:</b> {job?.energy? job.energy : 0} Wh</div>
</Col>
{#if carbonPerkWh}
<Col class="text-center" id={`energy-footprint-${job.jobId}-carbon`}>
<div class="cursor-help"><b>Carbon Emission:</b> {carbonMass} kg</div>
</Col>
{/if}
</Row>
</CardBody>
</Card>
<Tooltip
target={`energy-footprint-${job.jobId}-total`}
placement="top"
>Estimated total energy consumption of job.
</Tooltip>
{#if carbonPerkWh}
<Tooltip
target={`energy-footprint-${job.jobId}-carbon`}
placement="top"
>Estimated emission based on supplier energy mix and total energy consumption.
</Tooltip>
{/if}
<style>
.cursor-help {
cursor: help;
}
</style>