Просмотр исходного кода

Merge pull request #1920 from ameijer/parcs-accum-support-II

Parcs accum support ii
Niko Kovacevic 3 лет назад
Родитель
Сommit
a2c8bdd5ff
3 измененных файлов с 507 добавлено и 132 удалено
  1. 23 0
      pkg/costmodel/aggregation.go
  2. 207 100
      pkg/kubecost/allocation.go
  3. 277 32
      pkg/kubecost/allocation_test.go

+ 23 - 0
pkg/costmodel/aggregation.go

@@ -2242,6 +2242,19 @@ func (a *Accesses) ComputeAllocationHandler(w http.ResponseWriter, r *http.Reque
 
 	// IncludeIdle, if true, uses Asset data to incorporate Idle Allocation
 	includeIdle := qp.GetBool("includeIdle", false)
+	// Accumulate is an optional parameter, defaulting to false, which if true
+	// sums each Set in the Range, producing one Set.
+	accumulate := qp.GetBool("accumulate", false)
+
+	// Accumulate is an optional parameter that accumulates an AllocationSetRange
+	// by the resolution of the given time duration.
+	// Defaults to 0. If a value is not passed then the parameter is not used.
+	accumulateBy := kubecost.AccumulateOption(qp.Get("accumulateBy", ""))
+
+	// if accumulateBy is not explicitly set, and accumulate is true, ensure result is accumulated
+	if accumulateBy == kubecost.AccumulateOptionNone && accumulate {
+		accumulateBy = kubecost.AccumulateOptionAll
+	}
 
 	// IdleByNode, if true, computes idle allocations at the node level.
 	// Otherwise it is computed at the cluster level. (Not relevant if idle
@@ -2265,6 +2278,16 @@ func (a *Accesses) ComputeAllocationHandler(w http.ResponseWriter, r *http.Reque
 		return
 	}
 
+	// Accumulate, if requested
+	if accumulateBy != kubecost.AccumulateOptionNone {
+		asr, err = asr.Accumulate(accumulateBy)
+		if err != nil {
+			log.Errorf("error accumulating by %v: %s", accumulateBy, err)
+			WriteError(w, InternalServerError(fmt.Errorf("error accumulating by %v: %s", accumulateBy, err).Error()))
+			return
+		}
+	}
+
 	w.Write(WrapData(asr, nil))
 }
 

+ 207 - 100
pkg/kubecost/allocation.go

