Explorar o código

signing commit

Signed-off-by: Alex Meijer <ameijer@kubecost.com>
Alex Meijer %!s(int64=3) %!d(string=hai) anos
pai
achega
cfebe5ed35
Modificáronse 3 ficheiros con 941 adicións e 117 borrados
  1. 22 0
      pkg/costmodel/aggregation.go
  2. 213 54
      pkg/kubecost/allocation.go
  3. 706 63
      pkg/kubecost/allocation_test.go

+ 22 - 0
pkg/costmodel/aggregation.go

@@ -2241,6 +2241,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
@@ -2264,6 +2277,15 @@ 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 {
+			WriteError(w, InternalServerError(err.Error()))
+			return
+		}
+	}
+
 	w.Write(WrapData(asr, nil))
 }
 

+ 213 - 54
pkg/kubecost/allocation.go

@@ -246,16 +246,62 @@ func (pva *PVAllocation) Equal(that *PVAllocation) bool {
 		util.IsApproximately(pva.Cost, that.Cost)
 }
 
+// used to compute the average of 2 PARCs. A PARC is a dimensonless
+// percentage that loses information needed when finding an average
+// to that end, we track the percentage and resource usage of the
+// component used to make the parc, to allow us to average two PARCs
+type ParcsComponent struct {
+	TotalCost       float64
+	UsageProportion float64
+}
+
+func (p *ParcsComponent) Clone() ParcsComponent {
+	return ParcsComponent{
+		TotalCost:       p.TotalCost,
+		UsageProportion: p.UsageProportion,
+	}
+}
+
 type ProportionalAssetResourceCost struct {
-	Cluster                    string  `json:"cluster"`
-	Node                       string  `json:"node,omitempty"`
-	ProviderID                 string  `json:"providerID,omitempty"`
-	CPUPercentage              float64 `json:"cpuPercentage"`
-	GPUPercentage              float64 `json:"gpuPercentage"`
-	RAMPercentage              float64 `json:"ramPercentage"`
-	NodeResourceCostPercentage float64 `json:"nodeResourceCostPercentage"`
+	Cluster                    string           `json:"cluster"`
+	Node                       string           `json:"node,omitempty"`
+	ProviderID                 string           `json:"providerID,omitempty"`
+	CPUPercentage              float64          `json:"cpuPercentage"`
+	GPUPercentage              float64          `json:"gpuPercentage"`
+	RAMPercentage              float64          `json:"ramPercentage"`
+	NodeResourceCostPercentage float64          `json:"nodeResourceCostPercentage"`
+	GPUComponents              []ParcsComponent `json:"-"`
+	CPUComponents              []ParcsComponent `json:"-"`
+	RAMComponents              []ParcsComponent `json:"-"`
 }
 
