github.com/kubewharf/katalyst-core@v0.5.3/pkg/util/general/window.go (about) 1 /* 2 Copyright 2022 The Katalyst Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package general 18 19 import ( 20 "math" 21 "sort" 22 "strconv" 23 "sync" 24 "time" 25 26 "k8s.io/apimachinery/pkg/api/resource" 27 ) 28 29 const ( 30 SmoothWindowAggFuncAvg = "average" 31 SmoothWindowAggFuncPerc = "percentile" 32 ) 33 34 // SmoothWindow is used to smooth the resource 35 type SmoothWindow interface { 36 // GetWindowedResources receives a sample and returns the result after smoothing, 37 // it can return nil if there are not enough samples in this window 38 GetWindowedResources(value resource.Quantity) *resource.Quantity 39 40 Empty() bool 41 } 42 43 type SmoothWindowOpts struct { 44 WindowSize int 45 TTL time.Duration 46 UsedMillValue bool 47 AggregateFunc string 48 AggregateArgs string 49 } 50 51 type CappedSmoothWindow struct { 52 sync.Mutex 53 last *resource.Quantity 54 minStep resource.Quantity 55 maxStep resource.Quantity 56 SmoothWindow 57 } 58 59 // NewCappedSmoothWindow creates a capped SmoothWindow, which 60 func NewCappedSmoothWindow(minStep resource.Quantity, maxStep resource.Quantity, smoothWindow SmoothWindow) *CappedSmoothWindow { 61 return &CappedSmoothWindow{minStep: minStep, maxStep: maxStep, SmoothWindow: smoothWindow} 62 } 63 64 // GetWindowedResources cap the value return by smooth window min to max 65 func (m *CappedSmoothWindow) GetWindowedResources(value resource.Quantity) *resource.Quantity { 66 m.Lock() 67 defer m.Unlock() 68 69 cur := m.SmoothWindow.GetWindowedResources(value) 70 if cur == nil { 71 cur = m.last 72 } else if m.last == nil { 73 m.last = cur 74 } else if cur.Cmp(*m.last) > 0 { 75 step := cur.DeepCopy() 76 step.Sub(*m.last) 77 if step.Cmp(m.minStep) < 0 { 78 cur = m.last 79 } else if step.Cmp(m.maxStep) > 0 { 80 m.last.Add(m.maxStep) 81 cur = m.last 82 } else { 83 m.last = cur 84 } 85 } else { 86 step := m.last.DeepCopy() 87 step.Sub(*cur) 88 if step.Cmp(m.minStep) < 0 { 89 cur = m.last 90 } else if step.Cmp(m.maxStep) > 0 { 91 m.last.Sub(m.maxStep) 92 cur = m.last 93 } else { 94 m.last = cur 95 } 96 } 97 98 if cur == nil { 99 return nil 100 } 101 102 ret := cur.DeepCopy() 103 return &ret 104 } 105 106 type TTLSmoothWindow struct { 107 sync.RWMutex 108 windowSize int 109 ttl time.Duration 110 usedMillValue bool 111 112 index int 113 samples []*sample 114 } 115 116 func (w *TTLSmoothWindow) getValidSamples() []resource.Quantity { 117 w.RWMutex.RLock() 118 defer w.RWMutex.RUnlock() 119 120 timestamp := time.Now() 121 122 validSamples := make([]resource.Quantity, 0) 123 for _, s := range w.samples { 124 if s != nil && s.timestamp.Add(w.ttl).After(timestamp) { 125 validSamples = append(validSamples, s.value) 126 } 127 } 128 return validSamples 129 } 130 131 func (w *TTLSmoothWindow) pushSample(value resource.Quantity) { 132 w.RWMutex.Lock() 133 defer w.RWMutex.Unlock() 134 135 timestamp := time.Now() 136 w.samples[w.index] = &sample{ 137 value: value, 138 timestamp: timestamp, 139 } 140 141 w.index++ 142 if w.index >= w.windowSize { 143 w.index = 0 144 } 145 } 146 147 func (w *TTLSmoothWindow) Empty() bool { 148 validSamples := w.getValidSamples() 149 return len(validSamples) == 0 150 } 151 152 func NewTTLSmoothWindow(windowSize int, ttl time.Duration, usedMillValue bool) *TTLSmoothWindow { 153 return &TTLSmoothWindow{ 154 windowSize: windowSize, 155 ttl: ttl, 156 usedMillValue: usedMillValue, 157 index: 0, 158 samples: make([]*sample, windowSize), 159 } 160 } 161 162 type averageWithTTLSmoothWindow struct { 163 *TTLSmoothWindow 164 } 165 166 func (w *averageWithTTLSmoothWindow) getValueByAvg(values []resource.Quantity) resource.Quantity { 167 count := 0 168 total := resource.Quantity{} 169 170 for _, value := range values { 171 count++ 172 total.Add(value) 173 } 174 175 avg := total.AsApproximateFloat64() / float64(count) 176 return *resource.NewMilliQuantity(int64(avg*1000), resource.DecimalSI) 177 } 178 179 type sample struct { 180 value resource.Quantity 181 timestamp time.Time 182 } 183 184 func NewAggregatorSmoothWindow(opts SmoothWindowOpts) SmoothWindow { 185 switch opts.AggregateFunc { 186 case SmoothWindowAggFuncAvg: 187 return NewAverageWithTTLSmoothWindow(opts.WindowSize, opts.TTL, opts.UsedMillValue) 188 case SmoothWindowAggFuncPerc: 189 perc, err := strconv.ParseFloat(opts.AggregateArgs, 64) 190 if err != nil { 191 Errorf("failed to parse AggregateArgs %v, fallback to default aggregator", opts.AggregateFunc) 192 } else { 193 return NewPercentileWithTTLSmoothWindow(opts.WindowSize, opts.TTL, perc, opts.UsedMillValue) 194 } 195 } 196 return NewAverageWithTTLSmoothWindow(opts.WindowSize, opts.TTL, opts.UsedMillValue) 197 } 198 199 // NewAverageWithTTLSmoothWindow create a smooth window with ttl and window size, and the window size 200 // is the sample count while the ttl is the valid lifetime of each sample, and the usedMillValue means 201 // whether calculate the result with milli-value. 202 func NewAverageWithTTLSmoothWindow(windowSize int, ttl time.Duration, usedMillValue bool) SmoothWindow { 203 return &averageWithTTLSmoothWindow{ 204 TTLSmoothWindow: NewTTLSmoothWindow(windowSize, ttl, usedMillValue), 205 } 206 } 207 208 // GetWindowedResources inserts a sample, and returns the smoothed result by average all the valid samples. 209 func (w *averageWithTTLSmoothWindow) GetWindowedResources(value resource.Quantity) *resource.Quantity { 210 w.pushSample(value) 211 validSamples := w.getValidSamples() 212 213 // if count of valid sample is not enough just return nil 214 if len(validSamples) != w.windowSize { 215 return nil 216 } 217 218 v := w.getValueByAvg(validSamples) 219 220 if w.usedMillValue { 221 return resource.NewMilliQuantity(v.MilliValue(), value.Format) 222 } 223 224 return resource.NewQuantity(v.Value(), value.Format) 225 } 226 227 type percentileWithTTLSmoothWindow struct { 228 *TTLSmoothWindow 229 230 percentile float64 231 } 232 233 // NewPercentileWithTTLSmoothWindow create a smooth window with ttl and window size, and the window size 234 // is the sample count while the ttl is the valid lifetime of each sample, and the usedMillValue means 235 // whether calculate the result with milli-value. 236 func NewPercentileWithTTLSmoothWindow(windowSize int, ttl time.Duration, percentile float64, usedMillValue bool) SmoothWindow { 237 return &percentileWithTTLSmoothWindow{ 238 TTLSmoothWindow: NewTTLSmoothWindow(windowSize, ttl, usedMillValue), 239 percentile: percentile, 240 } 241 } 242 243 // GetWindowedResources inserts a sample, and returns the smoothed result by average all the valid samples. 244 func (w *percentileWithTTLSmoothWindow) GetWindowedResources(value resource.Quantity) *resource.Quantity { 245 w.pushSample(value) 246 validSamples := w.getValidSamples() 247 248 // if count of valid sample is not enough just return nil 249 if len(validSamples) != w.windowSize { 250 return nil 251 } 252 253 v := w.getValueByPercentile(validSamples, w.percentile) 254 255 if w.usedMillValue { 256 return resource.NewMilliQuantity(v.MilliValue(), value.Format) 257 } 258 259 return resource.NewQuantity(v.Value(), value.Format) 260 } 261 262 func (w *percentileWithTTLSmoothWindow) getValueByPercentile(values []resource.Quantity, percentile float64) resource.Quantity { 263 sort.Slice(values, func(i, j int) bool { 264 return values[i].Cmp(values[j]) < 0 265 }) 266 267 percentileIndex := int(math.Ceil(float64(len(values))*percentile/100.0) - 1) 268 if percentileIndex < 0 { 269 percentileIndex = 0 270 } else if percentileIndex >= len(values) { 271 percentileIndex = len(values) - 1 272 } 273 return values[percentileIndex] 274 }