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  }