@@ -254,6 +254,12 @@ type ProportionalAssetResourceCost struct {
 	GPUPercentage              float64 `json:"gpuPercentage"`
 	RAMPercentage              float64 `json:"ramPercentage"`
 	NodeResourceCostPercentage float64 `json:"nodeResourceCostPercentage"`
+	GPUTotalCost               float64 `json:"-"`
+	GPUProportionalCost        float64 `json:"-"`
+	CPUTotalCost               float64 `json:"-"`
+	CPUProportionalCost        float64 `json:"-"`
+	RAMTotalCost               float64 `json:"-"`
+	RAMProportionalCost        float64 `json:"-"`
 }
 
 func (parc ProportionalAssetResourceCost) Key(insertByNode bool) string {
@@ -267,26 +273,77 @@ func (parc ProportionalAssetResourceCost) Key(insertByNode bool) string {
 
 type ProportionalAssetResourceCosts map[string]ProportionalAssetResourceCost
 
+func (parcs ProportionalAssetResourceCosts) Clone() ProportionalAssetResourceCosts {
+	cloned := ProportionalAssetResourceCosts{}
+
+	for key, parc := range parcs {
+		cloned[key] = parc
+	}
+	return cloned
+}
+
 func (parcs ProportionalAssetResourceCosts) Insert(parc ProportionalAssetResourceCost, insertByNode bool) {
 	if !insertByNode {
 		parc.Node = ""
 		parc.ProviderID = ""
 	}
 	if curr, ok := parcs[parc.Key(insertByNode)]; ok {
-		parcs[parc.Key(insertByNode)] = ProportionalAssetResourceCost{
-			Node:                       curr.Node,
-			Cluster:                    curr.Cluster,
-			ProviderID:                 curr.ProviderID,
-			CPUPercentage:              curr.CPUPercentage + parc.CPUPercentage,
-			GPUPercentage:              curr.GPUPercentage + parc.GPUPercentage,
-			RAMPercentage:              curr.RAMPercentage + parc.RAMPercentage,
-			NodeResourceCostPercentage: curr.NodeResourceCostPercentage + parc.NodeResourceCostPercentage,
+
+		toInsert := ProportionalAssetResourceCost{
+			Node:                curr.Node,
+			Cluster:             curr.Cluster,
+			ProviderID:          curr.ProviderID,
+			CPUTotalCost:        curr.CPUTotalCost + parc.CPUTotalCost,
+			CPUProportionalCost: curr.CPUProportionalCost + parc.CPUProportionalCost,
+			RAMTotalCost:        curr.RAMTotalCost + parc.RAMTotalCost,
+			RAMProportionalCost: curr.RAMProportionalCost + parc.RAMProportionalCost,
+			GPUProportionalCost: curr.GPUProportionalCost + parc.GPUProportionalCost,
+			GPUTotalCost:        curr.GPUTotalCost + parc.GPUTotalCost,
 		}
+
+		computePercentages(&toInsert)
+		parcs[parc.Key(insertByNode)] = toInsert
 	} else {
+		computePercentages(&parc)
 		parcs[parc.Key(insertByNode)] = parc
 	}
 }
 
+func computePercentages(toInsert *ProportionalAssetResourceCost) {
+	// compute percentages
+	totalCost := toInsert.RAMTotalCost + toInsert.CPUTotalCost + toInsert.GPUTotalCost
+
+	if toInsert.CPUTotalCost > 0 {
+		toInsert.CPUPercentage = toInsert.CPUProportionalCost / toInsert.CPUTotalCost
+	}
+
+	if toInsert.GPUTotalCost > 0 {
+		toInsert.GPUPercentage = toInsert.GPUProportionalCost / toInsert.GPUTotalCost
+	}
+
+	if toInsert.RAMTotalCost > 0 {
+		toInsert.RAMPercentage = toInsert.RAMProportionalCost / toInsert.RAMTotalCost
+	}
+
+	ramFraction := toInsert.RAMTotalCost / totalCost
+	if ramFraction != ramFraction || ramFraction < 0 {
+		ramFraction = 0
+	}
+
+	cpuFraction := toInsert.CPUTotalCost / totalCost
+	if cpuFraction != cpuFraction || cpuFraction < 0 {
+		cpuFraction = 0
+	}
+
+	gpuFraction := toInsert.GPUTotalCost / totalCost
+	if gpuFraction != gpuFraction || gpuFraction < 0 {
+		gpuFraction = 0
+	}
+
+	toInsert.NodeResourceCostPercentage = (toInsert.RAMPercentage * ramFraction) +
+		(toInsert.CPUPercentage * cpuFraction) + (toInsert.GPUPercentage * gpuFraction)
+}
+
 func (parcs ProportionalAssetResourceCosts) Add(that ProportionalAssetResourceCosts) {
 
 	for _, parc := range that {
@@ -334,38 +391,39 @@ func (a *Allocation) Clone() *Allocation {
 	}
 
 	return &Allocation{
-		Name:                       a.Name,
-		Properties:                 a.Properties.Clone(),
-		Window:                     a.Window.Clone(),
-		Start:                      a.Start,
-		End:                        a.End,
-		CPUCoreHours:               a.CPUCoreHours,
-		CPUCoreRequestAverage:      a.CPUCoreRequestAverage,
-		CPUCoreUsageAverage:        a.CPUCoreUsageAverage,
-		CPUCost:                    a.CPUCost,
-		CPUCostAdjustment:          a.CPUCostAdjustment,
-		GPUHours:                   a.GPUHours,
-		GPUCost:                    a.GPUCost,
-		GPUCostAdjustment:          a.GPUCostAdjustment,
-		NetworkTransferBytes:       a.NetworkTransferBytes,
-		NetworkReceiveBytes:        a.NetworkReceiveBytes,
-		NetworkCost:                a.NetworkCost,
-		NetworkCrossZoneCost:       a.NetworkCrossZoneCost,
-		NetworkCrossRegionCost:     a.NetworkCrossRegionCost,
-		NetworkInternetCost:        a.NetworkInternetCost,
-		NetworkCostAdjustment:      a.NetworkCostAdjustment,
-		LoadBalancerCost:           a.LoadBalancerCost,
-		LoadBalancerCostAdjustment: a.LoadBalancerCostAdjustment,
-		PVs:                        a.PVs.Clone(),
-		PVCostAdjustment:           a.PVCostAdjustment,
-		RAMByteHours:               a.RAMByteHours,
-		RAMBytesRequestAverage:     a.RAMBytesRequestAverage,
-		RAMBytesUsageAverage:       a.RAMBytesUsageAverage,
-		RAMCost:                    a.RAMCost,
-		RAMCostAdjustment:          a.RAMCostAdjustment,
-		SharedCost:                 a.SharedCost,
-		ExternalCost:               a.ExternalCost,
-		RawAllocationOnly:          a.RawAllocationOnly.Clone(),
+		Name:                           a.Name,
+		Properties:                     a.Properties.Clone(),
+		Window:                         a.Window.Clone(),
+		Start:                          a.Start,
+		End:                            a.End,
+		CPUCoreHours:                   a.CPUCoreHours,
+		CPUCoreRequestAverage:          a.CPUCoreRequestAverage,
+		CPUCoreUsageAverage:            a.CPUCoreUsageAverage,
+		CPUCost:                        a.CPUCost,
+		CPUCostAdjustment:              a.CPUCostAdjustment,
+		GPUHours:                       a.GPUHours,
+		GPUCost:                        a.GPUCost,
+		GPUCostAdjustment:              a.GPUCostAdjustment,
+		NetworkTransferBytes:           a.NetworkTransferBytes,
+		NetworkReceiveBytes:            a.NetworkReceiveBytes,
+		NetworkCost:                    a.NetworkCost,
+		NetworkCrossZoneCost:           a.NetworkCrossZoneCost,
+		NetworkCrossRegionCost:         a.NetworkCrossRegionCost,
+		NetworkInternetCost:            a.NetworkInternetCost,
+		NetworkCostAdjustment:          a.NetworkCostAdjustment,
+		LoadBalancerCost:               a.LoadBalancerCost,
+		LoadBalancerCostAdjustment:     a.LoadBalancerCostAdjustment,
+		PVs:                            a.PVs.Clone(),
+		PVCostAdjustment:               a.PVCostAdjustment,
+		RAMByteHours:                   a.RAMByteHours,
+		RAMBytesRequestAverage:         a.RAMBytesRequestAverage,
+		RAMBytesUsageAverage:           a.RAMBytesUsageAverage,
+		RAMCost:                        a.RAMCost,
+		RAMCostAdjustment:              a.RAMCostAdjustment,
+		SharedCost:                     a.SharedCost,
+		ExternalCost:                   a.ExternalCost,
+		RawAllocationOnly:              a.RawAllocationOnly.Clone(),
+		ProportionalAssetResourceCosts: a.ProportionalAssetResourceCosts.Clone(),
 	}
 }
 
@@ -775,7 +833,14 @@ func (a *Allocation) add(that *Allocation) {
 
 	// If both Allocations have ProportionalAssetResourceCosts, then
 	// add those from the given Allocation into the receiver.
-	if a.ProportionalAssetResourceCosts != nil && that.ProportionalAssetResourceCosts != nil {
+	if a.ProportionalAssetResourceCosts != nil || that.ProportionalAssetResourceCosts != nil {
+		// init empty PARCs if either operand has nil PARCs
+		if a.ProportionalAssetResourceCosts == nil {
+			a.ProportionalAssetResourceCosts = ProportionalAssetResourceCosts{}
+		}
+		if that.ProportionalAssetResourceCosts == nil {
+			that.ProportionalAssetResourceCosts = ProportionalAssetResourceCosts{}
+		}
 		a.ProportionalAssetResourceCosts.Add(that.ProportionalAssetResourceCosts)
 	}
 
@@ -1144,34 +1209,13 @@ func (as *AllocationSet) AggregateBy(aggregateBy []string, options *AllocationAg
 		}
 	}
 
-	// (2b) If proportional asset resource costs are to be included, derive them
-	// from idle coefficients and add them to the allocations.
+	// (2b) If proportional asset resource costs are to be included, compute them
+	// and add them to the allocations.
 	if options.IncludeProportionalAssetResourceCosts {
-		var parcCoefficients map[string]map[string]map[string]float64
-		if parcSet.Length() > 0 {
-			parcCoefficients, allocatedTotalsMap, err = computeIdleCoeffs(options, as, shareSet)
-			if err != nil {
-				log.Warnf("AllocationSet.AggregateBy: compute parc idle coeff: %s", err)
-				return fmt.Errorf("error computing parc coefficients: %s", err)
-			}
-		}
-		if parcCoefficients == nil {
-			return fmt.Errorf("cannot include proportional resource costs because parc coefficients are nil")
-		}
-
-		for _, alloc := range as.Allocations {
-			// Create an empty set of proportional asset resource costs,
-			// regardless of whether or not we're successful in deriving them.
-			alloc.ProportionalAssetResourceCosts = ProportionalAssetResourceCosts{}
-
-			// Attempt to derive proportional asset resource costs from idle
-			// coefficients, and insert them into the set if successful.
-			parc, err := deriveProportionalAssetResourceCostsFromIdleCoefficients(parcCoefficients, allocatedTotalsMap, alloc, options)
-			if err != nil {
-				log.Debugf("AggregateBy: failed to derive proportional asset resource costs from idle coefficients for %s: %s", alloc.Name, err)
-				continue
-			}
-			alloc.ProportionalAssetResourceCosts.Insert(parc, options.IdleByNode)
+		err := deriveProportionalAssetResourceCosts(options, as, shareSet)
+		if err != nil {
+			log.Debugf("AggregateBy: failed to derive proportional asset resource costs from idle coefficients: %s", err)
+			return fmt.Errorf("AggregateBy: failed to derive proportional asset resource costs from idle coefficients: %s", err)
 		}
 	}
 
@@ -1741,48 +1785,111 @@ func computeIdleCoeffs(options *AllocationAggregationOptions, as *AllocationSet,
 	return coeffs, totals, nil
 }
 
-func deriveProportionalAssetResourceCostsFromIdleCoefficients(idleCoeffs map[string]map[string]map[string]float64, totals map[string]map[string]float64, allocation *Allocation, options *AllocationAggregationOptions) (ProportionalAssetResourceCost, error) {
-	idleId, err := allocation.getIdleId(options)
-	if err != nil {
-		return ProportionalAssetResourceCost{}, fmt.Errorf("failed to get idle ID for allocation %s", allocation.Name)
-	}
+func deriveProportionalAssetResourceCosts(options *AllocationAggregationOptions, as *AllocationSet, shareSet *AllocationSet) error {
 
-	if _, ok := idleCoeffs[idleId]; !ok {
-		return ProportionalAssetResourceCost{}, fmt.Errorf("failed to find idle coeffs for idle ID %s", idleId)
-	}
+	// Compute idle coefficients, then save them in AllocationAggregationOptions
+	// [idle_id][allocation name][resource] = [coeff]
+	coeffs := map[string]map[string]map[string]float64{}
 
-	if _, ok := idleCoeffs[idleId][allocation.Name]; !ok {
-		return ProportionalAssetResourceCost{}, fmt.Errorf("failed to find idle coeffs for allocation %s", allocation.Name)
+	// Compute totals per resource for CPU, GPU, RAM, and PV
+	// [idle_id][resource] = [total]
+	totals := map[string]map[string]float64{}
+
+	// Record allocation values first, then normalize by totals to get percentages
+	for _, alloc := range as.Allocations {
+		if alloc.IsIdle() {
+			// Skip idle allocations in coefficient calculation
+			continue
+		}
+
+		idleId, err := alloc.getIdleId(options)
+		if err != nil {
+			log.DedupedWarningf(3, "Missing Idle Key for %s", alloc.Name)
+		}
+
+		// get the name key for the allocation
+		name := alloc.Name
+
+		// Create key based tables if they don't exist
+		if _, ok := coeffs[idleId]; !ok {
+			coeffs[idleId] = map[string]map[string]float64{}
+		}
+		if _, ok := totals[idleId]; !ok {
+			totals[idleId] = map[string]float64{}
+		}
+
+		if _, ok := coeffs[idleId][name]; !ok {
+			coeffs[idleId][name] = map[string]float64{}
+		}
+
+		coeffs[idleId][name]["cpu"] += alloc.CPUTotalCost()
+		coeffs[idleId][name]["gpu"] += alloc.GPUTotalCost()
+		coeffs[idleId][name]["ram"] += alloc.RAMTotalCost()
+
+		totals[idleId]["cpu"] += alloc.CPUTotalCost()
+		totals[idleId]["gpu"] += alloc.GPUTotalCost()
+		totals[idleId]["ram"] += alloc.RAMTotalCost()
 	}
 
-	cpuPct := idleCoeffs[idleId][allocation.Name]["cpu"]
-	gpuPct := idleCoeffs[idleId][allocation.Name]["gpu"]
-	ramPct := idleCoeffs[idleId][allocation.Name]["ram"]
+	// Do the same for shared allocations
+	for _, alloc := range shareSet.Allocations {
+		if alloc.IsIdle() {
+			// Skip idle allocations in coefficient calculation
+			continue
+		}
+
+		// idleId will be providerId or cluster
+		idleId, err := alloc.getIdleId(options)
+		if err != nil {
+			log.DedupedWarningf(3, "Missing Idle Key in share set for %s", alloc.Name)
+		}
+
+		// get the name key for the allocation
+		name := alloc.Name
 
-	// compute how much each component (cpu, gpu, ram) contributes to the overall price
-	totalCost := totals[idleId]["ram"] + totals[idleId]["gpu"] + totals[idleId]["cpu"]
+		// Create idleId based tables if they don't exist
+		if _, ok := coeffs[idleId]; !ok {
+			coeffs[idleId] = map[string]map[string]float64{}
+		}
+		if _, ok := totals[idleId]; !ok {
+			totals[idleId] = map[string]float64{}
+		}
 
-	var ramFraction, cpuFraction, gpuFraction float64
+		if _, ok := coeffs[idleId][name]; !ok {
+			coeffs[idleId][name] = map[string]float64{}
+		}
 
-	// only compute fraction if totalCost is nonzero, otherwise returns in NaN
-	if totalCost > 0 {
-		ramFraction = totals[idleId]["ram"] / totalCost
-		cpuFraction = totals[idleId]["cpu"] / totalCost
-		gpuFraction = totals[idleId]["gpu"] / totalCost
+		coeffs[idleId][name]["cpu"] += alloc.CPUTotalCost()
+		coeffs[idleId][name]["gpu"] += alloc.GPUTotalCost()
+		coeffs[idleId][name]["ram"] += alloc.RAMTotalCost()
+
+		totals[idleId]["cpu"] += alloc.CPUTotalCost()
+		totals[idleId]["gpu"] += alloc.GPUTotalCost()
+		totals[idleId]["ram"] += alloc.RAMTotalCost()
 	}
 
-	// compute the resource usage percentage based on the weighted fractions
-	nodeResourceCostPercentage := (ramPct * ramFraction) + (cpuPct * cpuFraction) + (gpuPct * gpuFraction)
+	// after totals are computed, loop through and set parcs on allocations
+	for _, alloc := range as.Allocations {
+		idleId, err := alloc.getIdleId(options)
+		if err != nil {
+			log.DedupedWarningf(3, "Missing Idle Key in share set for %s", alloc.Name)
+		}
+
+		alloc.ProportionalAssetResourceCosts = ProportionalAssetResourceCosts{}
+		alloc.ProportionalAssetResourceCosts.Insert(ProportionalAssetResourceCost{
+			Cluster:             alloc.Properties.Cluster,
+			Node:                alloc.Properties.Node,
+			ProviderID:          alloc.Properties.ProviderID,
+			GPUTotalCost:        totals[idleId]["gpu"],
+			CPUTotalCost:        totals[idleId]["cpu"],
+			RAMTotalCost:        totals[idleId]["ram"],
+			GPUProportionalCost: coeffs[idleId][alloc.Name]["gpu"],
+			CPUProportionalCost: coeffs[idleId][alloc.Name]["cpu"],
+			RAMProportionalCost: coeffs[idleId][alloc.Name]["ram"],
+		}, options.IdleByNode)
+	}
 
-	return ProportionalAssetResourceCost{
-		Cluster:                    allocation.Properties.Cluster,
-		Node:                       allocation.Properties.Node,
-		ProviderID:                 allocation.Properties.ProviderID,
-		CPUPercentage:              cpuPct,
-		GPUPercentage:              gpuPct,
-		RAMPercentage:              ramPct,
-		NodeResourceCostPercentage: nodeResourceCostPercentage,
-	}, nil
+	return nil
 }
 
 // getIdleId returns the providerId or cluster of an Allocation depending on the IdleByNode

+ 277 - 32
pkg/kubecost/allocation_test.go

@@ -9,6 +9,7 @@ import (
 	"time"
 
 	"github.com/davecgh/go-spew/spew"
+	"github.com/opencost/opencost/pkg/log"
 	"github.com/opencost/opencost/pkg/util"
 	"github.com/opencost/opencost/pkg/util/json"
 )
@@ -517,6 +518,11 @@ func assertParcResults(t *testing.T, as *AllocationSet, msg string, exps map[str
 		for key, actualParc := range a.ProportionalAssetResourceCosts {
 			expectedParcs := exps[allocKey]
 
+			// round to prevent floating point issues from failing tests at ultra high precision
+			actualParc.NodeResourceCostPercentage = roundFloat(actualParc.NodeResourceCostPercentage)
+			actualParc.CPUPercentage = roundFloat(actualParc.CPUPercentage)
+			actualParc.RAMPercentage = roundFloat(actualParc.RAMPercentage)
+			actualParc.GPUPercentage = roundFloat(actualParc.GPUPercentage)
 			if !reflect.DeepEqual(expectedParcs[key], actualParc) {
 				t.Fatalf("actual PARC %v did not match expected PARC %v", actualParc, expectedParcs[key])
 			}
@@ -524,6 +530,10 @@ func assertParcResults(t *testing.T, as *AllocationSet, msg string, exps map[str
 
 	}
 }
+func roundFloat(val float64) float64 {
+	ratio := math.Pow(10, float64(5))
+	return math.Round(val*ratio) / ratio
+}
 
 func assertAllocationTotals(t *testing.T, as *AllocationSet, msg string, exps map[string]float64) {
 	for _, a := range as.Allocations {
@@ -1079,10 +1089,16 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 						Cluster:                    "cluster1",
 						Node:                       "",
 						ProviderID:                 "",
-						CPUPercentage:              0.5,
-						GPUPercentage:              0.5,
-						RAMPercentage:              0.8125,
-						NodeResourceCostPercentage: 0.6785714285714285,
+						CPUPercentage:              0.16667,
+						GPUPercentage:              0.16667,
+						RAMPercentage:              0.27083,
+						NodeResourceCostPercentage: 0.22619,
+						GPUTotalCost:               18,
+						GPUProportionalCost:        3,
+						CPUTotalCost:               18,
+						CPUProportionalCost:        3,
+						RAMTotalCost:               48,
+						RAMProportionalCost:        13,
 					},
 				},
 				"namespace2": {
@@ -1090,19 +1106,31 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 						Cluster:                    "cluster1",
 						Node:                       "",
 						ProviderID:                 "",
-						CPUPercentage:              0.5,
-						GPUPercentage:              0.5,
-						RAMPercentage:              0.1875,
-						NodeResourceCostPercentage: 0.3214285714285714,
+						CPUPercentage:              0.16667,
+						GPUPercentage:              0.16667,
+						RAMPercentage:              0.0625,
+						NodeResourceCostPercentage: 0.10714,
+						GPUTotalCost:               18,
+						GPUProportionalCost:        3,
+						CPUTotalCost:               18,
+						CPUProportionalCost:        3,
+						RAMTotalCost:               48,
+						RAMProportionalCost:        3,
 					},
 					"cluster2": ProportionalAssetResourceCost{
 						Cluster:                    "cluster2",
 						Node:                       "",
 						ProviderID:                 "",
-						CPUPercentage:              0.5,
-						GPUPercentage:              0.5,
-						RAMPercentage:              0.5,
-						NodeResourceCostPercentage: 0.5,
+						CPUPercentage:              0.16667,
+						GPUPercentage:              0.16667,
+						RAMPercentage:              0.16667,
+						NodeResourceCostPercentage: 0.16667,
+						GPUTotalCost:               18,
+						GPUProportionalCost:        3,
+						CPUTotalCost:               18,
+						CPUProportionalCost:        3,
+						RAMTotalCost:               18,
+						RAMProportionalCost:        3,
 					},
 				},
 			},
@@ -1517,19 +1545,31 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 						Cluster:                    "cluster1",
 						Node:                       "c1nodes",
 						ProviderID:                 "c1nodes",
-						CPUPercentage:              0.5,
-						GPUPercentage:              0.5,
-						RAMPercentage:              0.8125,
-						NodeResourceCostPercentage: 0.6785714285714285,
+						CPUPercentage:              0.16667,
+						GPUPercentage:              0.16667,
+						RAMPercentage:              0.27083,
+						NodeResourceCostPercentage: 0.22619,
+						GPUTotalCost:               18,
+						GPUProportionalCost:        3,
+						CPUTotalCost:               18,
+						CPUProportionalCost:        3,
+						RAMTotalCost:               48,
+						RAMProportionalCost:        13,
 					},
 					"cluster2,node2": ProportionalAssetResourceCost{
 						Cluster:                    "cluster2",
 						Node:                       "node2",
 						ProviderID:                 "node2",
-						CPUPercentage:              0.5,
-						GPUPercentage:              0.5,
-						RAMPercentage:              0.5,
-						NodeResourceCostPercentage: 0.5,
+						CPUPercentage:              0.16667,
+						GPUPercentage:              0.16667,
+						RAMPercentage:              0.0625,
+						NodeResourceCostPercentage: 0.10714,
+						GPUTotalCost:               18,
+						GPUProportionalCost:        3,
+						CPUTotalCost:               18,
+						CPUProportionalCost:        3,
+						RAMTotalCost:               48,
+						RAMProportionalCost:        3,
 					},
 				},
 				"namespace2": {
@@ -1537,19 +1577,31 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 						Cluster:                    "cluster1",
 						Node:                       "c1nodes",
 						ProviderID:                 "c1nodes",
-						CPUPercentage:              0.5,
-						GPUPercentage:              0.5,
-						RAMPercentage:              0.1875,
-						NodeResourceCostPercentage: 0.3214285714285714,
+						CPUPercentage:              0.16667,
+						GPUPercentage:              0.16667,
+						RAMPercentage:              0.0625,
+						NodeResourceCostPercentage: 0.10714,
+						GPUTotalCost:               18,
+						GPUProportionalCost:        3,
+						CPUTotalCost:               18,
+						CPUProportionalCost:        3,
+						RAMTotalCost:               48,
+						RAMProportionalCost:        3,
 					},
 					"cluster2,node1": ProportionalAssetResourceCost{
 						Cluster:                    "cluster2",
 						Node:                       "node1",
 						ProviderID:                 "node1",
-						CPUPercentage:              1,
-						GPUPercentage:              1,
-						RAMPercentage:              1,
-						NodeResourceCostPercentage: 1,
+						CPUPercentage:              0.5,
+						GPUPercentage:              0.5,
+						RAMPercentage:              0.5,
+						NodeResourceCostPercentage: 0.5,
+						GPUTotalCost:               4,
+						GPUProportionalCost:        2,
+						CPUTotalCost:               4,
+						CPUProportionalCost:        2,
+						RAMTotalCost:               4,
+						RAMProportionalCost:        2,
 					},
 					"cluster2,node2": ProportionalAssetResourceCost{
 						Cluster:                    "cluster2",
@@ -1559,6 +1611,12 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 						GPUPercentage:              0.5,
 						RAMPercentage:              0.5,
 						NodeResourceCostPercentage: 0.5,
+						GPUTotalCost:               2,
+						GPUProportionalCost:        1,
+						CPUTotalCost:               2,
+						CPUProportionalCost:        1,
+						RAMTotalCost:               2,
+						RAMProportionalCost:        1,
 					},
 				},
 				"namespace3": {
@@ -1566,10 +1624,16 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 						Cluster:                    "cluster2",
 						Node:                       "node3",
 						ProviderID:                 "node3",
-						CPUPercentage:              1,
-						GPUPercentage:              1,
-						RAMPercentage:              1,
-						NodeResourceCostPercentage: 1,
+						CPUPercentage:              0.5,
+						GPUPercentage:              0.5,
+						RAMPercentage:              0.5,
+						NodeResourceCostPercentage: 0.5,
+						GPUTotalCost:               4,
+						GPUProportionalCost:        2,
+						CPUTotalCost:               4,
+						CPUProportionalCost:        2,
+						RAMTotalCost:               4,
+						RAMProportionalCost:        2,
 					},
 					"cluster2,node2": ProportionalAssetResourceCost{
 						Cluster:                    "cluster2",
@@ -1579,6 +1643,12 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 						GPUPercentage:              0.5,
 						RAMPercentage:              0.5,
 						NodeResourceCostPercentage: 0.5,
+						GPUTotalCost:               2,
+						GPUProportionalCost:        1,
+						CPUTotalCost:               2,
+						CPUProportionalCost:        1,
+						RAMTotalCost:               2,
+						RAMProportionalCost:        1,
 					},
 				},
 			},
@@ -1708,6 +1778,181 @@ func TestAllocationSet_insertMatchingWindow(t *testing.T) {
 	}
 }
 
+// This tests PARC accumulation. Assuming Node cost is $1 per core per hour
+// From https://github.com/opencost/opencost/pull/1867#discussion_r1174109388:
+// Over the span of hour 1:
+
+//     Pod 1 runs for 30 minutes, consuming 1 CPU while alive. PARC: 12.5% (0.5 core-hours / 4 available core-hours)
+//     Pod 2 runs for 1 hour, consuming 2 CPU while alive. PARC: 50% (2 core-hours)
+//     Pod 3 runs for 1 hour, consuming 1 CPU while alive. PARC: 25% (1 core-hour)
+
+// Over the span of hour 2:
+
+//     Pod 1 does not run. PARC: 0% (0 core-hours / 4 available core-hours)
+//     Pod 2 runs for 30 minutes, consuming 2 CPU while active. PARC: 25% (1 core-hour)
+//     Pod 3 runs for 1 hour, consuming 1 CPU while active. PARC: 25% (1 core-hour)
+
+// Over the span of hour 3:
+
+//     Pod 1 does not run. PARC: 0% (0 core-hours / 4 available)
+//     Pod 2 runs for 30 minutes, consuming 3 CPU while active. PARC: 37.5% (1.5 core-hours)
+//     Pod 3 runs for 1 hour, consuming 1 CPU while active. PARC: 25% (1 core-hour)
+
+// We expect the following accumulated PARC:
+
+//     Pod 1: (0.5 + 0 + 0) core-hours used / (4 + 4 + 4) core-hours available = 0.5/12 = 4.16%
+//     Pod 2: (2 + 1 + 1.5) / (4 + 4 + 4) = 4.5/12 = 37.5%
+//     Pod 3: (1 + 1 + 1) / (4 + 4 + 4) = 3/12 = 25%
+
+func TestParcInsert(t *testing.T) {
+	pod1_hour1 := ProportionalAssetResourceCost{
+		Cluster:                    "cluster1",
+		Node:                       "node1",
+		ProviderID:                 "i-1234",
+		CPUPercentage:              0.125,
+		GPUPercentage:              0,
+		RAMPercentage:              0,
+		NodeResourceCostPercentage: 0,
+		CPUTotalCost:               4,
+		CPUProportionalCost:        0.5,
+	}
+
+	pod1_hour2 := ProportionalAssetResourceCost{
+		Cluster:                    "cluster1",
+		Node:                       "node1",
+		ProviderID:                 "i-1234",
+		CPUPercentage:              0.0,
+		GPUPercentage:              0,
+		RAMPercentage:              0,
+		NodeResourceCostPercentage: 0,
+		CPUTotalCost:               4,
+	}
+
+	pod1_hour3 := ProportionalAssetResourceCost{
+		Cluster:                    "cluster1",
+		Node:                       "node1",
+		ProviderID:                 "i-1234",
+		CPUPercentage:              0.0,
+		GPUPercentage:              0,
+		RAMPercentage:              0,
+		NodeResourceCostPercentage: 0,
+		CPUTotalCost:               4,
+	}
+
+	pod2_hour1 := ProportionalAssetResourceCost{
+		Cluster:                    "cluster1",
+		Node:                       "node2",
+		ProviderID:                 "i-1234",
+		CPUPercentage:              0.0,
+		GPUPercentage:              0,
+		RAMPercentage:              0,
+		NodeResourceCostPercentage: 0,
+		CPUTotalCost:               4,
+		CPUProportionalCost:        2,
+	}
+
+	pod2_hour2 := ProportionalAssetResourceCost{
+		Cluster:                    "cluster1",
+		Node:                       "node2",
+		ProviderID:                 "i-1234",
+		CPUPercentage:              0.0,
+		GPUPercentage:              0,
+		RAMPercentage:              0,
+		NodeResourceCostPercentage: 0,
+		CPUTotalCost:               4,
+		CPUProportionalCost:        1,
+	}
+
+	pod2_hour3 := ProportionalAssetResourceCost{
+		Cluster:                    "cluster1",
+		Node:                       "node2",
+		ProviderID:                 "i-1234",
+		CPUPercentage:              0.0,
+		GPUPercentage:              0,
+		RAMPercentage:              0,
+		NodeResourceCostPercentage: 0,
+		CPUTotalCost:               4,
+		CPUProportionalCost:        1.5,
+	}
+
+	pod3_hour1 := ProportionalAssetResourceCost{
+		Cluster:                    "cluster1",
+		Node:                       "node3",
+		ProviderID:                 "i-1234",
+		CPUPercentage:              0.0,
+		GPUPercentage:              0,
+		RAMPercentage:              0,
+		NodeResourceCostPercentage: 0,
+		CPUTotalCost:               4,
+		CPUProportionalCost:        1,
+	}
+
+	pod3_hour2 := ProportionalAssetResourceCost{
+		Cluster:                    "cluster1",
+		Node:                       "node3",
+		ProviderID:                 "i-1234",
+		CPUPercentage:              0.0,
+		GPUPercentage:              0,
+		RAMPercentage:              0,
+		NodeResourceCostPercentage: 0,
+		CPUTotalCost:               4,
+		CPUProportionalCost:        1,
+	}
+
+	pod3_hour3 := ProportionalAssetResourceCost{
+		Cluster:                    "cluster1",
+		Node:                       "node3",
+		ProviderID:                 "i-1234",
+		CPUPercentage:              0.0,
+		GPUPercentage:              0,
+		RAMPercentage:              0,
+		NodeResourceCostPercentage: 0,
+		CPUTotalCost:               4,
+		CPUProportionalCost:        1,
+	}
+
+	parcs := ProportionalAssetResourceCosts{}
+	parcs.Insert(pod1_hour1, true)
+	parcs.Insert(pod1_hour2, true)
+	parcs.Insert(pod1_hour3, true)
+	parcs.Insert(pod2_hour1, true)
+	parcs.Insert(pod2_hour2, true)
+	parcs.Insert(pod2_hour3, true)
+	parcs.Insert(pod3_hour1, true)
+	parcs.Insert(pod3_hour2, true)
+	parcs.Insert(pod3_hour3, true)
+	log.Debug("added all parcs")
+
+	expectedParcs := ProportionalAssetResourceCosts{
+		"cluster1,node1": ProportionalAssetResourceCost{
+			CPUPercentage:              0.041666666666666664,
+			NodeResourceCostPercentage: 0.041666666666666664,
+		},
+		"cluster1,node2": ProportionalAssetResourceCost{
+			CPUPercentage:              0.375,
+			NodeResourceCostPercentage: 0.375,
+		},
+		"cluster1,node3": ProportionalAssetResourceCost{
+			CPUPercentage:              0.25,
+			NodeResourceCostPercentage: 0.25,
+		},
+	}
+
+	for key, expectedParc := range expectedParcs {
+		actualParc, ok := parcs[key]
+		if !ok {
+			t.Fatalf("did not find expected PARC: %s", key)
+		}
+
+		if actualParc.CPUPercentage != expectedParc.CPUPercentage {
+			t.Fatalf("actual parc cpu percentage: %f did not match expected: %f", actualParc.CPUPercentage, expectedParc.CPUPercentage)
+		}
+		if actualParc.NodeResourceCostPercentage != expectedParc.NodeResourceCostPercentage {
+			t.Fatalf("actual parc node percentage: %f did not match expected: %f", actualParc.NodeResourceCostPercentage, expectedParc.NodeResourceCostPercentage)
+		}
+	}
+}
+
 // TODO niko/etl
 //func TestAllocationSet_IsEmpty(t *testing.T) {}