Răsfoiți Sursa

Merge pull request #197 from kubecost/nikovacevic-agg-efficiency

Add efficiency scores to aggregate API
Niko Kovacevic 6 ani în urmă
părinte
comite
d808ffad20

+ 210 - 97
costmodel/aggregations.go

@@ -12,25 +12,32 @@ import (
 )
 
 type Aggregation struct {
-	Aggregator        string    `json:"aggregation"`
-	Subfields         []string  `json:"subfields"`
-	Environment       string    `json:"environment"`
-	Cluster           string    `json:"cluster,omitempty"`
-	CPUAllocation     []*Vector `json:"-"`
-	CPUCostVector     []*Vector `json:"cpuCostVector,omitempty"`
-	RAMAllocation     []*Vector `json:"-"`
-	RAMCostVector     []*Vector `json:"ramCostVector,omitempty"`
-	PVCostVector      []*Vector `json:"pvCostVector,omitempty"`
-	GPUAllocation     []*Vector `json:"-"`
-	GPUCostVector     []*Vector `json:"gpuCostVector,omitempty"`
-	NetworkCostVector []*Vector `json:"networkCostVector,omitempty"`
-	CPUCost           float64   `json:"cpuCost"`
-	RAMCost           float64   `json:"ramCost"`
-	GPUCost           float64   `json:"gpuCost"`
-	PVCost            float64   `json:"pvCost"`
-	NetworkCost       float64   `json:"networkCost"`
-	SharedCost        float64   `json:"sharedCost"`
-	TotalCost         float64   `json:"totalCost"`
+	Aggregator           string    `json:"aggregation"`
+	Subfields            []string  `json:"subfields,omitempty"`
+	Environment          string    `json:"environment"`
+	Cluster              string    `json:"cluster,omitempty"`
+	CPUAllocationVectors []*Vector `json:"-"`
+	CPUCost              float64   `json:"cpuCost"`
+	CPUCostVector        []*Vector `json:"cpuCostVector,omitempty"`
+	CPUEfficiency        float64   `json:"cpuEfficiency"`
+	CPURequestedVectors  []*Vector `json:"-"`
+	CPUUsedVectors       []*Vector `json:"-"`
+	Efficiency           float64   `json:"efficiency"`
+	GPUAllocation        []*Vector `json:"-"`
+	GPUCost              float64   `json:"gpuCost"`
+	GPUCostVector        []*Vector `json:"gpuCostVector,omitempty"`
+	RAMAllocationVectors []*Vector `json:"-"`
+	RAMCost              float64   `json:"ramCost"`
+	RAMCostVector        []*Vector `json:"ramCostVector,omitempty"`
+	RAMEfficiency        float64   `json:"ramEfficiency"`
+	RAMRequestedVectors  []*Vector `json:"-"`
+	RAMUsedVectors       []*Vector `json:"-"`
+	PVCost               float64   `json:"pvCost"`
+	PVCostVector         []*Vector `json:"pvCostVector,omitempty"`
+	NetworkCost          float64   `json:"networkCost"`
+	NetworkCostVector    []*Vector `json:"networkCostVector,omitempty"`
+	SharedCost           float64   `json:"sharedCost"`
+	TotalCost            float64   `json:"totalCost"`
 }
 
 const (
@@ -82,9 +89,8 @@ func ComputeIdleCoefficient(costData map[string]*CostData, cli prometheusClient.
 	if err != nil {
 		return nil, err
 	}
-	tempCoefficient := make(map[string]float64)
 
-	aggregateContainerCosts := AggregateCostData(cp, costData, 0, "cluster", []string{}, "", false, discount, tempCoefficient, nil)
+	aggregateContainerCosts := AggregateCostData(costData, "cluster", []string{}, cp, &AggregationOptions{Discount: discount})
 	allTotals, err := ClusterCostsForAllClusters(cli, cp, windowString, offset)
 	if err != nil {
 		return nil, err
@@ -111,11 +117,11 @@ func ComputeIdleCoefficient(costData map[string]*CostData, cli prometheusClient.
 		totalContainerCost := 0.0
 		for _, costDatum := range costData {
 			cpuv, ramv, gpuv, pvvs, _ := getPriceVectors(cp, costDatum, "", discount, 1)
-			totalContainerCost += totalVector(cpuv)
-			totalContainerCost += totalVector(ramv)
-			totalContainerCost += totalVector(gpuv)
+			totalContainerCost += totalVectors(cpuv)
+			totalContainerCost += totalVectors(ramv)
+			totalContainerCost += totalVectors(gpuv)
 			for _, pv := range pvvs {
-				totalContainerCost += totalVector(pv)
+				totalContainerCost += totalVectors(pv)
 			}
 		}
 
@@ -125,9 +131,35 @@ func ComputeIdleCoefficient(costData map[string]*CostData, cli prometheusClient.
 	return coefficients, nil
 }
 
-// AggregateCostData reduces the dimensions of raw cost data by field and, optionally, by time. The field parameter determines the field
-// by which to group data, with an optional subfield, e.g. for groupings like field="label" and subfield="app" for grouping by "label.app".
-func AggregateCostData(cp cloud.Provider, costData map[string]*CostData, dataCount int64, field string, subfields []string, rate string, timeSeries bool, discount float64, idleCoefficients map[string]float64, sr *SharedResourceInfo) map[string]*Aggregation {
+// AggregationOptions provides optional parameters to AggregateCostData, allowing callers to perform more complex operations
+type AggregationOptions struct {
+	DataCount          int64              // number of cost data points expected; ensures proper rate calculation if data is incomplete
+	Discount           float64            // percent by which to discount CPU, RAM, and GPU cost
+	IdleCoefficients   map[string]float64 // scales costs by amount of idle resources on a per-cluster basis
+	IncludeEfficiency  bool               // set to true to receive efficiency/usage data
+	IncludeTimeSeries  bool               // set to true to receive time series data
+	Rate               string             // set to "hourly", "daily", or "monthly" to receive cost rate, rather than cumulative cost
+	SharedResourceInfo *SharedResourceInfo
+}
+
+// AggregateCostData aggregates raw cost data by field; e.g. namespace, cluster, service, or label. In the case of label, callers
+// must pass a slice of subfields indicating the labels by which to group. Provider is used to define custom resource pricing.
+// See AggregationOptions for optional parameters.
+// TODO: Can we restructure custom pricing code to allow that to be optional? Having to pass an entire Provider instance is way
+// overkill and tightly couples this code to the cloud package.
+func AggregateCostData(costData map[string]*CostData, field string, subfields []string, cp cloud.Provider, opts *AggregationOptions) map[string]*Aggregation {
+	dataCount := opts.DataCount
+	discount := opts.Discount
+	idleCoefficients := opts.IdleCoefficients
+	includeTimeSeries := opts.IncludeTimeSeries
+	includeEfficiency := opts.IncludeEfficiency
+	rate := opts.Rate
+	sr := opts.SharedResourceInfo
+
+	if idleCoefficients == nil {
+		idleCoefficients = make(map[string]float64)
+	}
+
 	// aggregations collects key-value pairs of resource group-to-aggregated data
 	// e.g. namespace-to-data or label-value-to-data
 	aggregations := make(map[string]*Aggregation)
@@ -143,12 +175,12 @@ func AggregateCostData(cp cloud.Provider, costData map[string]*CostData, dataCou
 		}
 		if sr != nil && sr.ShareResources && sr.IsSharedResource(costDatum) {
 			cpuv, ramv, gpuv, pvvs, netv := getPriceVectors(cp, costDatum, rate, discount, idleCoefficient)
-			sharedResourceCost += totalVector(cpuv)
-			sharedResourceCost += totalVector(ramv)
-			sharedResourceCost += totalVector(gpuv)
-			sharedResourceCost += totalVector(netv)
+			sharedResourceCost += totalVectors(cpuv)
+			sharedResourceCost += totalVectors(ramv)
+			sharedResourceCost += totalVectors(gpuv)
+			sharedResourceCost += totalVectors(netv)
 			for _, pv := range pvvs {
-				sharedResourceCost += totalVector(pv)
+				sharedResourceCost += totalVectors(pv)
 			}
 		} else {
 			if field == "cluster" {
@@ -183,30 +215,75 @@ func AggregateCostData(cp cloud.Provider, costData map[string]*CostData, dataCou
 	}
 
 	for _, agg := range aggregations {
-		agg.CPUCost = totalVector(agg.CPUCostVector)
-		agg.RAMCost = totalVector(agg.RAMCostVector)
-		agg.GPUCost = totalVector(agg.GPUCostVector)
-		agg.PVCost = totalVector(agg.PVCostVector)
-		agg.NetworkCost = totalVector(agg.NetworkCostVector)
+		agg.CPUCost = totalVectors(agg.CPUCostVector)
+		agg.RAMCost = totalVectors(agg.RAMCostVector)
+		agg.GPUCost = totalVectors(agg.GPUCostVector)
+		agg.PVCost = totalVectors(agg.PVCostVector)
+		agg.NetworkCost = totalVectors(agg.NetworkCostVector)
 		agg.SharedCost = sharedResourceCost / float64(len(aggregations))
 
-		if rate != "" {
-			klog.V(1).Infof("scaling '%s' costs to '%s' rate by %d", agg.Environment, rate, dataCount)
-
-			if dataCount > 0 {
-				agg.CPUCost /= float64(dataCount)
-				agg.RAMCost /= float64(dataCount)
-				agg.GPUCost /= float64(dataCount)
-				agg.PVCost /= float64(dataCount)
-				agg.NetworkCost /= float64(dataCount)
-				agg.SharedCost /= float64(dataCount)
-			}
+		if rate != "" && dataCount > 0 {
+			agg.CPUCost /= float64(dataCount)
+			agg.RAMCost /= float64(dataCount)
+			agg.GPUCost /= float64(dataCount)
+			agg.PVCost /= float64(dataCount)
+			agg.NetworkCost /= float64(dataCount)
+			agg.SharedCost /= float64(dataCount)
 		}
 
 		agg.TotalCost = agg.CPUCost + agg.RAMCost + agg.GPUCost + agg.PVCost + agg.NetworkCost + agg.SharedCost
 
+		if includeEfficiency {
+			// Default both RAM and CPU to 100% efficiency so that a 0-requested, 0-allocated, 0-used situation
+			// returns 100% efficiency, which should be a red-flag.
+			//
+			// If non-zero numbers are available, then efficiency is defined as:
+			//   idlePercentage =  (requested - used) / allocated
+			//   efficiency = (1.0 - idlePercentage)
+			//
+			// It is possible to score > 100% efficiency, which is meant to be interpreted as a red flag.
+			// It is not possible to score < 0% efficiency.
+
+			klog.V(1).Infof("\n\tlen(CPU allocation): %d\n\tlen(CPU requested): %d\n\tlen(CPU used): %d",
+				len(agg.CPUAllocationVectors),
+				len(agg.CPURequestedVectors),
+				len(agg.CPUUsedVectors))
+
+			agg.CPUEfficiency = 1.0
+			CPUIdle := 0.0
+			avgCPUAllocation := totalVectors(agg.CPUAllocationVectors) / float64(len(agg.CPUAllocationVectors))
+			if avgCPUAllocation > 0.0 {
+				avgCPURequested := averageVectors(agg.CPURequestedVectors)
+				avgCPUUsed := averageVectors(agg.CPUUsedVectors)
+				CPUIdle = ((avgCPURequested - avgCPUUsed) / avgCPUAllocation)
+				agg.CPUEfficiency = 1.0 - CPUIdle
+			}
+
+			klog.V(1).Infof("\n\tlen(RAM allocation): %d\n\tlen(RAM requested): %d\n\tlen(RAM used): %d",
+				len(agg.RAMAllocationVectors),
+				len(agg.RAMRequestedVectors),
+				len(agg.RAMUsedVectors))
+
+			agg.RAMEfficiency = 1.0
+			RAMIdle := 0.0
+			avgRAMAllocation := totalVectors(agg.RAMAllocationVectors) / float64(len(agg.RAMAllocationVectors))
+			if avgRAMAllocation > 0.0 {
+				avgRAMRequested := averageVectors(agg.RAMRequestedVectors)
+				avgRAMUsed := averageVectors(agg.RAMUsedVectors)
+				RAMIdle = ((avgRAMRequested - avgRAMUsed) / avgRAMAllocation)
+				agg.RAMEfficiency = 1.0 - RAMIdle
+			}
+
+			// Score total efficiency by the sum of CPU and RAM efficiency, weighted by their
+			// respective total costs.
+			agg.Efficiency = 1.0
+			if (agg.CPUCost + agg.RAMCost) > 0 {
+				agg.Efficiency = 1.0 - ((agg.CPUCost*CPUIdle)+(agg.RAMCost*RAMIdle))/(agg.CPUCost+agg.RAMCost)
+			}
+		}
+
 		// remove time series data if it is not explicitly requested
-		if !timeSeries {
+		if !includeTimeSeries {
 			agg.CPUCostVector = nil
 			agg.RAMCostVector = nil
 			agg.GPUCostVector = nil
@@ -219,21 +296,31 @@ func AggregateCostData(cp cloud.Provider, costData map[string]*CostData, dataCou
 }
 
 func aggregateDatum(cp cloud.Provider, aggregations map[string]*Aggregation, costDatum *CostData, field string, subfields []string, rate string, key string, discount float64, idleCoefficient float64) {
-	// add new entry to aggregation results if a new
+	// add new entry to aggregation results if a new key is encountered
 	if _, ok := aggregations[key]; !ok {
 		agg := &Aggregation{}
 		agg.Aggregator = field
-		agg.Subfields = subfields
+		if len(subfields) > 0 {
+			agg.Subfields = subfields
+		}
 		agg.Environment = key
 		aggregations[key] = agg
 	}
 
+	klog.V(1).Infoln(costDatum)
+
 	mergeVectors(cp, costDatum, aggregations[key], rate, discount, idleCoefficient)
 }
 
 func mergeVectors(cp cloud.Provider, costDatum *CostData, aggregation *Aggregation, rate string, discount float64, idleCoefficient float64) {
-	aggregation.CPUAllocation = addVectors(costDatum.CPUAllocation, aggregation.CPUAllocation)
-	aggregation.RAMAllocation = addVectors(costDatum.RAMAllocation, aggregation.RAMAllocation)
+	aggregation.CPUAllocationVectors = addVectors(costDatum.CPUAllocation, aggregation.CPUAllocationVectors)
+	aggregation.CPURequestedVectors = addVectors(costDatum.CPUReq, aggregation.CPURequestedVectors)
+	aggregation.CPUUsedVectors = addVectors(costDatum.CPUUsed, aggregation.CPUUsedVectors)
+
+	aggregation.RAMAllocationVectors = addVectors(costDatum.RAMAllocation, aggregation.RAMAllocationVectors)
+	aggregation.RAMRequestedVectors = addVectors(costDatum.RAMReq, aggregation.RAMRequestedVectors)
+	aggregation.RAMUsedVectors = addVectors(costDatum.RAMUsed, aggregation.RAMUsedVectors)
+
 	aggregation.GPUAllocation = addVectors(costDatum.GPUReq, aggregation.GPUAllocation)
 
 	cpuv, ramv, gpuv, pvvs, netv := getPriceVectors(cp, costDatum, rate, discount, idleCoefficient)
@@ -339,7 +426,14 @@ func getPriceVectors(cp cloud.Provider, costDatum *CostData, rate string, discou
 	return cpuv, ramv, gpuv, pvvs, netv
 }
 
-func totalVector(vectors []*Vector) float64 {
+func averageVectors(vectors []*Vector) float64 {
+	if len(vectors) == 0 {
+		return 0.0
+	}
+	return totalVectors(vectors) / float64(len(vectors))
+}
+
+func totalVectors(vectors []*Vector) float64 {
 	total := 0.0
 	for _, vector := range vectors {
 		total += vector.Value
@@ -347,65 +441,84 @@ func totalVector(vectors []*Vector) float64 {
 	return total
 }
 
-func addVectors(req []*Vector, used []*Vector) []*Vector {
-	if req == nil || len(req) == 0 {
-		for _, usedV := range used {
-			if usedV.Timestamp == 0 {
-				continue
-			}
-			usedV.Timestamp = math.Round(usedV.Timestamp/10) * 10
+// roundTimestamp rounds the given timestamp to the given precision; e.g. a
+// timestamp given in seconds, rounded to precision 10, will be rounded
+// to the nearest value dividible by 10 (24 goes to 20, but 25 goes to 30).
+func roundTimestamp(ts float64, precision float64) float64 {
+	return math.Round(ts/precision) * precision
+}
+
+// addVectors adds two slices of Vectors. Vector timestamps are rounded to the
+// nearest ten seconds to allow matching of Vectors within a delta allowance.
+// Matching Vectors are summed, while unmatched Vectors are passed through.
+// e.g. [(t=1, 1), (t=2, 2)] + [(t=2, 2), (t=3, 3)] = [(t=1, 1), (t=2, 4), (t=3, 3)]
+func addVectors(xvs []*Vector, yvs []*Vector) []*Vector {
+	// round all non-zero timestamps to the nearest 10 second mark
+	for _, yv := range yvs {
+		if yv.Timestamp != 0 {
+			yv.Timestamp = roundTimestamp(yv.Timestamp, 10.0)
 		}
-		return used
 	}
-	if used == nil || len(used) == 0 {
-		for _, reqV := range req {
-			if reqV.Timestamp == 0 {
-				continue
-			}
-			reqV.Timestamp = math.Round(reqV.Timestamp/10) * 10
+	for _, xv := range xvs {
+		if xv.Timestamp != 0 {
+			xv.Timestamp = roundTimestamp(xv.Timestamp, 10.0)
 		}
-		return req
 	}
-	var allocation []*Vector
 
+	// if xvs is empty, return yvs
+	if xvs == nil || len(xvs) == 0 {
+		return yvs
+	}
+
+	// if yvs is empty, return xvs
+	if yvs == nil || len(yvs) == 0 {
+		return xvs
+	}
+
+	// sum stores the sum of the vector slices xvs and yvs
+	var sum []*Vector
+
+	// timestamps stores all timestamps present in both vector slices
+	// without duplicates
 	var timestamps []float64
-	reqMap := make(map[float64]float64)
-	for _, reqV := range req {
-		if reqV.Timestamp == 0 {
+
+	// turn each vector slice into a map of timestamp-to-value so that
+	// values at equal timestamps can be lined-up and summed
+	xMap := make(map[float64]float64)
+	for _, xv := range xvs {
+		if xv.Timestamp == 0 {
 			continue
 		}
-		reqV.Timestamp = math.Round(reqV.Timestamp/10) * 10
-		reqMap[reqV.Timestamp] = reqV.Value
-		timestamps = append(timestamps, reqV.Timestamp)
+		xMap[xv.Timestamp] = xv.Value
+		timestamps = append(timestamps, xv.Timestamp)
 	}
-	usedMap := make(map[float64]float64)
-	for _, usedV := range used {
-		if usedV.Timestamp == 0 {
+	yMap := make(map[float64]float64)
+	for _, yv := range yvs {
+		if yv.Timestamp == 0 {
 			continue
 		}
-		usedV.Timestamp = math.Round(usedV.Timestamp/10) * 10
-		usedMap[usedV.Timestamp] = usedV.Value
-		if _, ok := reqMap[usedV.Timestamp]; !ok { // no need to double add, since we'll range over sorted timestamps and check.
-			timestamps = append(timestamps, usedV.Timestamp)
+		yMap[yv.Timestamp] = yv.Value
+		if _, ok := xMap[yv.Timestamp]; !ok {
+			// no need to double add, since we'll range over sorted timestamps and check.
+			timestamps = append(timestamps, yv.Timestamp)
 		}
 	}
 
+	// iterate over each timestamp to produce a final summed vector slice
 	sort.Float64s(timestamps)
 	for _, t := range timestamps {
-		rv, okR := reqMap[t]
-		uv, okU := usedMap[t]
-		allocationVector := &Vector{
-			Timestamp: t,
-		}
-		if okR && okU {
-			allocationVector.Value = rv + uv
-		} else if okR {
-			allocationVector.Value = rv
-		} else if okU {
-			allocationVector.Value = uv
+		x, okX := xMap[t]
+		y, okY := yMap[t]
+		sv := &Vector{Timestamp: t}
+		if okX && okY {
+			sv.Value = x + y
+		} else if okX {
+			sv.Value = x
+		} else if okY {
+			sv.Value = y
 		}
-		allocation = append(allocation, allocationVector)
+		sum = append(sum, sv)
 	}
 
-	return allocation
+	return sum
 }

+ 9 - 2
costmodel/costmodel.go

@@ -74,9 +74,9 @@ type CostData struct {
 	Jobs            []string                     `json:"jobs,omitempty"`
 	RAMReq          []*Vector                    `json:"ramreq,omitempty"`
 	RAMUsed         []*Vector                    `json:"ramused,omitempty"`
+	RAMAllocation   []*Vector                    `json:"ramallocated,omitempty"`
 	CPUReq          []*Vector                    `json:"cpureq,omitempty"`
 	CPUUsed         []*Vector                    `json:"cpuused,omitempty"`
-	RAMAllocation   []*Vector                    `json:"ramallocated,omitempty"`
 	CPUAllocation   []*Vector                    `json:"cpuallocated,omitempty"`
 	GPUReq          []*Vector                    `json:"gpureq,omitempty"`
 	PVCData         []*PersistentVolumeClaimData `json:"pvcData,omitempty"`
@@ -86,6 +86,13 @@ type CostData struct {
 	ClusterID       string                       `json:"clusterId"`
 }
 
+func (cd *CostData) String() string {
+	return fmt.Sprintf("\n\tName: %s; PodName: %s, NodeName: %s\n\tNamespace: %s\n\tDeployments: %s\n\tServices: %s\n\tCPU (req, used, alloc): %d, %d, %d\n\tRAM (req, used, alloc): %d, %d, %d",
+		cd.Name, cd.PodName, cd.NodeName, cd.Namespace, strings.Join(cd.Deployments, ", "), strings.Join(cd.Services, ", "),
+		len(cd.CPUReq), len(cd.CPUUsed), len(cd.CPUAllocation),
+		len(cd.RAMReq), len(cd.RAMUsed), len(cd.RAMAllocation))
+}
+
 type Vector struct {
 	Timestamp float64 `json:"timestamp"`
 	Value     float64 `json:"value"`
@@ -1903,7 +1910,7 @@ func Query(cli prometheusClient.Client, query string) (interface{}, error) {
 			return nil, fmt.Errorf("Error %s fetching query %s", err.Error(), query)
 		}
 
-		return nil, fmt.Errorf("%s Error %s fetching query %s", resp.StatusCode, err.Error(), query)
+		return nil, fmt.Errorf("%d Error %s fetching query %s", resp.StatusCode, err.Error(), query)
 	}
 	var toReturn interface{}
 	err = json.Unmarshal(body, &toReturn)

+ 37 - 11
costmodel/router.go

@@ -211,8 +211,13 @@ func (a *Accesses) CostDataModel(w http.ResponseWriter, r *http.Request, ps http
 		// This assumes hourly data, incremented by one to capture the 0th data point.
 		dataCount := int64(dur.Hours()) + 1
 		klog.V(1).Infof("for duration %s dataCount = %d", dur.String(), dataCount)
-		defaultIdleCoefficientMap := make(map[string]float64)
-		agg := AggregateCostData(a.Cloud, data, dataCount, aggregationField, subfields, "", false, discount, defaultIdleCoefficientMap, nil)
+
+		opts := &AggregationOptions{
+			DataCount:        dataCount,
+			Discount:         discount,
+			IdleCoefficients: make(map[string]float64),
+		}
+		agg := AggregateCostData(data, aggregationField, subfields, a.Cloud, opts)
 		w.Write(wrapData(agg, nil))
 	} else {
 		if fields != "" {
@@ -260,7 +265,7 @@ func (a *Accesses) AggregateCostModel(w http.ResponseWriter, r *http.Request, ps
 	namespace := r.URL.Query().Get("namespace")
 	cluster := r.URL.Query().Get("cluster")
 	field := r.URL.Query().Get("aggregation")
-	subfields := strings.Split(r.URL.Query().Get("aggregationSubfield"), ",")
+	subfieldStr := r.URL.Query().Get("aggregationSubfield")
 	rate := r.URL.Query().Get("rate")
 	allocateIdle := r.URL.Query().Get("allocateIdle") == "true"
 	sharedNamespaces := r.URL.Query().Get("sharedNamespaces")
@@ -268,9 +273,17 @@ func (a *Accesses) AggregateCostModel(w http.ResponseWriter, r *http.Request, ps
 	sharedLabelValues := r.URL.Query().Get("sharedLabelValues")
 	remote := r.URL.Query().Get("remote")
 
+	subfields := []string{}
+	if len(subfieldStr) > 0 {
+		subfields = strings.Split(r.URL.Query().Get("aggregationSubfield"), ",")
+	}
+
 	// timeSeries == true maintains the time series dimension of the data,
 	// which by default gets summed over the entire interval
-	timeSeries := r.URL.Query().Get("timeSeries") == "true"
+	includeTimeSeries := r.URL.Query().Get("timeSeries") == "true"
+
+	// efficiency == true aggregates and returns usage and efficiency data
+	includeEfficiency := r.URL.Query().Get("efficiency") == "true"
 
 	// disableCache, if set to "true", tells this function to recompute and
 	// cache the requested data
@@ -374,8 +387,8 @@ func (a *Accesses) AggregateCostModel(w http.ResponseWriter, r *http.Request, ps
 	}
 
 	// parametrize cache key by all request parameters
-	aggKey := fmt.Sprintf("aggregate:%s:%s:%s:%s:%s:%s:%s:%t:%t",
-		window, offset, namespace, cluster, field, strings.Join(subfields, ","), rate, timeSeries, allocateIdle)
+	aggKey := fmt.Sprintf("aggregate:%s:%s:%s:%s:%s:%s:%s:%t:%t:%t",
+		window, offset, namespace, cluster, field, strings.Join(subfields, ","), rate, allocateIdle, includeTimeSeries, includeEfficiency)
 
 	// check the cache for aggregated response; if cache is hit and not disabled, return response
 	if result, found := a.Cache.Get(aggKey); found && !disableCache {
@@ -417,13 +430,13 @@ func (a *Accesses) AggregateCostModel(w http.ResponseWriter, r *http.Request, ps
 	}
 	discount = discount * 0.01
 
-	idleCoefficient := make(map[string]float64)
+	idleCoefficients := make(map[string]float64)
 	if allocateIdle {
 		windowStr := fmt.Sprintf("%dh", int(dur.Hours()))
 		if a.ThanosClient != nil {
 			offset = "3h"
 		}
-		idleCoefficient, err = ComputeIdleCoefficient(data, pClient, a.Cloud, discount, windowStr, offset)
+		idleCoefficients, err = ComputeIdleCoefficient(data, pClient, a.Cloud, discount, windowStr, offset)
 		if err != nil {
 			klog.V(1).Infof("error computing idle coefficient: windowString=%s, offset=%s, err=%s", windowStr, offset, err)
 			w.Write(wrapData(nil, err))
@@ -451,7 +464,16 @@ func (a *Accesses) AggregateCostModel(w http.ResponseWriter, r *http.Request, ps
 	}
 
 	// aggregate cost model data by given fields and cache the result for the default expiration
-	result := AggregateCostData(a.Cloud, data, dataCount, field, subfields, rate, timeSeries, discount, idleCoefficient, sr)
+	opts := &AggregationOptions{
+		DataCount:          dataCount,
+		Discount:           discount,
+		IdleCoefficients:   idleCoefficients,
+		IncludeEfficiency:  includeEfficiency,
+		IncludeTimeSeries:  includeTimeSeries,
+		Rate:               rate,
+		SharedResourceInfo: sr,
+	}
+	result := AggregateCostData(data, field, subfields, a.Cloud, opts)
 	a.Cache.Set(aggKey, result, cache.DefaultExpiration)
 
 	w.Write(wrapDataWithMessage(result, nil, fmt.Sprintf("cache miss: %s", aggKey)))
@@ -527,8 +549,12 @@ func (a *Accesses) CostDataModelRange(w http.ResponseWriter, r *http.Request, ps
 		dataCount := (int64(dur.Hours()) / windowHrs) + 1
 		klog.V(1).Infof("for duration %s dataCount = %d", dur.String(), dataCount)
 
-		defaultIdleCoefficientMap := make(map[string]float64)
-		agg := AggregateCostData(a.Cloud, data, dataCount, aggregationField, subfields, "", false, discount, defaultIdleCoefficientMap, nil)
+		opts := &AggregationOptions{
+			DataCount:        dataCount,
+			Discount:         discount,
+			IdleCoefficients: make(map[string]float64),
+		}
+		agg := AggregateCostData(data, aggregationField, subfields, a.Cloud, opts)
 		w.Write(wrapData(agg, nil))
 	} else {
 		if fields != "" {

+ 1 - 4
test/aggregation_test.go

@@ -104,13 +104,10 @@ func TestAggregation(t *testing.T) {
 	costData["test1,foo,nginx,testnode"] = cd1
 	costData["test1,bar,nginx,testnode"] = cd2
 
-	dataCount := int64(1)
-
 	field := "namespace"
 	subfields := []string{""}
-	rate := ""
 
-	agg := costModel.AggregateCostData(cp, costData, dataCount, field, subfields, rate, false, 0.0, 1.0, nil)
+	agg := costModel.AggregateCostData(costData, field, subfields, cp, nil)
 	log.Printf("agg: %+v", agg["test1"])
 	assert.Equal(t, agg["test1"].TotalCost, 8.0)
 }

+ 9 - 5
test/historical_pod_test.go

@@ -243,8 +243,10 @@ func TestPodUpDown(t *testing.T) {
 		panic(err)
 	}
 
-	agg := costModel.AggregateCostData(provider, data, 1, "namespace", []string{""}, "", false, 0.0, 1.0, nil)
-	_, ok := agg["test2"]
+	agg := costModel.AggregateCostData(data, "namespace", []string{""}, provider, nil)
+	_, ok := agg["test"]
+	assert.Assert(t, ok)
+	_, ok = agg["test2"]
 	if !ok {
 		panic("No test2 namespace!")
 	}
@@ -254,13 +256,15 @@ func TestPodUpDown(t *testing.T) {
 		panic(err)
 	}
 
-	agg2 := costModel.AggregateCostData(provider, data2, 1, "namespace", []string{""}, "", false, 0.0, 1.0, nil)
-	_, ok2 := agg2["test2"]
+	agg2 := costModel.AggregateCostData(data2, "namespace", []string{""}, provider, nil)
+	_, ok2 := agg2["test"]
+	assert.Assert(t, ok2)
+	_, ok2 = agg2["test2"]
 	if !ok2 {
 		panic("No test2 namespace!")
 	}
 
-	agg3 := costModel.AggregateCostData(provider, data, 1, "label", []string{"testaggregation"}, "", false, 0.0, 1.0, nil)
+	agg3 := costModel.AggregateCostData(data, "label", []string{"testaggregation"}, provider, nil)
 	_, ok3 := agg3["foo"]
 	if !ok3 {
 		panic("No label foo aggregate!")

+ 2 - 2
test/remote_cluster_test.go

@@ -56,8 +56,8 @@ func TestClusterConvergence(t *testing.T) {
 		panic(err)
 	}
 
-	agg := costModel.AggregateCostData(provider, data, 1, "namespace", []string{""}, "", false, 0.0, 1.0, nil)
-	agg2 := costModel.AggregateCostData(provider, data2, 1, "namespace", []string{""}, "", false, 0.0, 1.0, nil)
+	agg := costModel.AggregateCostData(data, "namespace", []string{""}, provider, nil)
+	agg2 := costModel.AggregateCostData(data2, "namespace", []string{""}, provider, nil)
 
 	assert.Equal(t, agg["kubecost"].TotalCost, agg2["kubecost"].TotalCost)