summaryallocation.go 47 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495
  1. package kubecost
  2. import (
  3. "errors"
  4. "fmt"
  5. "strings"
  6. "sync"
  7. "time"
  8. "github.com/opencost/opencost/pkg/log"
  9. )
  10. // SummaryAllocation summarizes an Allocation, keeping only fields necessary
  11. // for providing a high-level view of identifying the Allocation over a period
  12. // of time (Start, End) over which it ran, and inspecting the associated per-
  13. // resource costs (subtotaled with adjustments), total cost, and efficiency.
  14. //
  15. // SummaryAllocation does not have a concept of Window (i.e. the time period
  16. // within which it is defined, as opposed to the Start and End times). That
  17. // context must be provided by a SummaryAllocationSet.
  18. type SummaryAllocation struct {
  19. Name string `json:"name"`
  20. Properties *AllocationProperties `json:"-"`
  21. Start time.Time `json:"start"`
  22. End time.Time `json:"end"`
  23. CPUCoreRequestAverage float64 `json:"cpuCoreRequestAverage"`
  24. CPUCoreUsageAverage float64 `json:"cpuCoreUsageAverage"`
  25. CPUCost float64 `json:"cpuCost"`
  26. GPUCost float64 `json:"gpuCost"`
  27. NetworkCost float64 `json:"networkCost"`
  28. LoadBalancerCost float64 `json:"loadBalancerCost"`
  29. PVCost float64 `json:"pvCost"`
  30. RAMBytesRequestAverage float64 `json:"ramByteRequestAverage"`
  31. RAMBytesUsageAverage float64 `json:"ramByteUsageAverage"`
  32. RAMCost float64 `json:"ramCost"`
  33. SharedCost float64 `json:"sharedCost"`
  34. ExternalCost float64 `json:"externalCost"`
  35. Share bool `json:"-"`
  36. }
  37. // NewSummaryAllocation converts an Allocation to a SummaryAllocation by
  38. // dropping unnecessary fields and consolidating others (e.g. adjustments).
  39. // Reconciliation happens here because that process is synonymous with the
  40. // consolidation of adjustment fields.
  41. func NewSummaryAllocation(alloc *Allocation, reconcile, reconcileNetwork bool) *SummaryAllocation {
  42. if alloc == nil {
  43. return nil
  44. }
  45. sa := &SummaryAllocation{
  46. Name: alloc.Name,
  47. Properties: alloc.Properties,
  48. Start: alloc.Start,
  49. End: alloc.End,
  50. CPUCoreRequestAverage: alloc.CPUCoreRequestAverage,
  51. CPUCoreUsageAverage: alloc.CPUCoreUsageAverage,
  52. CPUCost: alloc.CPUCost + alloc.CPUCostAdjustment,
  53. GPUCost: alloc.GPUCost + alloc.GPUCostAdjustment,
  54. NetworkCost: alloc.NetworkCost + alloc.NetworkCostAdjustment,
  55. LoadBalancerCost: alloc.LoadBalancerCost + alloc.LoadBalancerCostAdjustment,
  56. PVCost: alloc.PVCost() + alloc.PVCostAdjustment,
  57. RAMBytesRequestAverage: alloc.RAMBytesRequestAverage,
  58. RAMBytesUsageAverage: alloc.RAMBytesUsageAverage,
  59. RAMCost: alloc.RAMCost + alloc.RAMCostAdjustment,
  60. SharedCost: alloc.SharedCost,
  61. ExternalCost: alloc.ExternalCost,
  62. }
  63. // Revert adjustments if reconciliation is off. If only network
  64. // reconciliation is off, only revert network adjustment.
  65. if !reconcile {
  66. sa.CPUCost -= alloc.CPUCostAdjustment
  67. sa.GPUCost -= alloc.GPUCostAdjustment
  68. sa.NetworkCost -= alloc.NetworkCostAdjustment
  69. sa.LoadBalancerCost -= alloc.LoadBalancerCostAdjustment
  70. sa.PVCost -= alloc.PVCostAdjustment
  71. sa.RAMCost -= alloc.RAMCostAdjustment
  72. } else if !reconcileNetwork {
  73. sa.NetworkCost -= alloc.NetworkCostAdjustment
  74. }
  75. return sa
  76. }
  77. // Add sums two SummaryAllocations, adding the given SummaryAllocation to the
  78. // receiving one, thus mutating the receiver. For performance reasons, it
  79. // simply drops Properties, so a SummaryAllocation can only be Added once.
  80. func (sa *SummaryAllocation) Add(that *SummaryAllocation) error {
  81. if sa == nil || that == nil {
  82. return errors.New("cannot Add a nil SummaryAllocation")
  83. }
  84. // Once Added, a SummaryAllocation has no Properties. This saves us from
  85. // having to compute the intersection of two sets of Properties, which is
  86. // expensive.
  87. sa.Properties = nil
  88. // Sum non-cumulative fields by turning them into cumulative, adding them,
  89. // and then converting them back into averages after minutes have been
  90. // combined (just below).
  91. cpuReqCoreMins := sa.CPUCoreRequestAverage * sa.Minutes()
  92. cpuReqCoreMins += that.CPUCoreRequestAverage * that.Minutes()
  93. cpuUseCoreMins := sa.CPUCoreUsageAverage * sa.Minutes()
  94. cpuUseCoreMins += that.CPUCoreUsageAverage * that.Minutes()
  95. ramReqByteMins := sa.RAMBytesRequestAverage * sa.Minutes()
  96. ramReqByteMins += that.RAMBytesRequestAverage * that.Minutes()
  97. ramUseByteMins := sa.RAMBytesUsageAverage * sa.Minutes()
  98. ramUseByteMins += that.RAMBytesUsageAverage * that.Minutes()
  99. // Expand Start and End to be the "max" of among the given Allocations
  100. if that.Start.Before(sa.Start) {
  101. sa.Start = that.Start
  102. }
  103. if that.End.After(sa.End) {
  104. sa.End = that.End
  105. }
  106. // Convert cumulative request and usage back into rates
  107. if sa.Minutes() > 0 {
  108. sa.CPUCoreRequestAverage = cpuReqCoreMins / sa.Minutes()
  109. sa.CPUCoreUsageAverage = cpuUseCoreMins / sa.Minutes()
  110. sa.RAMBytesRequestAverage = ramReqByteMins / sa.Minutes()
  111. sa.RAMBytesUsageAverage = ramUseByteMins / sa.Minutes()
  112. } else {
  113. sa.CPUCoreRequestAverage = 0.0
  114. sa.CPUCoreUsageAverage = 0.0
  115. sa.RAMBytesRequestAverage = 0.0
  116. sa.RAMBytesUsageAverage = 0.0
  117. }
  118. // Sum all cumulative cost fields
  119. sa.CPUCost += that.CPUCost
  120. sa.ExternalCost += that.ExternalCost
  121. sa.GPUCost += that.GPUCost
  122. sa.LoadBalancerCost += that.LoadBalancerCost
  123. sa.NetworkCost += that.NetworkCost
  124. sa.PVCost += that.PVCost
  125. sa.RAMCost += that.RAMCost
  126. sa.SharedCost += that.SharedCost
  127. return nil
  128. }
  129. // Clone copies the SummaryAllocation and returns the copy
  130. func (sa *SummaryAllocation) Clone() *SummaryAllocation {
  131. return &SummaryAllocation{
  132. Name: sa.Name,
  133. Properties: sa.Properties.Clone(),
  134. Start: sa.Start,
  135. End: sa.End,
  136. CPUCoreRequestAverage: sa.CPUCoreRequestAverage,
  137. CPUCoreUsageAverage: sa.CPUCoreUsageAverage,
  138. CPUCost: sa.CPUCost,
  139. GPUCost: sa.GPUCost,
  140. NetworkCost: sa.NetworkCost,
  141. LoadBalancerCost: sa.LoadBalancerCost,
  142. PVCost: sa.PVCost,
  143. RAMBytesRequestAverage: sa.RAMBytesRequestAverage,
  144. RAMBytesUsageAverage: sa.RAMBytesUsageAverage,
  145. RAMCost: sa.RAMCost,
  146. SharedCost: sa.SharedCost,
  147. ExternalCost: sa.ExternalCost,
  148. }
  149. }
  150. // CPUEfficiency is the ratio of usage to request. If there is no request and
  151. // no usage or cost, then efficiency is zero. If there is no request, but there
  152. // is usage or cost, then efficiency is 100%.
  153. func (sa *SummaryAllocation) CPUEfficiency() float64 {
  154. if sa == nil || sa.IsIdle() {
  155. return 0.0
  156. }
  157. if sa.CPUCoreRequestAverage > 0 {
  158. return sa.CPUCoreUsageAverage / sa.CPUCoreRequestAverage
  159. }
  160. if sa.CPUCoreUsageAverage == 0.0 || sa.CPUCost == 0.0 {
  161. return 0.0
  162. }
  163. return 1.0
  164. }
  165. func (sa *SummaryAllocation) generateKey(aggregateBy []string, labelConfig *LabelConfig) string {
  166. if sa == nil {
  167. return ""
  168. }
  169. return sa.Properties.GenerateKey(aggregateBy, labelConfig)
  170. }
  171. // IsExternal is true if the given SummaryAllocation represents external costs.
  172. func (sa *SummaryAllocation) IsExternal() bool {
  173. if sa == nil {
  174. return false
  175. }
  176. return strings.Contains(sa.Name, ExternalSuffix)
  177. }
  178. // IsIdle is true if the given SummaryAllocation represents idle costs.
  179. func (sa *SummaryAllocation) IsIdle() bool {
  180. if sa == nil {
  181. return false
  182. }
  183. return strings.Contains(sa.Name, IdleSuffix)
  184. }
  185. // IsUnallocated is true if the given SummaryAllocation represents unallocated
  186. // costs.
  187. func (sa *SummaryAllocation) IsUnallocated() bool {
  188. if sa == nil {
  189. return false
  190. }
  191. return strings.Contains(sa.Name, UnallocatedSuffix)
  192. }
  193. // IsUnmounted is true if the given SummaryAllocation represents unmounted
  194. // volume costs.
  195. func (sa *SummaryAllocation) IsUnmounted() bool {
  196. if sa == nil {
  197. return false
  198. }
  199. return strings.Contains(sa.Name, UnmountedSuffix)
  200. }
  201. // Minutes returns the number of minutes the SummaryAllocation represents, as
  202. // defined by the difference between the end and start times.
  203. func (sa *SummaryAllocation) Minutes() float64 {
  204. if sa == nil {
  205. return 0.0
  206. }
  207. return sa.End.Sub(sa.Start).Minutes()
  208. }
  209. // RAMEfficiency is the ratio of usage to request. If there is no request and
  210. // no usage or cost, then efficiency is zero. If there is no request, but there
  211. // is usage or cost, then efficiency is 100%.
  212. func (sa *SummaryAllocation) RAMEfficiency() float64 {
  213. if sa == nil || sa.IsIdle() {
  214. return 0.0
  215. }
  216. if sa.RAMBytesRequestAverage > 0 {
  217. return sa.RAMBytesUsageAverage / sa.RAMBytesRequestAverage
  218. }
  219. if sa.RAMBytesUsageAverage == 0.0 || sa.RAMCost == 0.0 {
  220. return 0.0
  221. }
  222. return 1.0
  223. }
  224. // TotalCost is the total cost of the SummaryAllocation
  225. func (sa *SummaryAllocation) TotalCost() float64 {
  226. if sa == nil {
  227. return 0.0
  228. }
  229. return sa.CPUCost + sa.GPUCost + sa.RAMCost + sa.PVCost + sa.NetworkCost + sa.LoadBalancerCost + sa.SharedCost + sa.ExternalCost
  230. }
  231. // TotalEfficiency is the cost-weighted average of CPU and RAM efficiency. If
  232. // there is no cost at all, then efficiency is zero.
  233. func (sa *SummaryAllocation) TotalEfficiency() float64 {
  234. if sa == nil || sa.IsIdle() {
  235. return 0.0
  236. }
  237. if sa.RAMCost+sa.CPUCost > 0 {
  238. ramCostEff := sa.RAMEfficiency() * sa.RAMCost
  239. cpuCostEff := sa.CPUEfficiency() * sa.CPUCost
  240. return (ramCostEff + cpuCostEff) / (sa.CPUCost + sa.RAMCost)
  241. }
  242. return 0.0
  243. }
  244. // SummaryAllocationSet stores a set of SummaryAllocations, each with a unique
  245. // name, that share a window. An AllocationSet is mutable, so treat it like a
  246. // threadsafe map.
  247. type SummaryAllocationSet struct {
  248. sync.RWMutex
  249. externalKeys map[string]bool
  250. idleKeys map[string]bool
  251. SummaryAllocations map[string]*SummaryAllocation `json:"allocations"`
  252. Window Window `json:"window"`
  253. }
  254. // NewSummaryAllocationSet converts an AllocationSet to a SummaryAllocationSet.
  255. // Filter functions, keep functions, and reconciliation parameters are
  256. // required for unfortunate reasons to do with performance and legacy order-of-
  257. // operations details, as well as the fact that reconciliation has been
  258. // pushed down to the conversion step between Allocation and SummaryAllocation.
  259. func NewSummaryAllocationSet(as *AllocationSet, filter AllocationFilter, kfs []AllocationMatchFunc, reconcile, reconcileNetwork bool) *SummaryAllocationSet {
  260. if as == nil {
  261. return nil
  262. }
  263. // Pre-flatten the filter so we can just check == nil to see if there are
  264. // filters.
  265. if filter != nil {
  266. filter = filter.Flattened()
  267. }
  268. // If we can know the exact size of the map, use it. If filters or sharing
  269. // functions are present, we can't know the size, so we make a default map.
  270. var sasMap map[string]*SummaryAllocation
  271. if filter == nil && len(kfs) == 0 {
  272. // No filters, so make the map of summary allocations exactly the size
  273. // of the origin allocation set.
  274. sasMap = make(map[string]*SummaryAllocation, len(as.Allocations))
  275. } else {
  276. // There are filters, so start with a standard map
  277. sasMap = make(map[string]*SummaryAllocation)
  278. }
  279. sas := &SummaryAllocationSet{
  280. SummaryAllocations: sasMap,
  281. Window: as.Window.Clone(),
  282. }
  283. for _, alloc := range as.Allocations {
  284. // First, detect if the allocation should be kept. If so, mark it as
  285. // such, insert it, and continue.
  286. shouldKeep := false
  287. for _, kf := range kfs {
  288. if kf(alloc) {
  289. shouldKeep = true
  290. break
  291. }
  292. }
  293. if shouldKeep {
  294. sa := NewSummaryAllocation(alloc, reconcile, reconcileNetwork)
  295. sa.Share = true
  296. sas.Insert(sa)
  297. continue
  298. }
  299. // If the allocation does not pass any of the given filter functions,
  300. // do not insert it into the set.
  301. if filter != nil && !filter.Matches(alloc) {
  302. continue
  303. }
  304. err := sas.Insert(NewSummaryAllocation(alloc, reconcile, reconcileNetwork))
  305. if err != nil {
  306. log.Errorf("SummaryAllocation: error inserting summary of %s", alloc.Name)
  307. }
  308. }
  309. for key := range as.ExternalKeys {
  310. sas.externalKeys[key] = true
  311. }
  312. for key := range as.IdleKeys {
  313. sas.idleKeys[key] = true
  314. }
  315. return sas
  316. }
  317. // Clone creates a deep copy of the SummaryAllocationSet
  318. func (sas *SummaryAllocationSet) Clone() *SummaryAllocationSet {
  319. sas.RLock()
  320. defer sas.RUnlock()
  321. externalKeys := make(map[string]bool, len(sas.externalKeys))
  322. for k, v := range sas.externalKeys {
  323. externalKeys[k] = v
  324. }
  325. idleKeys := make(map[string]bool, len(sas.idleKeys))
  326. for k, v := range sas.idleKeys {
  327. idleKeys[k] = v
  328. }
  329. summaryAllocations := make(map[string]*SummaryAllocation, len(sas.SummaryAllocations))
  330. for k, v := range sas.SummaryAllocations {
  331. summaryAllocations[k] = v.Clone()
  332. }
  333. return &SummaryAllocationSet{
  334. externalKeys: externalKeys,
  335. idleKeys: idleKeys,
  336. SummaryAllocations: summaryAllocations,
  337. Window: sas.Window.Clone(),
  338. }
  339. }
  340. // Add sums two SummaryAllocationSets, which Adds all SummaryAllocations in the
  341. // given SummaryAllocationSet to thier counterparts in the receiving set. Add
  342. // also expands the Window to include both constituent Windows, in the case
  343. // that Add is being used from accumulating (as opposed to aggregating). For
  344. // performance reasons, the function may return either a new set, or an
  345. // unmodified original, so it should not be assumed that the original sets are
  346. // safeuly usable after calling Add.
  347. func (sas *SummaryAllocationSet) Add(that *SummaryAllocationSet) (*SummaryAllocationSet, error) {
  348. if sas == nil || len(sas.SummaryAllocations) == 0 {
  349. return that, nil
  350. }
  351. if that == nil || len(that.SummaryAllocations) == 0 {
  352. return sas, nil
  353. }
  354. if sas.Window.IsOpen() {
  355. return nil, errors.New("cannot add a SummaryAllocationSet with an open window")
  356. }
  357. // Set start, end to min(start), max(end)
  358. start := *sas.Window.Start()
  359. end := *sas.Window.End()
  360. if that.Window.Start().Before(start) {
  361. start = *that.Window.Start()
  362. }
  363. if that.Window.End().After(end) {
  364. end = *that.Window.End()
  365. }
  366. acc := &SummaryAllocationSet{
  367. SummaryAllocations: make(map[string]*SummaryAllocation, len(sas.SummaryAllocations)),
  368. Window: NewClosedWindow(start, end),
  369. }
  370. sas.RLock()
  371. defer sas.RUnlock()
  372. that.RLock()
  373. defer that.RUnlock()
  374. for _, alloc := range sas.SummaryAllocations {
  375. err := acc.Insert(alloc)
  376. if err != nil {
  377. return nil, err
  378. }
  379. }
  380. for _, alloc := range that.SummaryAllocations {
  381. err := acc.Insert(alloc)
  382. if err != nil {
  383. return nil, err
  384. }
  385. }
  386. return acc, nil
  387. }
  388. // AggregateBy aggregates the Allocations in the given AllocationSet by the given
  389. // AllocationProperty. This will only be legal if the AllocationSet is divisible by the
  390. // given AllocationProperty; e.g. Containers can be divided by Namespace, but not vice-a-versa.
  391. func (sas *SummaryAllocationSet) AggregateBy(aggregateBy []string, options *AllocationAggregationOptions) error {
  392. if sas == nil || len(sas.SummaryAllocations) == 0 {
  393. return nil
  394. }
  395. if sas.Window.IsOpen() {
  396. return errors.New("cannot aggregate a SummaryAllocationSet with an open window")
  397. }
  398. if options == nil {
  399. options = &AllocationAggregationOptions{}
  400. }
  401. if options.LabelConfig == nil {
  402. options.LabelConfig = NewLabelConfig()
  403. }
  404. // Pre-flatten the filter so we can just check == nil to see if there are
  405. // filters.
  406. if options.Filter != nil {
  407. options.Filter = options.Filter.Flattened()
  408. }
  409. // Check if we have any work to do; if not, then early return. If
  410. // aggregateBy is nil, we don't aggregate anything. On the other hand,
  411. // an empty slice implies that we should aggregate everything. (See
  412. // generateKey for why that makes sense.)
  413. shouldAggregate := aggregateBy != nil
  414. shouldKeep := len(options.SharedHourlyCosts) > 0 || len(options.ShareFuncs) > 0
  415. if !shouldAggregate && !shouldKeep {
  416. return nil
  417. }
  418. // The order of operations for aggregating a SummaryAllotionSet is as
  419. // follows:
  420. //
  421. // 1. Partition external, idle, and shared allocations into separate sets.
  422. // Also, create the resultSet into which the results will be aggregated.
  423. //
  424. // 2. Record resource totals for shared costs and unmounted volumes so
  425. // that we can account for them in computing idle coefficients.
  426. //
  427. // 3. Retrieve pre-computed allocation resource totals, which will be used
  428. // to compute idle sharing coefficients.
  429. //
  430. // 4. Convert shared hourly cost into a cumulative allocation to share,
  431. // and insert it into the share set.
  432. //
  433. // 5. Compute sharing coefficients per-aggregation, if sharing resources.
  434. //
  435. // 6. Distribute idle allocations according to the idle coefficients.
  436. //
  437. // 7. Record allocation resource totals (after filtration) if filters have
  438. // been applied. (Used for filtering proportional amount of idle.)
  439. //
  440. // 8. Generate aggregation key and insert allocation into the output set
  441. //
  442. // 9. If idle is shared and resources are shared, it's probable that some
  443. // amount of idle cost will be shared with a shared resource.
  444. // Distribute that idle cost, if it exists, among the respective shared
  445. // allocations before sharing them with the aggregated allocations.
  446. //
  447. // 10. Apply idle filtration, which "filters" the idle cost, or scales it
  448. // by the proportion of allocation resources remaining after filters
  449. // have been applied.
  450. //
  451. // 11. Distribute shared resources according to sharing coefficients.
  452. //
  453. // 12. Insert external allocations into the result set.
  454. //
  455. // 13. Insert any undistributed idle, in the case that idle
  456. // coefficients end up being zero and some idle is not shared.
  457. //
  458. // 14. Combine all idle allocations into a single idle allocation, unless
  459. // the option to keep idle split by cluster or node is enabled.
  460. // 1. Partition external, idle, and shared allocations into separate sets.
  461. // Also, create the resultSet into which the results will be aggregated.
  462. // resultSet will collect the aggregated allocations
  463. resultSet := &SummaryAllocationSet{
  464. Window: sas.Window.Clone(),
  465. }
  466. // externalSet will collect external allocations
  467. externalSet := &SummaryAllocationSet{
  468. Window: sas.Window.Clone(),
  469. }
  470. // idleSet will be shared among resultSet after initial aggregation
  471. // is complete
  472. idleSet := &SummaryAllocationSet{
  473. Window: sas.Window.Clone(),
  474. }
  475. // shareSet will be shared among resultSet after initial aggregation
  476. // is complete
  477. shareSet := &SummaryAllocationSet{
  478. Window: sas.Window.Clone(),
  479. }
  480. sas.Lock()
  481. defer sas.Unlock()
  482. // 2. Record resource totals for shared costs, aggregating by cluster or by
  483. // node (depending on if idle is partitioned by cluster or node) so that we
  484. // can account for them in computing idle coefficients. Do the same for
  485. // unmounted volume costs, which only require a total cost.
  486. sharedResourceTotals := map[string]*AllocationTotals{}
  487. totalUnmountedCost := 0.0
  488. // 1 & 2. Identify set membership and aggregate aforementioned totals.
  489. for _, sa := range sas.SummaryAllocations {
  490. if sa.Share {
  491. var key string
  492. if options.IdleByNode {
  493. key = fmt.Sprintf("%s/%s", sa.Properties.Cluster, sa.Properties.Node)
  494. } else {
  495. key = sa.Properties.Cluster
  496. }
  497. if _, ok := sharedResourceTotals[key]; !ok {
  498. sharedResourceTotals[key] = &AllocationTotals{}
  499. }
  500. sharedResourceTotals[key].CPUCost += sa.CPUCost
  501. sharedResourceTotals[key].GPUCost += sa.GPUCost
  502. sharedResourceTotals[key].LoadBalancerCost += sa.LoadBalancerCost
  503. sharedResourceTotals[key].NetworkCost += sa.NetworkCost
  504. sharedResourceTotals[key].PersistentVolumeCost += sa.PVCost
  505. sharedResourceTotals[key].RAMCost += sa.RAMCost
  506. shareSet.Insert(sa)
  507. delete(sas.SummaryAllocations, sa.Name)
  508. continue
  509. }
  510. // External allocations get aggregated post-hoc (see step 6) and do
  511. // not necessarily contain complete sets of properties, so they are
  512. // moved to a separate AllocationSet.
  513. if sa.IsExternal() {
  514. delete(sas.externalKeys, sa.Name)
  515. delete(sas.SummaryAllocations, sa.Name)
  516. externalSet.Insert(sa)
  517. continue
  518. }
  519. // Idle allocations should be separated into idleSet if they are to be
  520. // shared later on. If they are not to be shared, then add them to the
  521. // resultSet like any other allocation.
  522. if sa.IsIdle() {
  523. delete(sas.idleKeys, sa.Name)
  524. delete(sas.SummaryAllocations, sa.Name)
  525. if options.ShareIdle == ShareEven || options.ShareIdle == ShareWeighted {
  526. idleSet.Insert(sa)
  527. } else {
  528. resultSet.Insert(sa)
  529. }
  530. continue
  531. }
  532. // Track total unmounted cost because it must be taken out of total
  533. // allocated costs for sharing coefficients.
  534. if sa.IsUnmounted() {
  535. totalUnmountedCost += sa.TotalCost()
  536. }
  537. }
  538. // It's possible that no more un-shared, non-idle, non-external allocations
  539. // remain at this point. This always results in an emptySet, so return early.
  540. if len(sas.SummaryAllocations) == 0 {
  541. sas.SummaryAllocations = map[string]*SummaryAllocation{}
  542. return nil
  543. }
  544. // 3. Retrieve pre-computed allocation resource totals, which will be used
  545. // to compute idle coefficients, based on the ratio of an allocation's per-
  546. // resource cost to the per-resource totals of that allocation's cluster or
  547. // node. Whether to perform this operation based on cluster or node is an
  548. // option. (See IdleByNode documentation; defaults to idle-by-cluster.)
  549. var allocTotals map[string]*AllocationTotals
  550. var ok bool
  551. if options.AllocationTotalsStore != nil {
  552. if options.IdleByNode {
  553. allocTotals, ok = options.AllocationTotalsStore.GetAllocationTotalsByNode(*sas.Window.Start(), *sas.Window.End())
  554. if !ok {
  555. return fmt.Errorf("nil allocation resource totals by node for %s", sas.Window)
  556. }
  557. } else {
  558. allocTotals, ok = options.AllocationTotalsStore.GetAllocationTotalsByCluster(*sas.Window.Start(), *sas.Window.End())
  559. if !ok {
  560. return fmt.Errorf("nil allocation resource totals by cluster for %s", sas.Window)
  561. }
  562. }
  563. }
  564. // If reconciliation has been fully or partially disabled, clear the
  565. // relevant adjustments from the alloc totals
  566. if allocTotals != nil && (!options.Reconcile || !options.ReconcileNetwork) {
  567. if !options.Reconcile {
  568. for _, tot := range allocTotals {
  569. tot.ClearAdjustments()
  570. }
  571. } else if !options.ReconcileNetwork {
  572. for _, tot := range allocTotals {
  573. tot.NetworkCostAdjustment = 0.0
  574. }
  575. }
  576. }
  577. // If filters have been applied, then we need to record allocation resource
  578. // totals after filtration (i.e. the allocations that are present) so that
  579. // we can identify the proportion of idle cost to keep. That is, we should
  580. // only return the idle cost that would be shared with the remaining
  581. // allocations, even if we're keeping idle separate. The totals should be
  582. // recorded by idle-key (cluster or node, depending on the IdleByNode
  583. // option). Instantiating this map is a signal to record the totals.
  584. var allocTotalsAfterFilters map[string]*AllocationTotals
  585. if len(resultSet.idleKeys) > 0 && options.Filter != nil {
  586. allocTotalsAfterFilters = make(map[string]*AllocationTotals, len(resultSet.idleKeys))
  587. }
  588. // If we're recording allocTotalsAfterFilters and there are shared costs,
  589. // then record those resource totals here so that idle for those shared
  590. // resources gets included.
  591. if allocTotalsAfterFilters != nil {
  592. for key, rt := range sharedResourceTotals {
  593. if _, ok := allocTotalsAfterFilters[key]; !ok {
  594. allocTotalsAfterFilters[key] = &AllocationTotals{}
  595. }
  596. // Record only those fields required for computing idle
  597. allocTotalsAfterFilters[key].CPUCost += rt.CPUCost
  598. allocTotalsAfterFilters[key].GPUCost += rt.GPUCost
  599. allocTotalsAfterFilters[key].RAMCost += rt.RAMCost
  600. }
  601. }
  602. // 4. Convert shared hourly cost into a cumulative allocation to share,
  603. // and insert it into the share set.
  604. for name, cost := range options.SharedHourlyCosts {
  605. if cost > 0.0 {
  606. hours := sas.Window.Hours()
  607. // If set ends in the future, adjust hours accordingly
  608. diff := time.Since(*sas.Window.End())
  609. if diff < 0.0 {
  610. hours += diff.Hours()
  611. }
  612. totalSharedCost := cost * hours
  613. shareSet.Insert(&SummaryAllocation{
  614. Name: fmt.Sprintf("%s/%s", name, SharedSuffix),
  615. Properties: &AllocationProperties{},
  616. Start: *sas.Window.Start(),
  617. End: *sas.Window.End(),
  618. SharedCost: totalSharedCost,
  619. })
  620. }
  621. }
  622. // Sharing coefficients are recorded by post-aggregation-key (e.g. if
  623. // aggregating by namespace, then the key will be the namespace) and only
  624. // need to be recorded if there are shared resources. Instantiating this
  625. // map is the signal to record sharing coefficients.
  626. var sharingCoeffs map[string]float64
  627. if len(shareSet.SummaryAllocations) > 0 {
  628. sharingCoeffs = map[string]float64{}
  629. }
  630. // Loop over all remaining SummaryAllocations (after filters, sharing, &c.)
  631. // doing the following, in this order:
  632. // 5. Compute sharing coefficients, if there are shared resources
  633. // 6. Distribute idle cost, if sharing idle
  634. // 7. Record allocTotalsAfterFiltration, if filters have been applied
  635. // 8. Aggregate by key
  636. for _, sa := range sas.SummaryAllocations {
  637. // Generate key to use for aggregation-by-key and allocation name
  638. key := sa.generateKey(aggregateBy, options.LabelConfig)
  639. // 5. Incrementally add to sharing coefficients before adding idle
  640. // cost, which would skew the coefficients. These coefficients will be
  641. // later divided by a total, turning them into a coefficient between
  642. // 0.0 and 1.0.
  643. // NOTE: SummaryAllocation does not support ShareEven, so only record
  644. // by cost for cost-weighted distribution.
  645. if sharingCoeffs != nil {
  646. sharingCoeffs[key] += sa.TotalCost() - sa.SharedCost
  647. }
  648. // 6. Distribute idle allocations according to the idle coefficients.
  649. // NOTE: if idle allocation is off (i.e. options.ShareIdle: ShareNone)
  650. // then all idle allocations will be in the resultSet at this point, so
  651. // idleSet will be empty and we won't enter this block.
  652. if len(idleSet.SummaryAllocations) > 0 {
  653. for _, idle := range idleSet.SummaryAllocations {
  654. // Idle key is either cluster or node, as determined by the
  655. // IdleByNode option.
  656. var key string
  657. // Only share idle allocation with current allocation (sa) if
  658. // the relevant properties match (i.e. cluster and/or node)
  659. if idle.Properties.Cluster != sa.Properties.Cluster {
  660. continue
  661. }
  662. key = idle.Properties.Cluster
  663. if options.IdleByNode {
  664. if idle.Properties.Node != sa.Properties.Node {
  665. continue
  666. }
  667. key = fmt.Sprintf("%s/%s", idle.Properties.Cluster, idle.Properties.Node)
  668. }
  669. cpuCoeff, gpuCoeff, ramCoeff := ComputeIdleCoefficients(options.ShareIdle, key, sa.CPUCost, sa.GPUCost, sa.RAMCost, allocTotals)
  670. sa.CPUCost += idle.CPUCost * cpuCoeff
  671. sa.GPUCost += idle.GPUCost * gpuCoeff
  672. sa.RAMCost += idle.RAMCost * ramCoeff
  673. }
  674. }
  675. // The key becomes the allocation's name, which is used as the key by
  676. // which the allocation is inserted into the set.
  677. sa.Name = key
  678. // If merging unallocated allocations, rename all unallocated
  679. // allocations as simply __unallocated__
  680. if options.MergeUnallocated && sa.IsUnallocated() {
  681. sa.Name = UnallocatedSuffix
  682. }
  683. // 7. Record filtered resource totals for idle allocation filtration,
  684. // only if necessary.
  685. if allocTotalsAfterFilters != nil {
  686. key := sa.Properties.Cluster
  687. if options.IdleByNode {
  688. key = fmt.Sprintf("%s/%s", sa.Properties.Cluster, sa.Properties.Node)
  689. }
  690. if _, ok := allocTotalsAfterFilters[key]; !ok {
  691. allocTotalsAfterFilters[key] = &AllocationTotals{}
  692. }
  693. allocTotalsAfterFilters[key].CPUCost += sa.CPUCost
  694. allocTotalsAfterFilters[key].GPUCost += sa.GPUCost
  695. allocTotalsAfterFilters[key].RAMCost += sa.RAMCost
  696. }
  697. // 8. Inserting the allocation with the generated key for a name
  698. // performs the actual aggregation step.
  699. resultSet.Insert(sa)
  700. }
  701. // 9. If idle is shared and resources are shared, it's probable that some
  702. // amount of idle cost will be shared with a shared resource. Distribute
  703. // that idle cost, if it exists, among the respective shared allocations
  704. // before sharing them with the aggregated allocations.
  705. if len(idleSet.SummaryAllocations) > 0 && len(shareSet.SummaryAllocations) > 0 {
  706. for _, sa := range shareSet.SummaryAllocations {
  707. for _, idle := range idleSet.SummaryAllocations {
  708. var key string
  709. // Only share idle allocation with current allocation (sa) if
  710. // the relevant property matches (i.e. Cluster or Node,
  711. // depending on which idle sharing option is selected)
  712. if options.IdleByNode {
  713. if idle.Properties.Cluster != sa.Properties.Cluster || idle.Properties.Node != sa.Properties.Node {
  714. continue
  715. }
  716. key = fmt.Sprintf("%s/%s", idle.Properties.Cluster, idle.Properties.Node)
  717. } else {
  718. if idle.Properties.Cluster != sa.Properties.Cluster {
  719. continue
  720. }
  721. key = idle.Properties.Cluster
  722. }
  723. cpuCoeff, gpuCoeff, ramCoeff := ComputeIdleCoefficients(options.ShareIdle, key, sa.CPUCost, sa.GPUCost, sa.RAMCost, allocTotals)
  724. sa.CPUCost += idle.CPUCost * cpuCoeff
  725. sa.GPUCost += idle.GPUCost * gpuCoeff
  726. sa.RAMCost += idle.RAMCost * ramCoeff
  727. }
  728. }
  729. }
  730. // 10. Apply idle filtration, which "filters" the idle cost, i.e. scales
  731. // idle allocation costs per-resource by the proportion of allocation
  732. // resources remaining after filtering. In effect, this returns only the
  733. // idle costs that would have been shared with the remaining allocations,
  734. // even if idle is kept separated.
  735. if allocTotalsAfterFilters != nil {
  736. for idleKey := range resultSet.idleKeys {
  737. ia := resultSet.SummaryAllocations[idleKey]
  738. var key string
  739. if options.IdleByNode {
  740. key = fmt.Sprintf("%s/%s", ia.Properties.Cluster, ia.Properties.Node)
  741. } else {
  742. key = ia.Properties.Cluster
  743. }
  744. // Percentage of idle that should remain after filters are applied,
  745. // which equals the proportion of filtered-to-actual cost.
  746. cpuFilterCoeff := 0.0
  747. if allocTotals[key].TotalCPUCost() > 0.0 {
  748. filteredAlloc, ok := allocTotalsAfterFilters[key]
  749. if ok {
  750. cpuFilterCoeff = filteredAlloc.TotalCPUCost() / allocTotals[key].TotalCPUCost()
  751. } else {
  752. cpuFilterCoeff = 0.0
  753. }
  754. }
  755. gpuFilterCoeff := 0.0
  756. if allocTotals[key].TotalGPUCost() > 0.0 {
  757. filteredAlloc, ok := allocTotalsAfterFilters[key]
  758. if ok {
  759. gpuFilterCoeff = filteredAlloc.TotalGPUCost() / allocTotals[key].TotalGPUCost()
  760. } else {
  761. gpuFilterCoeff = 0.0
  762. }
  763. }
  764. ramFilterCoeff := 0.0
  765. if allocTotals[key].TotalRAMCost() > 0.0 {
  766. filteredAlloc, ok := allocTotalsAfterFilters[key]
  767. if ok {
  768. ramFilterCoeff = filteredAlloc.TotalRAMCost() / allocTotals[key].TotalRAMCost()
  769. } else {
  770. ramFilterCoeff = 0.0
  771. }
  772. }
  773. ia.CPUCost *= cpuFilterCoeff
  774. ia.GPUCost *= gpuFilterCoeff
  775. ia.RAMCost *= ramFilterCoeff
  776. }
  777. }
  778. // 11. Distribute shared resources according to sharing coefficients.
  779. // NOTE: ShareEven is not supported
  780. if len(shareSet.SummaryAllocations) > 0 {
  781. sharingCoeffDenominator := 0.0
  782. for _, rt := range allocTotals {
  783. sharingCoeffDenominator += rt.TotalCost()
  784. }
  785. // Do not include the shared costs, themselves, when determining
  786. // sharing coefficients.
  787. for _, rt := range sharedResourceTotals {
  788. sharingCoeffDenominator -= rt.TotalCost()
  789. }
  790. // Do not include the unmounted costs when determining sharing
  791. // coefficients becuase they do not receive shared costs.
  792. sharingCoeffDenominator -= totalUnmountedCost
  793. if sharingCoeffDenominator <= 0.0 {
  794. log.Warnf("SummaryAllocation: sharing coefficient denominator is %f", sharingCoeffDenominator)
  795. } else {
  796. // Compute sharing coeffs by dividing the thus-far accumulated
  797. // numerators by the now-finalized denominator.
  798. for key := range sharingCoeffs {
  799. if sharingCoeffs[key] > 0.0 {
  800. sharingCoeffs[key] /= sharingCoeffDenominator
  801. } else {
  802. log.Warnf("SummaryAllocation: detected illegal sharing coefficient for %s: %v (setting to zero)", key, sharingCoeffs[key])
  803. sharingCoeffs[key] = 0.0
  804. }
  805. }
  806. for key, sa := range resultSet.SummaryAllocations {
  807. // Idle and unmounted allocations, by definition, do not
  808. // receive shared cost
  809. if sa.IsIdle() || sa.IsUnmounted() {
  810. continue
  811. }
  812. sharingCoeff := sharingCoeffs[key]
  813. // Distribute each shared cost with the current allocation on the
  814. // basis of the proportion of the allocation's cost (ShareWeighted)
  815. // or count (ShareEven) to the total aggregated cost or count. This
  816. // condition should hold in spite of filters because the sharing
  817. // coefficient denominator is held constant by pre-computed
  818. // resource totals and the post-aggregation total cost of the
  819. // remaining allocations will, by definition, not be affected.
  820. for _, shared := range shareSet.SummaryAllocations {
  821. sa.SharedCost += shared.TotalCost() * sharingCoeff
  822. }
  823. }
  824. }
  825. }
  826. // 12. Insert external allocations into the result set.
  827. for _, sa := range externalSet.SummaryAllocations {
  828. skip := false
  829. // Make an allocation with the same properties and test that
  830. // against the FilterFunc to see if the external allocation should
  831. // be filtered or not.
  832. // TODO:CLEANUP do something about external cost, this stinks
  833. ea := &Allocation{Properties: sa.Properties}
  834. if options.Filter != nil {
  835. skip = !options.Filter.Matches(ea)
  836. }
  837. if !skip {
  838. key := sa.generateKey(aggregateBy, options.LabelConfig)
  839. sa.Name = key
  840. resultSet.Insert(sa)
  841. }
  842. }
  843. // 13. Distribute remaining, undistributed idle. Undistributed idle is any
  844. // per-resource idle cost for which there can be no idle coefficient
  845. // computed because there is zero usage across all allocations.
  846. for _, isa := range idleSet.SummaryAllocations {
  847. // if the idle does not apply to the non-filtered values, skip it
  848. skip := false
  849. // Make an allocation with the same properties and test that
  850. // against the FilterFunc to see if the external allocation should
  851. // be filtered or not.
  852. // TODO:CLEANUP do something about external cost, this stinks
  853. ia := &Allocation{Properties: isa.Properties}
  854. if options.Filter != nil {
  855. skip = !options.Filter.Matches(ia)
  856. }
  857. if skip {
  858. continue
  859. }
  860. key := isa.Properties.Cluster
  861. if options.IdleByNode {
  862. key = fmt.Sprintf("%s/%s", isa.Properties.Cluster, isa.Properties.Node)
  863. }
  864. rt, ok := allocTotals[key]
  865. if !ok {
  866. log.Warnf("SummaryAllocation: AggregateBy: cannot handle undistributed idle for '%s'", key)
  867. continue
  868. }
  869. hasUndistributableCost := false
  870. if isa.CPUCost > 0.0 && rt.CPUCost == 0.0 {
  871. // There is idle CPU cost, but no allocated CPU cost, so that cost
  872. // is undistributable and must be inserted.
  873. hasUndistributableCost = true
  874. } else {
  875. // Cost was entirely distributed, so zero it out
  876. isa.CPUCost = 0.0
  877. }
  878. if isa.GPUCost > 0.0 && rt.GPUCost == 0.0 {
  879. // There is idle GPU cost, but no allocated GPU cost, so that cost
  880. // is undistributable and must be inserted.
  881. hasUndistributableCost = true
  882. } else {
  883. // Cost was entirely distributed, so zero it out
  884. isa.GPUCost = 0.0
  885. }
  886. if isa.RAMCost > 0.0 && rt.RAMCost == 0.0 {
  887. // There is idle CPU cost, but no allocated CPU cost, so that cost
  888. // is undistributable and must be inserted.
  889. hasUndistributableCost = true
  890. } else {
  891. // Cost was entirely distributed, so zero it out
  892. isa.RAMCost = 0.0
  893. }
  894. if hasUndistributableCost {
  895. isa.Name = fmt.Sprintf("%s/%s", key, IdleSuffix)
  896. resultSet.Insert(isa)
  897. }
  898. }
  899. // 14. Combine all idle allocations into a single idle allocation, unless
  900. // the option to keep idle split by cluster or node is enabled.
  901. if !options.SplitIdle {
  902. for _, ia := range resultSet.idleAllocations() {
  903. resultSet.Delete(ia.Name)
  904. ia.Name = IdleSuffix
  905. resultSet.Insert(ia)
  906. }
  907. }
  908. // Replace the existing set's data with the new, aggregated summary data
  909. sas.SummaryAllocations = resultSet.SummaryAllocations
  910. return nil
  911. }
  912. // Delete removes the allocation with the given name from the set
  913. func (sas *SummaryAllocationSet) Delete(name string) {
  914. if sas == nil {
  915. return
  916. }
  917. sas.Lock()
  918. defer sas.Unlock()
  919. delete(sas.externalKeys, name)
  920. delete(sas.idleKeys, name)
  921. delete(sas.SummaryAllocations, name)
  922. }
  923. // Each invokes the given function for each SummaryAllocation in the set
  924. func (sas *SummaryAllocationSet) Each(f func(string, *SummaryAllocation)) {
  925. if sas == nil {
  926. return
  927. }
  928. for k, a := range sas.SummaryAllocations {
  929. f(k, a)
  930. }
  931. }
  932. // IdleAllocations returns a map of the idle allocations in the AllocationSet.
  933. func (sas *SummaryAllocationSet) idleAllocations() map[string]*SummaryAllocation {
  934. idles := map[string]*SummaryAllocation{}
  935. if sas == nil || len(sas.SummaryAllocations) == 0 {
  936. return idles
  937. }
  938. sas.RLock()
  939. defer sas.RUnlock()
  940. for key := range sas.idleKeys {
  941. if sa, ok := sas.SummaryAllocations[key]; ok {
  942. idles[key] = sa
  943. }
  944. }
  945. return idles
  946. }
  947. // Insert aggregates the current entry in the SummaryAllocationSet by the given Allocation,
  948. // but only if the Allocation is valid, i.e. matches the SummaryAllocationSet's window. If
  949. // there is no existing entry, one is created. Nil error response indicates success.
  950. func (sas *SummaryAllocationSet) Insert(sa *SummaryAllocation) error {
  951. if sas == nil {
  952. return fmt.Errorf("cannot insert into nil SummaryAllocationSet")
  953. }
  954. if sa == nil {
  955. return fmt.Errorf("cannot insert a nil SummaryAllocation")
  956. }
  957. sas.Lock()
  958. defer sas.Unlock()
  959. if sas.SummaryAllocations == nil {
  960. sas.SummaryAllocations = map[string]*SummaryAllocation{}
  961. }
  962. if sas.externalKeys == nil {
  963. sas.externalKeys = map[string]bool{}
  964. }
  965. if sas.idleKeys == nil {
  966. sas.idleKeys = map[string]bool{}
  967. }
  968. // Add the given Allocation to the existing entry, if there is one;
  969. // otherwise just set directly into allocations
  970. if _, ok := sas.SummaryAllocations[sa.Name]; ok {
  971. err := sas.SummaryAllocations[sa.Name].Add(sa)
  972. if err != nil {
  973. return fmt.Errorf("SummaryAllocationSet.Insert: error trying to Add: %s", err)
  974. }
  975. } else {
  976. sas.SummaryAllocations[sa.Name] = sa
  977. }
  978. // If the given Allocation is an external one, record that
  979. if sa.IsExternal() {
  980. sas.externalKeys[sa.Name] = true
  981. }
  982. // If the given Allocation is an idle one, record that
  983. if sa.IsIdle() {
  984. sas.idleKeys[sa.Name] = true
  985. }
  986. return nil
  987. }
  988. func (sas *SummaryAllocationSet) TotalCost() float64 {
  989. if sas == nil {
  990. return 0.0
  991. }
  992. sas.RLock()
  993. defer sas.RUnlock()
  994. tc := 0.0
  995. for _, sa := range sas.SummaryAllocations {
  996. tc += sa.TotalCost()
  997. }
  998. return tc
  999. }
  1000. // RAMEfficiency func to calculate average RAM efficiency over SummaryAllocationSet
  1001. func (sas *SummaryAllocationSet) RAMEfficiency() float64 {
  1002. if sas == nil {
  1003. return 0.0
  1004. }
  1005. sas.RLock()
  1006. defer sas.RUnlock()
  1007. totalRAMBytesMinutesUsage := 0.0
  1008. totalRAMBytesMinutesRequest := 0.0
  1009. totalRAMCost := 0.0
  1010. for _, sa := range sas.SummaryAllocations {
  1011. if sa.IsIdle() {
  1012. continue
  1013. }
  1014. totalRAMBytesMinutesUsage += sa.RAMBytesUsageAverage * sa.Minutes()
  1015. totalRAMBytesMinutesRequest += sa.RAMBytesRequestAverage * sa.Minutes()
  1016. totalRAMCost += sa.RAMCost
  1017. }
  1018. if totalRAMBytesMinutesRequest > 0 {
  1019. return totalRAMBytesMinutesUsage / totalRAMBytesMinutesRequest
  1020. }
  1021. if totalRAMBytesMinutesUsage == 0.0 || totalRAMCost == 0.0 {
  1022. return 0.0
  1023. }
  1024. return 1.0
  1025. }
  1026. // CPUEfficiency func to calculate average CPU efficiency over SummaryAllocationSet
  1027. func (sas *SummaryAllocationSet) CPUEfficiency() float64 {
  1028. if sas == nil {
  1029. return 0.0
  1030. }
  1031. sas.RLock()
  1032. defer sas.RUnlock()
  1033. totalCPUCoreMinutesUsage := 0.0
  1034. totalCPUCoreMinutesRequest := 0.0
  1035. totalCPUCost := 0.0
  1036. for _, sa := range sas.SummaryAllocations {
  1037. if sa.IsIdle() {
  1038. continue
  1039. }
  1040. totalCPUCoreMinutesUsage += sa.CPUCoreUsageAverage * sa.Minutes()
  1041. totalCPUCoreMinutesRequest += sa.CPUCoreRequestAverage * sa.Minutes()
  1042. totalCPUCost += sa.CPUCost
  1043. }
  1044. if totalCPUCoreMinutesRequest > 0 {
  1045. return totalCPUCoreMinutesUsage / totalCPUCoreMinutesRequest
  1046. }
  1047. if totalCPUCoreMinutesUsage == 0.0 || totalCPUCost == 0.0 {
  1048. return 0.0
  1049. }
  1050. return 1.0
  1051. }
  1052. // TotalEfficiency func to calculate average Total efficiency over SummaryAllocationSet
  1053. func (sas *SummaryAllocationSet) TotalEfficiency() float64 {
  1054. if sas == nil {
  1055. return 0.0
  1056. }
  1057. sas.RLock()
  1058. defer sas.RUnlock()
  1059. totalRAMCost := 0.0
  1060. totalCPUCost := 0.0
  1061. for _, sa := range sas.SummaryAllocations {
  1062. if sa.IsIdle() {
  1063. continue
  1064. }
  1065. totalRAMCost += sa.RAMCost
  1066. totalCPUCost += sa.CPUCost
  1067. }
  1068. if totalRAMCost+totalCPUCost > 0 {
  1069. return (totalRAMCost*sas.RAMEfficiency() + totalCPUCost*sas.CPUEfficiency()) / (totalRAMCost + totalCPUCost)
  1070. }
  1071. return 0.0
  1072. }
  1073. // SummaryAllocationSetRange is a thread-safe slice of SummaryAllocationSets.
  1074. type SummaryAllocationSetRange struct {
  1075. sync.RWMutex
  1076. Step time.Duration `json:"step"`
  1077. SummaryAllocationSets []*SummaryAllocationSet `json:"sets"`
  1078. Window Window `json:"window"`
  1079. Message string `json:"-"`
  1080. }
  1081. // NewSummaryAllocationSetRange instantiates a new range composed of the given
  1082. // SummaryAllocationSets in the order provided. The expectations about the
  1083. // SummaryAllocationSets are as follows:
  1084. // - window durations are all equal
  1085. // - sets are consecutive (i.e. chronologically sorted)
  1086. // - there are no gaps between sets
  1087. // - sets do not have overlapping windows
  1088. func NewSummaryAllocationSetRange(sass ...*SummaryAllocationSet) *SummaryAllocationSetRange {
  1089. var step time.Duration
  1090. window := NewWindow(nil, nil)
  1091. for _, sas := range sass {
  1092. if window.Start() == nil || (sas.Window.Start() != nil && sas.Window.Start().Before(*window.Start())) {
  1093. window.start = sas.Window.Start()
  1094. }
  1095. if window.End() == nil || (sas.Window.End() != nil && sas.Window.End().After(*window.End())) {
  1096. window.end = sas.Window.End()
  1097. }
  1098. if step == 0 {
  1099. step = sas.Window.Duration()
  1100. } else if step != sas.Window.Duration() {
  1101. log.Warnf("instantiating range with step %s using set of step %s is illegal", step, sas.Window.Duration())
  1102. }
  1103. }
  1104. return &SummaryAllocationSetRange{
  1105. Step: step,
  1106. SummaryAllocationSets: sass,
  1107. Window: window,
  1108. }
  1109. }
  1110. // Accumulate sums each AllocationSet in the given range, returning a single cumulative
  1111. // AllocationSet for the entire range.
  1112. func (sasr *SummaryAllocationSetRange) Accumulate() (*SummaryAllocationSet, error) {
  1113. var result *SummaryAllocationSet
  1114. var err error
  1115. sasr.RLock()
  1116. defer sasr.RUnlock()
  1117. for _, sas := range sasr.SummaryAllocationSets {
  1118. result, err = result.Add(sas)
  1119. if err != nil {
  1120. return nil, err
  1121. }
  1122. }
  1123. return result, nil
  1124. }
  1125. // NewAccumulation clones the first available SummaryAllocationSet to use as the data structure to
  1126. // accumulate the remaining data. This leaves the original SummaryAllocationSetRange intact.
  1127. func (sasr *SummaryAllocationSetRange) NewAccumulation() (*SummaryAllocationSet, error) {
  1128. var result *SummaryAllocationSet
  1129. var err error
  1130. sasr.RLock()
  1131. defer sasr.RUnlock()
  1132. for _, sas := range sasr.SummaryAllocationSets {
  1133. // we want to clone the first summary allocation set, then just Add the others
  1134. // to the clone. We will eventually use the clone to create the set range.
  1135. if result == nil {
  1136. result = sas.Clone()
  1137. continue
  1138. }
  1139. // Copy if sas is non-nil
  1140. var sasCopy *SummaryAllocationSet = nil
  1141. if sas != nil {
  1142. sasCopy = sas.Clone()
  1143. }
  1144. // nil is ok to pass into Add
  1145. result, err = result.Add(sasCopy)
  1146. if err != nil {
  1147. return nil, err
  1148. }
  1149. }
  1150. return result, nil
  1151. }
  1152. // AggregateBy aggregates each AllocationSet in the range by the given
  1153. // properties and options.
  1154. func (sasr *SummaryAllocationSetRange) AggregateBy(aggregateBy []string, options *AllocationAggregationOptions) error {
  1155. sasr.Lock()
  1156. defer sasr.Unlock()
  1157. for _, sas := range sasr.SummaryAllocationSets {
  1158. err := sas.AggregateBy(aggregateBy, options)
  1159. if err != nil {
  1160. // Wipe out data so that corrupt data cannot be mistakenly used
  1161. sasr.SummaryAllocationSets = []*SummaryAllocationSet{}
  1162. return err
  1163. }
  1164. }
  1165. return nil
  1166. }
  1167. // Append appends the given AllocationSet to the end of the range. It does not
  1168. // validate whether or not that violates window continuity.
  1169. func (sasr *SummaryAllocationSetRange) Append(sas *SummaryAllocationSet) error {
  1170. if sasr.Step != 0 && sas.Window.Duration() != sasr.Step {
  1171. return fmt.Errorf("cannot append set with duration %s to range of step %s", sas.Window.Duration(), sasr.Step)
  1172. }
  1173. sasr.Lock()
  1174. defer sasr.Unlock()
  1175. // Append to list of sets
  1176. sasr.SummaryAllocationSets = append(sasr.SummaryAllocationSets, sas)
  1177. // Set step, if not set
  1178. if sasr.Step == 0 {
  1179. sasr.Step = sas.Window.Duration()
  1180. }
  1181. // Adjust window
  1182. if sasr.Window.Start() == nil || (sas.Window.Start() != nil && sas.Window.Start().Before(*sasr.Window.Start())) {
  1183. sasr.Window.start = sas.Window.Start()
  1184. }
  1185. if sasr.Window.End() == nil || (sas.Window.End() != nil && sas.Window.End().After(*sasr.Window.End())) {
  1186. sasr.Window.end = sas.Window.End()
  1187. }
  1188. return nil
  1189. }
  1190. // Each invokes the given function for each AllocationSet in the range
  1191. func (sasr *SummaryAllocationSetRange) Each(f func(int, *SummaryAllocationSet)) {
  1192. if sasr == nil {
  1193. return
  1194. }
  1195. for i, as := range sasr.SummaryAllocationSets {
  1196. f(i, as)
  1197. }
  1198. }
  1199. // InsertExternalAllocations takes all allocations in the given
  1200. // AllocationSetRange (they should all be considered "external") and inserts
  1201. // them into the receiving SummaryAllocationSetRange.
  1202. // TODO:CLEANUP replace this with a better idea (or get rid of external
  1203. // allocations, as such, altogether)
  1204. func (sasr *SummaryAllocationSetRange) InsertExternalAllocations(that *AllocationSetRange) error {
  1205. if sasr == nil {
  1206. return fmt.Errorf("cannot insert range into nil AllocationSetRange")
  1207. }
  1208. // keys maps window to index in range
  1209. keys := map[string]int{}
  1210. for i, as := range sasr.SummaryAllocationSets {
  1211. if as == nil {
  1212. continue
  1213. }
  1214. keys[as.Window.String()] = i
  1215. }
  1216. // Nothing to merge, so simply return
  1217. if len(keys) == 0 {
  1218. return nil
  1219. }
  1220. var err error
  1221. for _, thatAS := range that.Allocations {
  1222. if thatAS == nil || err != nil {
  1223. continue
  1224. }
  1225. // Find matching AllocationSet in asr
  1226. i, ok := keys[thatAS.Window.String()]
  1227. if !ok {
  1228. err = fmt.Errorf("cannot merge AllocationSet into window that does not exist: %s", thatAS.Window.String())
  1229. continue
  1230. }
  1231. sas := sasr.SummaryAllocationSets[i]
  1232. // Insert each Allocation from the given set
  1233. for _, alloc := range thatAS.Allocations {
  1234. externalSA := NewSummaryAllocation(alloc, true, true)
  1235. // This error will be returned below
  1236. // TODO:CLEANUP should Each have early-error-return functionality?
  1237. err = sas.Insert(externalSA)
  1238. }
  1239. }
  1240. // err might be nil
  1241. return err
  1242. }
  1243. func (sasr *SummaryAllocationSetRange) TotalCost() float64 {
  1244. if sasr == nil {
  1245. return 0.0
  1246. }
  1247. sasr.RLock()
  1248. defer sasr.RUnlock()
  1249. tc := 0.0
  1250. for _, sas := range sasr.SummaryAllocationSets {
  1251. tc += sas.TotalCost()
  1252. }
  1253. return tc
  1254. }
  1255. // TODO remove after testing
  1256. func (sasr *SummaryAllocationSetRange) Print(verbose bool) {
  1257. fmt.Printf("%s (dur=%s, len=%d, cost=%.5f)\n", sasr.Window, sasr.Window.Duration(), len(sasr.SummaryAllocationSets), sasr.TotalCost())
  1258. for _, sas := range sasr.SummaryAllocationSets {
  1259. fmt.Printf(" > %s (dur=%s, len=%d, cost=%.5f) \n", sas.Window, sas.Window.Duration(), len(sas.SummaryAllocations), sas.TotalCost())
  1260. for key, sa := range sas.SummaryAllocations {
  1261. if verbose {
  1262. fmt.Printf(" {\"%s\", cpu: %.5f, gpu: %.5f, lb: %.5f, net: %.5f, pv: %.5f, ram: %.5f, shared: %.5f, external: %.5f}\n",
  1263. key, sa.CPUCost, sa.GPUCost, sa.LoadBalancerCost, sa.NetworkCost, sa.PVCost, sa.RAMCost, sa.SharedCost, sa.ExternalCost)
  1264. } else {
  1265. fmt.Printf(" - \"%s\": %.5f\n", key, sa.TotalCost())
  1266. }
  1267. }
  1268. }
  1269. }