Skip to content

Commit

Permalink
calc: Restructure to account for all metric types
Browse files Browse the repository at this point in the history
  • Loading branch information
gabibeyer committed Jan 25, 2024
1 parent 1391cf2 commit 96c7dcd
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 91 deletions.
38 changes: 31 additions & 7 deletions pkg/calculator/calculator.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package calculator

import (
"errors"
"fmt"
"time"

v1 "github.com/re-cinq/cloud-carbon/pkg/types/v1"
)

// TODO add links / sources for where numbers and calculations are gathered
Expand All @@ -11,8 +15,6 @@ import (
const lifespan = 4

type calculate struct {
cores float64
usageCPU float64
minWatts float64
maxWatts float64
chip float64
Expand All @@ -21,26 +23,48 @@ type calculate struct {
totalEmbodied float64
}

// OperationalCPUEmissions are the emissions released from the machines the service is
// operationalEmissions determines the correct function to run to calculate the
// operational emissions for the metric type
func (c *calculate) operationalEmissions(metric *v1.Metric, interval time.Duration) (float64, error) {
switch metric.Name() {
case v1.CPU.String():
return c.cpu(metric, interval)
case v1.Memory.String():
return 0, errors.New("error memory is not yet being calculated")
case v1.Storage.String():
return 0, errors.New("error storage is not yet being calculated")
case v1.Network.String():
return 0, errors.New("error networking is not yet being calculated")
default:
return 0, fmt.Errorf("error metric not supported: %+v", metric)
}
}

// cpu are the emissions released from the machines the service is
// running on based on architecture and utilization.
func (c *calculate) operationalCPUEmissions(interval time.Duration) float64 {
func (c *calculate) cpu(m *v1.Metric, interval time.Duration) (float64, error) {
// Check that number of cores is set
if m.UnitAmount() == 0 {
return 0, errors.New("error Cores set to 0, this should never be the case")
}

// vCPUHours is the amount of cores on the machine multiplied by the interval of time
// for 1 hour. For example, if the machine has 4 cores and the interval of time is
// 5 minutes: The hourly time is 5/60 (0.083333333) * 4 cores = 0.333333333.
//nolint:unconvert //conversion to minutes does affect calculation
vCPUHours := c.cores * (float64(interval.Minutes()) / float64(60))
vCPUHours := m.UnitAmount() * (float64(interval.Minutes()) / float64(60))

// Average Watts is the average energy consumption of the service. It is based on
// CPU utilization and Minimum and Maximum wattage of the server. If the machine
// architecture is unknown the Min and Max wattage is the average of all machines
// for that provider, and is supplied in the provider defaults. This is being
// handled in the types/factors package (the point of reading in coefficient data).
avgWatts := c.minWatts + c.usageCPU*(c.maxWatts-c.minWatts)
avgWatts := c.minWatts + m.Usage()*(c.maxWatts-c.minWatts)

// Operational Emissions are calculated by multiplying the avgWatts, vCPUHours, PUE,
// and region grid CO2e. The PUE is collected from the providers. The CO2e grid data
// is the electrical grid emissions for the region at the specified time.
return avgWatts * vCPUHours * c.pue * c.gridCO2e
return avgWatts * vCPUHours * c.pue * c.gridCO2e, nil
}

// EmbodiedEmissions are the released emissions of production and destruction of the
Expand Down
131 changes: 78 additions & 53 deletions pkg/calculator/calculator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,24 @@ import (
"testing"
"time"

v1 "github.com/re-cinq/cloud-carbon/pkg/types/v1"
"github.com/stretchr/testify/assert"
)

type testcase struct {
name string
interval time.Duration // this is nanoseconds
calc *calculate
expRes float64
name string
interval time.Duration // this is nanoseconds
calculate *calculate
metric *v1.Metric
expRes float64
hasErr bool
expErr string
}

// defaultCalc is contains basic/typical emissions
// data numbers that are used as the default for tests
func defaultCalc() *calculate {
return &calculate{
cores: 4,
usageCPU: 25.0,
minWatts: 1.3423402398570,
maxWatts: 4.00498247528,
chip: 35.23458732,
Expand All @@ -28,91 +30,108 @@ func defaultCalc() *calculate {
}
}

func TestCalculateEmissions(t *testing.T) {
func defaultMetric() *v1.Metric {
m := v1.NewMetric("basic")
m.SetType(v1.CPU).SetUsage(25)
m.SetUnitAmount(4).SetResourceUnit(v1.Core)
return m
}

func TestCalculateCPUEmissions(t *testing.T) {
for _, test := range []*testcase{
func() *testcase {
// Default test case
return &testcase{
name: "basic default numbers",
interval: 30 * time.Second,
calc: defaultCalc(),
expRes: 0.0005270347987162735,
name: "basic default numbers",
interval: 30 * time.Second,
calculate: defaultCalc(),
metric: defaultMetric(),
expRes: 0.0005270347987162735,
}
}(),
func() *testcase {
// All data set to zero values
return &testcase{
name: "no values 30 sec",
name: "no values in calculator",
interval: 30 * time.Second,
calc: &calculate{
cores: 0,
usageCPU: 0,
calculate: &calculate{
minWatts: 0,
maxWatts: 0,
chip: 0,
pue: 0,
gridCO2e: 0,
},
metric: defaultMetric(),
expRes: 0,
}
}(),
func() *testcase {
// cores not set
return &testcase{
name: "no cores set",
interval: 30 * time.Second,
calculate: defaultCalc(),
metric: defaultMetric().SetUnitAmount(0),
hasErr: true,
expErr: "error Cores set to 0, this should never be the case",
}
}(),

func() *testcase {
// Calculate the default values over
// a 5 minute interval, instead of
// 30 seconds
return &testcase{
name: "5 minutes interval",
interval: 5 * time.Minute,
calc: defaultCalc(),
expRes: 0.005270347987162734,
name: "5 minutes interval",
interval: 5 * time.Minute,
calculate: defaultCalc(),
metric: defaultMetric(),
expRes: 0.005270347987162734,
}
}(),

func() *testcase {
// Calculate the default values over
// one hour
return &testcase{
name: "1 hour interval",
interval: 1 * time.Hour,
calc: defaultCalc(),
expRes: 0.06324417584595282,
name: "1 hour interval",
interval: 1 * time.Hour,
calculate: defaultCalc(),
metric: defaultMetric(),
expRes: 0.06324417584595282,
}
}(),

func() *testcase {
c := defaultCalc()
c.cores = 1
// calculate with only a single core
return &testcase{
name: "single core",
interval: 30 * time.Second,
calc: c,
expRes: 0.00013175869967906837,
name: "single core",
interval: 30 * time.Second,
calculate: defaultCalc(),
metric: defaultMetric().SetUnitAmount(1),
expRes: 0.00013175869967906837,
}
}(),

func() *testcase {
c := defaultCalc()
c.usageCPU = 50
// test with vCPU utilization at 50%
return &testcase{
name: "50% utilization",
interval: 30 * time.Second,
calc: c,
expRes: 0.0010436517395756915,
name: "50% utilization",
interval: 30 * time.Second,
calculate: defaultCalc(),
metric: defaultMetric().SetUsage(50),
expRes: 0.0010436517395756915,
}
}(),

func() *testcase {
c := defaultCalc()
c.usageCPU = 100
// test with vCPU utilization at 100%
return &testcase{
name: "100% utilization",
interval: 30 * time.Second,
calc: c,
expRes: 0.002076885621294527,
name: "100% utilization",
interval: 30 * time.Second,
calculate: defaultCalc(),
metric: defaultMetric().SetUsage(100),
expRes: 0.002076885621294527,
}
}(),

Expand All @@ -121,10 +140,11 @@ func TestCalculateEmissions(t *testing.T) {
c.pue = 1.0
// test if PUE is exactly 1
return &testcase{
name: "PUE is exactly 1.0",
interval: 30 * time.Second,
calc: c,
expRes: 0.0005206310369616452,
name: "PUE is exactly 1.0",
interval: 30 * time.Second,
calculate: c,
metric: defaultMetric(),
expRes: 0.0005206310369616452,
}
}(),

Expand All @@ -135,10 +155,11 @@ func TestCalculateEmissions(t *testing.T) {
// This value was collected from azures
// Germany West Central region
return &testcase{
name: "High grid CO2e",
interval: 30 * time.Second,
calc: c,
expRes: 921.1651699301823,
name: "High grid CO2e",
interval: 30 * time.Second,
calculate: c,
metric: defaultMetric(),
expRes: 921.1651699301823,
}
}(),

Expand All @@ -149,22 +170,26 @@ func TestCalculateEmissions(t *testing.T) {
return &testcase{
name: "large server and large workload",
interval: 30 * time.Second,
calc: &calculate{
cores: 32,
usageCPU: 90,
calculate: &calculate{
minWatts: 3.0369270833333335,
maxWatts: 8.575357663690477,
chip: 129.77777777777777,
pue: 1.1,
gridCO2e: 0.00079,
},
metric: defaultMetric().SetUnitAmount(32).SetUsage(90),
expRes: 0.11621326542003971,
}
}(),
} {
t.Run(test.name, func(t *testing.T) {
res := test.calc.operationalCPUEmissions(test.interval)
res, err := test.calculate.cpu(test.metric, test.interval)
assert.Equalf(t, test.expRes, res, "Result should be: %v, got: %v", test.expRes, res)
if test.hasErr {
assert.EqualErrorf(t, err, test.expErr, "Error should be: %v, got: %v", test.expErr, err)
} else {
assert.Nil(t, err)
}
})
}
}
Expand Down
49 changes: 18 additions & 31 deletions pkg/calculator/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ func (ec *EmissionCalculator) Apply(event bus.Event) {
}

instance := metricsCollected.Instance

cfg := config.AppConfig().ProvidersConfig
interval := config.AppConfig().ProvidersConfig.Interval

emFactors, err := factors.GetProviderEmissionFactors(
instance.Provider(),
Expand All @@ -51,14 +50,6 @@ func (ec *EmissionCalculator) Apply(event bus.Event) {
return
}

// TODO having this as a map is making it complicated and dupliacting work
// we should use a slice and then use a switch case for different types
mCPU, ok := instance.Metrics()[v1.CPU.String()]
if !ok {
klog.Errorf("error instance metrics for CPU don't exist")
return
}

gridCO2e, ok := emFactors.Coefficient[instance.Region()]
if !ok {
klog.Errorf("error region: %s does not exist in factors for %s", instance.Region(), "gcp")
Expand All @@ -69,22 +60,29 @@ func (ec *EmissionCalculator) Apply(event bus.Event) {
minWatts: specs.MinWatts,
maxWatts: specs.MaxWatts,
totalEmbodied: specs.TotalEmbodiedKiloWattCO2e,
cores: mCPU.UnitAmount(),
usageCPU: mCPU.Usage(),
pue: emFactors.AveragePUE,
gridCO2e: gridCO2e,
}

mCPU.SetEmissions(
v1.NewResourceEmission(
c.operationalCPUEmissions(cfg.Interval),
v1.GCO2eqkWh,
),
)
// calculate and set the operational emissions for each
// metric type (CPU, Memory, Storage, and networking)
metrics := instance.Metrics()
for _, v := range metrics {
// reassign to avoid implicit memory aliasing
v := v
em, err := c.operationalEmissions(&v, interval)
if err != nil {
klog.Errorf("error calculating %s operational emissions: %+v", v.Name(), err)
continue
}
v.SetEmissions(v1.NewResourceEmission(em, v1.GCO2eqkWh))
// update the instance metrics
metrics.Upsert(&v)
}

instance.SetEmbodiedEmissions(
v1.NewResourceEmission(
c.embodiedEmissions(cfg.Interval),
c.embodiedEmissions(interval),
v1.GCO2eqkWh,
),
)
Expand All @@ -93,16 +91,5 @@ func (ec *EmissionCalculator) Apply(event bus.Event) {
Instance: instance,
})

instance.Metrics()[v1.CPU.String()] = mCPU

for _, metric := range instance.Metrics() {
klog.Infof(
"Collected metric: %s %s %s %s | %s",
instance.Service(),
instance.Region(),
instance.Name(),
instance.Kind(),
metric.String(),
)
}
instance.PrintPretty()
}
Loading

0 comments on commit 96c7dcd

Please sign in to comment.