+func (parc ProportionalAssetResourceCost) Clone() ProportionalAssetResourceCost {
+	gpuComps := []ParcsComponent{}
+	cpuComps := []ParcsComponent{}
+	ramComps := []ParcsComponent{}
+
+	for _, gpuComp := range parc.GPUComponents {
+		gpuComps = append(gpuComps, gpuComp.Clone())
+	}
+	for _, cpuComp := range parc.CPUComponents {
+		cpuComps = append(cpuComps, cpuComp.Clone())
+	}
+	for _, ramComp := range parc.RAMComponents {
+		ramComps = append(ramComps, ramComp.Clone())
+	}
+	return ProportionalAssetResourceCost{
+		Cluster:                    parc.Cluster,
+		Node:                       parc.Node,
+		ProviderID:                 parc.ProviderID,
+		CPUPercentage:              parc.CPUPercentage,
+		GPUPercentage:              parc.GPUPercentage,
+		RAMPercentage:              parc.RAMPercentage,
+		NodeResourceCostPercentage: parc.NodeResourceCostPercentage,
+		RAMComponents:              ramComps,
+		CPUComponents:              cpuComps,
+		GPUComponents:              gpuComps,
+	}
+}
 func (parc ProportionalAssetResourceCost) Key(insertByNode bool) string {
 	if insertByNode {
 		return parc.Cluster + "," + parc.Node
@@ -267,27 +313,103 @@ func (parc ProportionalAssetResourceCost) Key(insertByNode bool) string {
 
 type ProportionalAssetResourceCosts map[string]ProportionalAssetResourceCost
 
-func (parcs ProportionalAssetResourceCosts) Insert(parc ProportionalAssetResourceCost, insertByNode bool) {
+func (parcs ProportionalAssetResourceCosts) Clone() ProportionalAssetResourceCosts {
+	cloned := ProportionalAssetResourceCosts{}
+
+	for key, parc := range parcs {
+		cloned[key] = parc.Clone()
+	}
+	return cloned
+}
+
+func (parcs ProportionalAssetResourceCosts) Insert(parc ProportionalAssetResourceCost, insertByNode, isAccumulation bool) {
 	if !insertByNode {
 		parc.Node = ""
 		parc.ProviderID = ""
 	}
 	if curr, ok := parcs[parc.Key(insertByNode)]; ok {
-		parcs[parc.Key(insertByNode)] = ProportionalAssetResourceCost{
+
+		CPUPercentage := curr.CPUPercentage + parc.CPUPercentage
+		GPUPercentage := curr.GPUPercentage + parc.GPUPercentage
+		RAMPercentage := curr.RAMPercentage + parc.RAMPercentage
+
+		toInsert := 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,
+			CPUPercentage:              CPUPercentage,
+			GPUPercentage:              GPUPercentage,
+			RAMPercentage:              RAMPercentage,
 			NodeResourceCostPercentage: curr.NodeResourceCostPercentage + parc.NodeResourceCostPercentage,
+			CPUComponents:              append(curr.CPUComponents, parc.CPUComponents...),
+			GPUComponents:              append(curr.GPUComponents, parc.GPUComponents...),
+			RAMComponents:              append(curr.RAMComponents, parc.RAMComponents...),
+		}
+
+		if isAccumulation {
+			// when accumulating, use the usage hours to perform a weighted average
+			toInsert.CPUPercentage = weightedParcComponentsAverage(toInsert.CPUComponents)
+			toInsert.GPUPercentage = weightedParcComponentsAverage(toInsert.GPUComponents)
+			toInsert.RAMPercentage = weightedParcComponentsAverage(toInsert.RAMComponents)
+			toInsert.NodeResourceCostPercentage = weightedNodeTotalCostAverage(toInsert.CPUComponents, toInsert.GPUComponents, toInsert.RAMComponents)
 		}
+
+		parcs[parc.Key(insertByNode)] = toInsert
 	} else {
 		parcs[parc.Key(insertByNode)] = parc
 	}
 }
 
-func (parcs ProportionalAssetResourceCosts) Add(that ProportionalAssetResourceCosts) {
+func weightedNodeTotalCostAverage(cpuComponents, gpuComponents, ramComponents []ParcsComponent) float64 {
+
+	totalRamCost := 0.0
+	for _, ramComponent := range ramComponents {
+		totalRamCost += ramComponent.TotalCost
+	}
+
+	totalCPUCost := 0.0
+	for _, cpuComponent := range cpuComponents {
+		totalCPUCost += cpuComponent.TotalCost
+	}
+
+	totalGPUCost := 0.0
+	for _, gpuComponent := range gpuComponents {
+		totalGPUCost += gpuComponent.TotalCost
+	}
+
+	totalCost := totalCPUCost + totalGPUCost + totalRamCost
+
+	var ramFraction, cpuFraction, gpuFraction float64
+
+	// only compute fraction if totalCost is nonzero, otherwise returns in NaN
+	if totalCost > 0 {
+		ramFraction = totalRamCost / totalCost
+		cpuFraction = totalCPUCost / totalCost
+		gpuFraction = totalGPUCost / totalCost
+	}
+
+	// compute the resource usage percentage based on the weighted fractions
+	nodeResourceCostPercentage := (weightedParcComponentsAverage(ramComponents) * ramFraction) + (weightedParcComponentsAverage(cpuComponents) * cpuFraction) + (weightedParcComponentsAverage(gpuComponents) * gpuFraction)
+
+	return nodeResourceCostPercentage
+}
+func weightedParcComponentsAverage(components []ParcsComponent) float64 {
+	totalResourceCosts := 0.0
+	costOfResource := 0.0
+	for _, component := range components {
+		totalResourceCosts += component.TotalCost
+
+		costOfResource += component.TotalCost * component.UsageProportion
+
+	}
+
+	if totalResourceCosts <= 0 {
+		return 0
+	}
+	return costOfResource / totalResourceCosts
+}
+
+func (parcs ProportionalAssetResourceCosts) Add(that ProportionalAssetResourceCosts, isAccumulation bool) {
 
 	for _, parc := range that {
 		// if node field is empty, we know this is a cluster level PARC aggregation
@@ -295,7 +417,7 @@ func (parcs ProportionalAssetResourceCosts) Add(that ProportionalAssetResourceCo
 		if parc.Node == "" {
 			insertByNode = false
 		}
-		parcs.Insert(parc, insertByNode)
+		parcs.Insert(parc, insertByNode, isAccumulation)
 	}
 }
 
@@ -334,38 +456,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(),
 	}
 }
 
@@ -757,6 +880,10 @@ func (a *Allocation) String() string {
 }
 
 func (a *Allocation) add(that *Allocation) {
+	a.addWithAccum(that, false)
+}
+
+func (a *Allocation) addWithAccum(that *Allocation, isAccumulation bool) {
 	if a == nil {
 		log.Warnf("Allocation.AggregateBy: trying to add a nil receiver")
 		return
@@ -775,8 +902,15 @@ 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 {
-		a.ProportionalAssetResourceCosts.Add(that.ProportionalAssetResourceCosts)
+	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, isAccumulation)
 	}
 
 	// Overwrite regular intersection logic for the controller name property in the
@@ -1171,7 +1305,8 @@ func (as *AllocationSet) AggregateBy(aggregateBy []string, options *AllocationAg
 				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)
+
+			alloc.ProportionalAssetResourceCosts.Insert(parc, options.IdleByNode, false)
 		}
 	}
 
@@ -1774,7 +1909,7 @@ func deriveProportionalAssetResourceCostsFromIdleCoefficients(idleCoeffs map[str
 	// compute the resource usage percentage based on the weighted fractions
 	nodeResourceCostPercentage := (ramPct * ramFraction) + (cpuPct * cpuFraction) + (gpuPct * gpuFraction)
 
-	return ProportionalAssetResourceCost{
+	parc := ProportionalAssetResourceCost{
 		Cluster:                    allocation.Properties.Cluster,
 		Node:                       allocation.Properties.Node,
 		ProviderID:                 allocation.Properties.ProviderID,
@@ -1782,7 +1917,27 @@ func deriveProportionalAssetResourceCostsFromIdleCoefficients(idleCoeffs map[str
 		GPUPercentage:              gpuPct,
 		RAMPercentage:              ramPct,
 		NodeResourceCostPercentage: nodeResourceCostPercentage,
-	}, nil
+	}
+
+	parc.CPUComponents = []ParcsComponent{
+		{
+			TotalCost:       totals[idleId]["cpu"],
+			UsageProportion: idleCoeffs[idleId][allocation.Name]["cpu"],
+		},
+	}
+	parc.GPUComponents = []ParcsComponent{
+		{
+			TotalCost:       totals[idleId]["gpu"],
+			UsageProportion: idleCoeffs[idleId][allocation.Name]["gpu"],
+		},
+	}
+	parc.RAMComponents = []ParcsComponent{
+		{
+			TotalCost:       totals[idleId]["ram"],
+			UsageProportion: idleCoeffs[idleId][allocation.Name]["ram"],
+		},
+	}
+	return parc, nil
 }
 
 // getIdleId returns the providerId or cluster of an Allocation depending on the IdleByNode
@@ -2025,6 +2180,10 @@ func (as *AllocationSet) IdleAllocations() map[string]*Allocation {
 // but only if the Allocation is valid, i.e. matches the AllocationSet's window. If
 // there is no existing entry, one is created. Nil error response indicates success.
 func (as *AllocationSet) Insert(that *Allocation) error {
+	return as.InsertWithAccum(that, false)
+}
+
+func (as *AllocationSet) InsertWithAccum(that *Allocation, isAccumulation bool) error {
 	if as == nil {
 		return fmt.Errorf("cannot insert into nil AllocationSet")
 	}
@@ -2046,7 +2205,7 @@ func (as *AllocationSet) Insert(that *Allocation) error {
 	if _, ok := as.Allocations[that.Name]; !ok {
 		as.Allocations[that.Name] = that
 	} else {
-		as.Allocations[that.Name].add(that)
+		as.Allocations[that.Name].addWithAccum(that, isAccumulation)
 	}
 
 	// If the given Allocation is an external one, record that
@@ -2197,14 +2356,14 @@ func (as *AllocationSet) Accumulate(that *AllocationSet) (*AllocationSet, error)
 	acc := NewAllocationSet(start, end)
 
 	for _, alloc := range as.Allocations {
-		err := acc.Insert(alloc)
+		err := acc.InsertWithAccum(alloc, true)
 		if err != nil {
 			return nil, err
 		}
 	}
 
 	for _, alloc := range that.Allocations {
-		err := acc.Insert(alloc)
+		err := acc.InsertWithAccum(alloc, true)
 		if err != nil {
 			return nil, err
 		}

+ 706 - 63
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"
 )
@@ -516,7 +517,9 @@ func assertParcResults(t *testing.T, as *AllocationSet, msg string, exps map[str
 	for allocKey, a := range as.Allocations {
 		for key, actualParc := range a.ProportionalAssetResourceCosts {
 			expectedParcs := exps[allocKey]
-
+			sortParcsComponent(actualParc.RAMComponents)
+			sortParcsComponent(actualParc.CPUComponents)
+			sortParcsComponent(actualParc.GPUComponents)
 			if !reflect.DeepEqual(expectedParcs[key], actualParc) {
 				t.Fatalf("actual PARC %v did not match expected PARC %v", actualParc, expectedParcs[key])
 			}
@@ -525,6 +528,19 @@ func assertParcResults(t *testing.T, as *AllocationSet, msg string, exps map[str
 	}
 }
 
+func sortParcsComponent(parcs []ParcsComponent) {
+	var n = len(parcs)
+	for i := 1; i < n; i++ {
+		j := i
+		for j > 0 {
+			if parcs[j-1].UsageProportion > parcs[j].UsageProportion {
+				parcs[j-1], parcs[j] = parcs[j], parcs[j-1]
+			}
+			j = j - 1
+		}
+	}
+}
+
 func assertAllocationTotals(t *testing.T, as *AllocationSet, msg string, exps map[string]float64) {
 	for _, a := range as.Allocations {
 		if exp, ok := exps[a.Name]; ok {
@@ -1074,35 +1090,161 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 			windowEnd:   endYesterday,
 			expMinutes:  1440.0,
 			expectedParcResults: map[string]ProportionalAssetResourceCosts{
-				"namespace1": ProportionalAssetResourceCosts{
+				"namespace1": {
 					"cluster1": ProportionalAssetResourceCost{
-						Cluster:            "cluster1",
-						Node:               "",
-						ProviderID:         "",
-						CPUPercentage:      0.5,
-						GPUPercentage:      0.5,
-						RAMPercentage:      0.8125,
+						Cluster:                    "cluster1",
+						Node:                       "",
+						ProviderID:                 "",
+						CPUPercentage:              0.5,
+						GPUPercentage:              0.5,
+						RAMPercentage:              0.8125,
 						NodeResourceCostPercentage: 0.6785714285714285,
+						GPUComponents: []ParcsComponent{
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+						},
+						CPUComponents: []ParcsComponent{
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+						},
+						RAMComponents: []ParcsComponent{
+							{
+								TotalCost:       16,
+								UsageProportion: 0.0625,
+							},
+							{
+								TotalCost:       16,
+								UsageProportion: 0.0625,
+							},
+							{
+								TotalCost:       16,
+								UsageProportion: 0.6875,
+							},
+						},
 					},
 				},
-				"namespace2": ProportionalAssetResourceCosts{
+				"namespace2": {
 					"cluster1": ProportionalAssetResourceCost{
-						Cluster:            "cluster1",
-						Node:               "",
-						ProviderID:         "",
-						CPUPercentage:      0.5,
-						GPUPercentage:      0.5,
-						RAMPercentage:      0.1875,
+						Cluster:                    "cluster1",
+						Node:                       "",
+						ProviderID:                 "",
+						CPUPercentage:              0.5,
+						GPUPercentage:              0.5,
+						RAMPercentage:              0.1875,
 						NodeResourceCostPercentage: 0.3214285714285714,
+						GPUComponents: []ParcsComponent{
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+						},
+						CPUComponents: []ParcsComponent{
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+						},
+						RAMComponents: []ParcsComponent{
+							{
+								TotalCost:       16,
+								UsageProportion: 0.0625,
+							},
+							{
+								TotalCost:       16,
+								UsageProportion: 0.0625,
+							},
+							{
+								TotalCost:       16,
+								UsageProportion: 0.0625,
+							},
+						},
 					},
 					"cluster2": ProportionalAssetResourceCost{
-						Cluster:            "cluster2",
-						Node:               "",
-						ProviderID:         "",
-						CPUPercentage:      0.5,
-						GPUPercentage:      0.5,
-						RAMPercentage:      0.5,
+						Cluster:                    "cluster2",
+						Node:                       "",
+						ProviderID:                 "",
+						CPUPercentage:              0.5,
+						GPUPercentage:              0.5,
+						RAMPercentage:              0.5,
 						NodeResourceCostPercentage: 0.5,
+						GPUComponents: []ParcsComponent{
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+						},
+						CPUComponents: []ParcsComponent{
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+						},
+						RAMComponents: []ParcsComponent{
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+						},
 					},
 				},
 			},
@@ -1514,71 +1656,251 @@ func TestAllocationSet_AggregateBy(t *testing.T) {
 			expectedParcResults: map[string]ProportionalAssetResourceCosts{
 				"namespace1": {
 					"cluster1,c1nodes": ProportionalAssetResourceCost{
-						Cluster:            "cluster1",
-						Node:               "c1nodes",
-						ProviderID:         "c1nodes",
-						CPUPercentage:      0.5,
-						GPUPercentage:      0.5,
-						RAMPercentage:      0.8125,
+						Cluster:                    "cluster1",
+						Node:                       "c1nodes",
+						ProviderID:                 "c1nodes",
+						CPUPercentage:              0.5,
+						GPUPercentage:              0.5,
+						RAMPercentage:              0.8125,
 						NodeResourceCostPercentage: 0.6785714285714285,
+						GPUComponents: []ParcsComponent{
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+						},
+						CPUComponents: []ParcsComponent{
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+						},
+						RAMComponents: []ParcsComponent{
+							{
+								TotalCost:       16,
+								UsageProportion: 0.0625,
+							},
+							{
+								TotalCost:       16,
+								UsageProportion: 0.0625,
+							},
+							{
+								TotalCost:       16,
+								UsageProportion: 0.6875,
+							},
+						},
 					},
 					"cluster2,node2": ProportionalAssetResourceCost{
-						Cluster:            "cluster2",
-						Node:               "node2",
-						ProviderID:         "node2",
-						CPUPercentage:      0.5,
-						GPUPercentage:      0.5,
-						RAMPercentage:      0.5,
+						Cluster:                    "cluster2",
+						Node:                       "node2",
+						ProviderID:                 "node2",
+						CPUPercentage:              0.5,
+						GPUPercentage:              0.5,
+						RAMPercentage:              0.5,
 						NodeResourceCostPercentage: 0.5,
 					},
 				},
 				"namespace2": {
 					"cluster1,c1nodes": ProportionalAssetResourceCost{
-						Cluster:            "cluster1",
-						Node:               "c1nodes",
-						ProviderID:         "c1nodes",
-						CPUPercentage:      0.5,
-						GPUPercentage:      0.5,
-						RAMPercentage:      0.1875,
+						Cluster:                    "cluster1",
+						Node:                       "c1nodes",
+						ProviderID:                 "c1nodes",
+						CPUPercentage:              0.5,
+						GPUPercentage:              0.5,
+						RAMPercentage:              0.1875,
 						NodeResourceCostPercentage: 0.3214285714285714,
+						GPUComponents: []ParcsComponent{
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+						},
+						CPUComponents: []ParcsComponent{
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+							{
+								TotalCost:       6,
+								UsageProportion: 0.16666666666666666,
+							},
+						},
+						RAMComponents: []ParcsComponent{
+							{
+								TotalCost:       16,
+								UsageProportion: 0.0625,
+							},
+							{
+								TotalCost:       16,
+								UsageProportion: 0.0625,
+							},
+							{
+								TotalCost:       16,
+								UsageProportion: 0.0625,
+							},
+						},
 					},
 					"cluster2,node1": ProportionalAssetResourceCost{
-						Cluster:            "cluster2",
-						Node:               "node1",
-						ProviderID:         "node1",
-						CPUPercentage:      1,
-						GPUPercentage:      1,
-						RAMPercentage:      1,
+						Cluster:                    "cluster2",
+						Node:                       "node1",
+						ProviderID:                 "node1",
+						CPUPercentage:              1,
+						GPUPercentage:              1,
+						RAMPercentage:              1,
 						NodeResourceCostPercentage: 1,
+						GPUComponents: []ParcsComponent{
+							{
+								TotalCost:       2,
+								UsageProportion: 0.5,
+							},
+							{
+								TotalCost:       2,
+								UsageProportion: 0.5,
+							},
+						},
+						CPUComponents: []ParcsComponent{
+							{
+								TotalCost:       2,
+								UsageProportion: 0.5,
+							},
+							{
+								TotalCost:       2,
+								UsageProportion: 0.5,
+							},
+						},
+						RAMComponents: []ParcsComponent{
+							{
+								TotalCost:       2,
+								UsageProportion: 0.5,
+							},
+							{
+								TotalCost:       2,
+								UsageProportion: 0.5,
+							},
+						},
 					},
 					"cluster2,node2": ProportionalAssetResourceCost{
-						Cluster:            "cluster2",
-						Node:               "node2",
-						ProviderID:         "node2",
-						CPUPercentage:      0.5,
-						GPUPercentage:      0.5,
-						RAMPercentage:      0.5,
+						Cluster:                    "cluster2",
+						Node:                       "node2",
+						ProviderID:                 "node2",
+						CPUPercentage:              0.5,
+						GPUPercentage:              0.5,
+						RAMPercentage:              0.5,
 						NodeResourceCostPercentage: 0.5,
+						GPUComponents: []ParcsComponent{
+							{
+								TotalCost:       2,
+								UsageProportion: 0.5,
+							},
+						},
+						CPUComponents: []ParcsComponent{
+							{
+								TotalCost:       2,
+								UsageProportion: 0.5,
+							},
+						},
+						RAMComponents: []ParcsComponent{
+							{
+								TotalCost:       2,
+								UsageProportion: 0.5,
+							},
+						},
 					},
 				},
 				"namespace3": {
 					"cluster2,node3": ProportionalAssetResourceCost{
-						Cluster:            "cluster2",
-						Node:               "node3",
-						ProviderID:         "node3",
-						CPUPercentage:      1,
-						GPUPercentage:      1,
-						RAMPercentage:      1,
+						Cluster:                    "cluster2",
+						Node:                       "node3",
+						ProviderID:                 "node3",
+						CPUPercentage:              1,
+						GPUPercentage:              1,
+						RAMPercentage:              1,
 						NodeResourceCostPercentage: 1,
+						GPUComponents: []ParcsComponent{
+							{
+								TotalCost:       2,
+								UsageProportion: 0.5,
+							},
+							{
+								TotalCost:       2,
+								UsageProportion: 0.5,
+							},
+						},
+						CPUComponents: []ParcsComponent{
+							{
+								TotalCost:       2,
+								UsageProportion: 0.5,
+							},
+							{
+								TotalCost:       2,
+								UsageProportion: 0.5,
+							},
+						},
+						RAMComponents: []ParcsComponent{
+							{
+								TotalCost:       2,
+								UsageProportion: 0.5,
+							},
+							{
+								TotalCost:       2,
+								UsageProportion: 0.5,
+							},
+						},
 					},
 					"cluster2,node2": ProportionalAssetResourceCost{
-						Cluster:            "cluster2",
-						Node:               "node2",
-						ProviderID:         "node2",
-						CPUPercentage:      0.5,
-						GPUPercentage:      0.5,
-						RAMPercentage:      0.5,
+						Cluster:                    "cluster2",
+						Node:                       "node2",
+						ProviderID:                 "node2",
+						CPUPercentage:              0.5,
+						GPUPercentage:              0.5,
+						RAMPercentage:              0.5,
 						NodeResourceCostPercentage: 0.5,
+						GPUComponents: []ParcsComponent{
+							{
+								TotalCost:       2,
+								UsageProportion: 0.5,
+							},
+						},
+						CPUComponents: []ParcsComponent{
+							{
+								TotalCost:       2,
+								UsageProportion: 0.5,
+							},
+						},
+						RAMComponents: []ParcsComponent{
+							{
+								TotalCost:       2,
+								UsageProportion: 0.5,
+							},
+						},
 					},
 				},
 			},
@@ -1708,6 +2030,327 @@ 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,
+		CPUComponents: []ParcsComponent{
+			{
+				TotalCost:       4,
+				UsageProportion: 0.125,
+			},
+		},
+		GPUComponents: []ParcsComponent{
+			{
+				TotalCost:       0,
+				UsageProportion: 0.0,
+			},
+		},
+		RAMComponents: []ParcsComponent{
+			{
+				TotalCost:       0,
+				UsageProportion: 0.0,
+			},
+		},
+	}
+
+	pod1_hour2 := ProportionalAssetResourceCost{
+		Cluster:                    "cluster1",
+		Node:                       "node1",
+		ProviderID:                 "i-1234",
+		CPUPercentage:              0.0,
+		GPUPercentage:              0,
+		RAMPercentage:              0,
+		NodeResourceCostPercentage: 0,
+		CPUComponents: []ParcsComponent{
+			{
+				TotalCost:       4,
+				UsageProportion: 0.0,
+			},
+		},
+		GPUComponents: []ParcsComponent{
+			{
+				TotalCost:       0,
+				UsageProportion: 0.0,
+			},
+		},
+		RAMComponents: []ParcsComponent{
+			{
+				TotalCost:       0,
+				UsageProportion: 0.0,
+			},
+		},
+	}
+
+	pod1_hour3 := ProportionalAssetResourceCost{
+		Cluster:                    "cluster1",
+		Node:                       "node1",
+		ProviderID:                 "i-1234",
+		CPUPercentage:              0.0,
+		GPUPercentage:              0,
+		RAMPercentage:              0,
+		NodeResourceCostPercentage: 0,
+		CPUComponents: []ParcsComponent{
+			{
+				TotalCost:       4,
+				UsageProportion: 0.0,
+			},
+		},
+		GPUComponents: []ParcsComponent{
+			{
+				TotalCost:       0,
+				UsageProportion: 0.0,
+			},
+		},
+		RAMComponents: []ParcsComponent{
+			{
+				TotalCost:       0,
+				UsageProportion: 0.0,
+			},
+		},
+	}
+
+	pod2_hour1 := ProportionalAssetResourceCost{
+		Cluster:                    "cluster1",
+		Node:                       "node2",
+		ProviderID:                 "i-1234",
+		CPUPercentage:              0.0,
+		GPUPercentage:              0,
+		RAMPercentage:              0,
+		NodeResourceCostPercentage: 0,
+		CPUComponents: []ParcsComponent{
+			{
+				TotalCost:       4,
+				UsageProportion: 0.5,
+			},
+		},
+		GPUComponents: []ParcsComponent{
+			{
+				TotalCost:       0,
+				UsageProportion: 0.0,
+			},
+		},
+		RAMComponents: []ParcsComponent{
+			{
+				TotalCost:       0,
+				UsageProportion: 0.0,
+			},
+		},
+	}
+
+	pod2_hour2 := ProportionalAssetResourceCost{
+		Cluster:                    "cluster1",
+		Node:                       "node2",
+		ProviderID:                 "i-1234",
+		CPUPercentage:              0.0,
+		GPUPercentage:              0,
+		RAMPercentage:              0,
+		NodeResourceCostPercentage: 0,
+		CPUComponents: []ParcsComponent{
+			{
+				TotalCost:       4,
+				UsageProportion: 0.25,
+			},
+		},
+		GPUComponents: []ParcsComponent{
+			{
+				TotalCost:       0,
+				UsageProportion: 0.0,
+			},
+		},
+		RAMComponents: []ParcsComponent{
+			{
+				TotalCost:       0,
+				UsageProportion: 0.0,
+			},
+		},
+	}
+
+	pod2_hour3 := ProportionalAssetResourceCost{
+		Cluster:                    "cluster1",
+		Node:                       "node2",
+		ProviderID:                 "i-1234",
+		CPUPercentage:              0.0,
+		GPUPercentage:              0,
+		RAMPercentage:              0,
+		NodeResourceCostPercentage: 0,
+		CPUComponents: []ParcsComponent{
+			{
+				TotalCost:       4,
+				UsageProportion: 0.375,
+			},
+		},
+		GPUComponents: []ParcsComponent{
+			{
+				TotalCost:       0,
+				UsageProportion: 0.0,
+			},
+		},
+		RAMComponents: []ParcsComponent{
+			{
+				TotalCost:       0,
+				UsageProportion: 0.0,
+			},
+		},
+	}
+
+	pod3_hour1 := ProportionalAssetResourceCost{
+		Cluster:                    "cluster1",
+		Node:                       "node3",
+		ProviderID:                 "i-1234",
+		CPUPercentage:              0.0,
+		GPUPercentage:              0,
+		RAMPercentage:              0,
+		NodeResourceCostPercentage: 0,
+		CPUComponents: []ParcsComponent{
+			{
+				TotalCost:       4,
+				UsageProportion: 0.25,
+			},
+		},
+		GPUComponents: []ParcsComponent{
+			{
+				TotalCost:       0,
+				UsageProportion: 0.0,
+			},
+		},
+		RAMComponents: []ParcsComponent{
+			{
+				TotalCost:       0,
+				UsageProportion: 0.0,
+			},
+		},
+	}
+
+	pod3_hour2 := ProportionalAssetResourceCost{
+		Cluster:                    "cluster1",
+		Node:                       "node3",
+		ProviderID:                 "i-1234",
+		CPUPercentage:              0.0,
+		GPUPercentage:              0,
+		RAMPercentage:              0,
+		NodeResourceCostPercentage: 0,
+		CPUComponents: []ParcsComponent{
+			{
+				TotalCost:       4,
+				UsageProportion: 0.25,
+			},
+		},
+		GPUComponents: []ParcsComponent{
+			{
+				TotalCost:       0,
+				UsageProportion: 0.0,
+			},
+		},
+		RAMComponents: []ParcsComponent{
+			{
+				TotalCost:       0,
+				UsageProportion: 0.0,
+			},
+		},
+	}
+
+	pod3_hour3 := ProportionalAssetResourceCost{
+		Cluster:                    "cluster1",
+		Node:                       "node3",
+		ProviderID:                 "i-1234",
+		CPUPercentage:              0.0,
+		GPUPercentage:              0,
+		RAMPercentage:              0,
+		NodeResourceCostPercentage: 0,
+		CPUComponents: []ParcsComponent{
+			{
+				TotalCost:       4,
+				UsageProportion: 0.25,
+			},
+		},
+		GPUComponents: []ParcsComponent{
+			{
+				TotalCost:       0,
+				UsageProportion: 0.0,
+			},
+		},
+		RAMComponents: []ParcsComponent{
+			{
+				TotalCost:       0,
+				UsageProportion: 0.0,
+			},
+		},
+	}
+
+	parcs := ProportionalAssetResourceCosts{}
+	parcs.Insert(pod1_hour1, true, true)
+	parcs.Insert(pod1_hour2, true, true)
+	parcs.Insert(pod1_hour3, true, true)
+	parcs.Insert(pod2_hour1, true, true)
+	parcs.Insert(pod2_hour2, true, true)
+	parcs.Insert(pod2_hour3, true, true)
+	parcs.Insert(pod3_hour1, true, true)
+	parcs.Insert(pod3_hour2, true, true)
+	parcs.Insert(pod3_hour3, true, 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) {}