github.com/mrgossett/heapster@v0.18.2/store/statstore/stat_store.go (about) 1 // Copyright 2015 Google Inc. All Rights Reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package statstore 16 17 import ( 18 "container/list" 19 "fmt" 20 "math" 21 "sort" 22 "sync" 23 "time" 24 ) 25 26 // StatStore is an in-memory rolling timeseries window that allows the extraction of - 27 // segments of the stored timeseries and derived stats. 28 // StatStore only retains information about the latest TimePoints that are within its 29 // specified duration. 30 31 // The tradeoff between space and precision can be configured through the epsilon and resolution - 32 // parameters. 33 // @epsilon: the acceptable error margin, in absolute value, such as 1024 bytes. 34 // @resolution: the desired resolution of the StatStore, such as 2 * time.Minute 35 36 // Compression in the StatStore is performed by storing values of consecutive time resolutions - 37 // that differ less than epsilon in the same "bucket", represented by the tpBucket struct. 38 39 // For example, a timeseries may be represented in the following manner. 40 // Each line parallel to the x-axis represents a single tpBucket. 41 // This example leads to 4 tpBuckets being stored in the StatStore, even though the length of the 42 // window is 7 resolutions. 43 // 44 // Legend: 45 // ε : epsilon 46 // δ : resolution 47 // W : window length 48 // 49 // value ^ 50 // | 51 // | 52 // 4ε | ---- 53 // | | | 54 // 2ε |--------| | |----| 55 // ε | |------------| 56 // |_____________________________|______> 57 // W time 58 // |--------|--|------------|----| 59 // 2δ δ 3δ δ 60 61 // StatStore assumes that values are inserted in a chronologically ascending order through the - 62 // Put method. If a TimePoint with a past Timestamp is inserted, it is ignored. 63 // The last one resolution's worth of Timepoints are held in a putState structure, as - 64 // we are not confident of that resolution's average until values from the next resolution have - 65 // arrived. Due to this assumption, the data extraction methods of the StatStore ignore values - 66 // currently in the putState struct. 67 68 func NewStatStore(epsilon uint64, resolution time.Duration, windowDuration uint, supportedPercentiles []float64) *StatStore { 69 return &StatStore{ 70 buffer: list.New(), 71 epsilon: epsilon, 72 resolution: resolution, 73 windowDuration: windowDuration, 74 supportedPercentiles: supportedPercentiles, 75 } 76 } 77 78 // TimePoint is a single point of a timeseries, representing a time-value pair. 79 type TimePoint struct { 80 Timestamp time.Time 81 Value uint64 82 } 83 84 // StatStore is a TimeStore-like object that does not implement the TimeStore interface. 85 type StatStore struct { 86 // start is the start of the represented time window. 87 start time.Time 88 89 // buffer is a list of tpBucket that is sequenced in a time-descending order, meaning that 90 // Front points to the latest tpBucket and Back to the oldest one. 91 buffer *list.List 92 93 // A RWMutex guards all operations on the StatStore. 94 sync.RWMutex 95 96 // epsilon is the acceptable error difference for the storage of TimePoints. 97 // Increasing epsilon decreases memory usage of the StatStore, at the cost of precision. 98 // The precision of max is not affected by epsilon. 99 epsilon uint64 100 101 // resolution is the standardized duration between points in the StatStore. 102 // the Get operation returns TimePoints at every multiple of resolution, 103 // even if TimePoints have not been Put for all such times. 104 resolution time.Duration 105 106 // windowDuration is the maximum number of time resolutions that is stored in the StatStore 107 // e.g. windowDuration 60, with a resolution of time.Minute represents an 1-hour window 108 windowDuration uint 109 110 // tpCount is the number of TimePoints that are represented in the StatStore. 111 // If tpCount is equal to windowDuration, then the StatStore window is considered full. 112 tpCount uint 113 114 // lastPut maintains the state of values inserted within the last resolution. 115 // When values of a later resolution are added to the StatStore, lastPut is flushed to 116 // the last tpBucket, if its average is within epsilon. Otherwise, a new bucket is - 117 // created. 118 lastPut putState 119 120 // suportedPercentiles is a slice of values from (0,1) that represents the percentiles 121 // that are calculated by the StatStore. 122 supportedPercentiles []float64 123 124 // validCache is true if lastPut has not been flushed since the calculation of the - 125 // cached derived stats. 126 validCache bool 127 128 // cachedAverage, cachedMax and cachedPercentiles are the cached derived stats that - 129 // are exposed by the StatStore. They are calculated upon the first request of any - 130 // derived stat, and invalidated when lastPut is flushed into the StatStore 131 cachedAverage uint64 132 cachedMax uint64 133 cachedPercentiles []uint64 134 } 135 136 // tpBucket is a bucket that represents a set of consecutive TimePoints with different 137 // timestamps whose values differ less than epsilon. 138 // tpBucket essentially represents a time window with a constant value. 139 type tpBucket struct { 140 // count is the number of TimePoints represented in the tpBucket. 141 count uint 142 143 // value is the approximate value of all in the tpBucket, +- epsilon. 144 value uint64 145 146 // max is the maximum value of all TimePoints that have been used to generate the tpBucket. 147 max uint64 148 149 // maxIdx is the number of resolutions after the start time where the max value is located. 150 maxIdx uint 151 } 152 153 // putState is a structure that maintains context of the values in the resolution that is currently 154 // being inserted. 155 // Assumes that Puts are performed in a time-ascending order. 156 type putState struct { 157 actualCount uint 158 average float64 159 max uint64 160 stamp time.Time 161 } 162 163 // IsEmpty returns true if the StatStore is empty 164 func (ss *StatStore) IsEmpty() bool { 165 if ss.buffer.Front() == nil { 166 return true 167 } 168 return false 169 } 170 171 // MaxSize returns the total duration of data that can be stored in the StatStore. 172 func (ss *StatStore) MaxSize() time.Duration { 173 return time.Duration(ss.windowDuration) * ss.resolution 174 } 175 176 func (ss *StatStore) Put(tp TimePoint) error { 177 ss.Lock() 178 defer ss.Unlock() 179 180 // Flatten timestamp to the last multiple of resolution 181 ts := tp.Timestamp.Truncate(ss.resolution) 182 183 lastPutTime := ss.lastPut.stamp 184 185 // Handle the case where the buffer and lastPut are both empty 186 if lastPutTime.Equal(time.Time{}) { 187 ss.resetLastPut(ts, tp.Value) 188 return nil 189 } 190 191 if ts.Before(lastPutTime) { 192 // Ignore TimePoints with Timestamps in the past 193 return fmt.Errorf("the provided timepoint has a timestamp in the past") 194 } 195 196 if ts.Equal(lastPutTime) { 197 // update lastPut with the new TimePoint 198 newVal := tp.Value 199 if newVal > ss.lastPut.max { 200 ss.lastPut.max = newVal 201 } 202 oldAvg := ss.lastPut.average 203 n := float64(ss.lastPut.actualCount) 204 ss.lastPut.average = (float64(newVal) + (n * oldAvg)) / (n + 1) 205 ss.lastPut.actualCount++ 206 return nil 207 } 208 209 ss.flush(ts, tp.Value) 210 return nil 211 } 212 213 // resetLastPut initializes the lastPut field of the StatStore, given a time and a value. 214 func (ss *StatStore) resetLastPut(timestamp time.Time, value uint64) { 215 ss.lastPut.stamp = timestamp 216 ss.lastPut.actualCount = 1 217 ss.lastPut.average = float64(value) 218 ss.lastPut.max = value 219 } 220 221 // newBucket appends a new bucket to the StatStore, using the values of lastPut. 222 // newBucket should be always called BEFORE resetting lastPut. 223 // newBuckets are created by rounding up the lastPut average to the closest epsilon. 224 // numRes represents the number of resolutions from the newest TimePoint to the lastPut. 225 // numRes resolutions will be represented in the newly created bucket. 226 func (ss *StatStore) newBucket(numRes uint) { 227 // Calculate the value of the new bucket based on the average of lastPut. 228 newVal := (uint64(ss.lastPut.average) / ss.epsilon) * ss.epsilon 229 if (uint64(ss.lastPut.average) % ss.epsilon) != 0 { 230 newVal += ss.epsilon 231 } 232 newEntry := tpBucket{ 233 count: numRes, 234 value: newVal, 235 max: ss.lastPut.max, 236 maxIdx: 0, 237 } 238 ss.buffer.PushFront(newEntry) 239 ss.tpCount += numRes 240 241 // If this was the first bucket, update ss.start 242 if ss.start.Equal(time.Time{}) { 243 ss.start = ss.lastPut.stamp 244 } 245 } 246 247 // flush causes the lastPut struct to be flushed to the StatStore list. 248 func (ss *StatStore) flush(ts time.Time, val uint64) { 249 // The new point is in the future, lastPut needs to be flushed to the StatStore. 250 ss.validCache = false 251 252 // Determine how many resolutions in the future the new point is at. 253 // The StatStore always represents values up until 1 resolution from lastPut. 254 // If the TimePoint is more than one resolutions in the future, the last bucket is - 255 // extended to be exactly one resolution behind the new lastPut timestamp. 256 numRes := uint(0) 257 curr := ts 258 for curr.After(ss.lastPut.stamp) { 259 curr = curr.Add(-ss.resolution) 260 numRes++ 261 } 262 263 // Create a new bucket if the buffer is empty 264 if ss.IsEmpty() { 265 ss.newBucket(numRes) 266 ss.resetLastPut(ts, val) 267 for ss.tpCount > ss.windowDuration { 268 ss.rewind() 269 } 270 return 271 } 272 273 lastElem := ss.buffer.Front() 274 lastEntry := lastElem.Value.(tpBucket) 275 lastAvg := ss.lastPut.average 276 277 // Place lastPut in the latest bucket if the difference from its average 278 // is less than epsilon 279 if uint64(math.Abs(float64(lastEntry.value)-lastAvg)) < ss.epsilon { 280 lastEntry.count += numRes 281 ss.tpCount += numRes 282 if ss.lastPut.max > lastEntry.max { 283 lastEntry.max = ss.lastPut.max 284 lastEntry.maxIdx = lastEntry.count - 1 285 } 286 287 // update in list 288 lastElem.Value = lastEntry 289 } else { 290 // Create a new bucket 291 ss.newBucket(numRes) 292 } 293 294 // Delete the earliest represented TimePoints if the window is full 295 for ss.tpCount > ss.windowDuration { 296 ss.rewind() 297 } 298 299 ss.resetLastPut(ts, val) 300 } 301 302 // rewind deletes the oldest one resolution of data in the StatStore. 303 func (ss *StatStore) rewind() { 304 firstElem := ss.buffer.Back() 305 firstEntry := firstElem.Value.(tpBucket) 306 // Decrement number of TimePoints in the earliest tpBucket 307 firstEntry.count-- 308 // Decrement total number of TimePoints in the StatStore 309 ss.tpCount-- 310 311 // Update the max 312 if firstEntry.maxIdx == 0 { 313 // The Max value was just removed, lose precision for other maxes in this bucket 314 firstEntry.max = firstEntry.value 315 firstEntry.maxIdx = firstEntry.count - 1 316 } else { 317 firstEntry.maxIdx-- 318 } 319 320 if firstEntry.count == 0 { 321 // Delete the entry if no TimePoints are represented any more 322 ss.buffer.Remove(firstElem) 323 } else { 324 firstElem.Value = firstEntry 325 } 326 327 // Update the start time of the StatStore 328 ss.start = ss.start.Add(ss.resolution) 329 } 330 331 // Get generates a []TimePoint from the appropriate tpEntries. 332 // Get receives a start and end time as parameters. 333 // If start or end are equal to time.Time{}, then we consider no such bound. 334 func (ss *StatStore) Get(start, end time.Time) []TimePoint { 335 ss.RLock() 336 defer ss.RUnlock() 337 338 var result []TimePoint 339 340 if start.After(end) && end.After(time.Time{}) { 341 return result 342 } 343 344 // Generate a TimePoint for the lastPut, if within range 345 low := start.Equal(time.Time{}) || start.Before(ss.lastPut.stamp) 346 hi := end.Equal(time.Time{}) || !end.Before(ss.lastPut.stamp) 347 if ss.lastPut.actualCount > 0 && low && hi { 348 newTP := TimePoint{ 349 Timestamp: ss.lastPut.stamp, 350 Value: uint64(ss.lastPut.max), // expose the max to avoid conflicts when viewing derived stats 351 } 352 result = append(result, newTP) 353 } 354 355 if ss.IsEmpty() { 356 return result 357 } 358 359 // Generate TimePoints from the buckets in the buffer 360 skipped := 0 361 for elem := ss.buffer.Front(); elem != nil; elem = elem.Next() { 362 entry := elem.Value.(tpBucket) 363 364 // calculate the start time of the entry 365 offset := int(ss.tpCount) - skipped - int(entry.count) 366 entryStart := ss.start.Add(time.Duration(offset) * ss.resolution) 367 368 // ignore tpEntries later than the requested end time 369 if end.After(time.Time{}) && entryStart.After(end) { 370 skipped += int(entry.count) 371 continue 372 } 373 374 // break if we have reached a tpBucket with no values before or equal to 375 // the start time. 376 if !entryStart.Add(time.Duration(entry.count-1) * ss.resolution).After(start) { 377 break 378 } 379 380 // generate as many TimePoints as required from this bucket 381 newSkip := 0 382 for curr := 1; curr <= int(entry.count); curr++ { 383 offset = int(ss.tpCount) - skipped - curr 384 newStamp := ss.start.Add(time.Duration(offset) * ss.resolution) 385 if end.After(time.Time{}) && newStamp.After(end) { 386 continue 387 } 388 389 if newStamp.Before(start) { 390 break 391 } 392 393 // this TimePoint is within (start, end), generate it 394 newSkip++ 395 newTP := TimePoint{ 396 Timestamp: newStamp, 397 Value: entry.value, 398 } 399 result = append(result, newTP) 400 } 401 skipped += newSkip 402 } 403 404 return result 405 } 406 407 // Last returns the latest TimePoint, representing the average value of lastPut. 408 // Last also returns the max value of all Puts represented in lastPut. 409 // Last returns an error if no Put operations have been performed on the StatStore. 410 func (ss *StatStore) Last() (TimePoint, uint64, error) { 411 ss.RLock() 412 defer ss.RUnlock() 413 414 if ss.lastPut.stamp.Equal(time.Time{}) { 415 return TimePoint{}, uint64(0), fmt.Errorf("the StatStore is empty") 416 } 417 418 tp := TimePoint{ 419 Timestamp: ss.lastPut.stamp, 420 Value: uint64(ss.lastPut.average), 421 } 422 423 return tp, ss.lastPut.max, nil 424 } 425 426 // fillCache caches the average, max and percentiles of the StatStore. 427 // Assumes a write lock is taken by the caller. 428 func (ss *StatStore) fillCache() { 429 // Calculate the average and max, flatten values into a slice 430 sum := uint64(0) 431 curMax := ss.lastPut.max 432 vals := []float64{} 433 for elem := ss.buffer.Front(); elem != nil; elem = elem.Next() { 434 entry := elem.Value.(tpBucket) 435 436 // Calculate the weighted sum of all tpBuckets 437 sum += uint64(entry.count) * entry.value 438 439 // Compare the bucket value with the current max 440 if entry.value > curMax { 441 curMax = entry.value 442 } 443 444 // Create a slice of values to generate percentiles 445 for i := uint(0); i < entry.count; i++ { 446 vals = append(vals, float64(entry.value)) 447 } 448 } 449 ss.cachedAverage = sum / uint64(ss.tpCount) 450 ss.cachedMax = curMax 451 452 // Calculate all supported percentiles 453 sort.Float64s(vals) 454 ss.cachedPercentiles = []uint64{} 455 for _, spc := range ss.supportedPercentiles { 456 pcIdx := int(math.Trunc(spc * float64(ss.tpCount))) 457 ss.cachedPercentiles = append(ss.cachedPercentiles, uint64(vals[pcIdx])) 458 } 459 460 ss.validCache = true 461 } 462 463 // Average returns a weighted average across all buckets, using the count of - 464 // resolutions at each bucket as the weight. 465 func (ss *StatStore) Average() (uint64, error) { 466 ss.Lock() 467 defer ss.Unlock() 468 469 if ss.IsEmpty() { 470 return uint64(0), fmt.Errorf("the StatStore is empty") 471 } 472 473 if !ss.validCache { 474 ss.fillCache() 475 } 476 return ss.cachedAverage, nil 477 } 478 479 // Max returns the maximum element currently in the StatStore. 480 // Max does NOT consider the case where the maximum is in the last one minute. 481 func (ss *StatStore) Max() (uint64, error) { 482 ss.Lock() 483 defer ss.Unlock() 484 485 if ss.IsEmpty() { 486 return uint64(0), fmt.Errorf("the StatStore is empty") 487 } 488 489 if !ss.validCache { 490 ss.fillCache() 491 } 492 return ss.cachedMax, nil 493 } 494 495 // Percentile returns the requested percentile from the StatStore. 496 func (ss *StatStore) Percentile(p float64) (uint64, error) { 497 ss.Lock() 498 defer ss.Unlock() 499 500 if ss.IsEmpty() { 501 return uint64(0), fmt.Errorf("the StatStore is empty") 502 } 503 504 // Check if the specific percentile is supported 505 found := false 506 idx := 0 507 for i, spc := range ss.supportedPercentiles { 508 if p == spc { 509 found = true 510 idx = i 511 break 512 } 513 } 514 515 if !found { 516 return uint64(0), fmt.Errorf("the requested percentile is not supported") 517 } 518 519 if !ss.validCache { 520 ss.fillCache() 521 } 522 523 return ss.cachedPercentiles[idx], nil 524 }