summaryallocation.go 58 KB

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