Skip to content

Commit

Permalink
Merge pull request #8 from kgrygiel/master
Browse files Browse the repository at this point in the history
Base Recommender model classes: Histogram and CircularBuffer.
  • Loading branch information
mwielgus authored Apr 24, 2017
2 parents 1358f0c + 491e643 commit 7432121
Show file tree
Hide file tree
Showing 6 changed files with 563 additions and 0 deletions.
127 changes: 127 additions & 0 deletions vertical-pod-autoscaler/recommender/util/histogram.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package util

// Histogram represents an approximate distribution of some variable.
type Histogram interface {
// Returns an approximation of the given percentile of the distribution.
// Note: the argument passed to Percentile() is a number between
// 0 and 1. For example 0.5 corresponds to the median and 0.9 to the
// 90th percentile.
// If the histogram is empty, Percentile() returns 0.0.
Percentile(percentile float64) float64

// Add a sample with a given value and weight.
AddSample(value float64, weight float64)

// Remove a sample with a given value and weight. Note that the total
// weight of samples with a given value cannot be negative.
SubtractSample(value float64, weight float64)

// Returns true if the histogram is empty.
IsEmpty() bool
}

// NewHistogram returns a new Histogram instance using given options.
func NewHistogram(options HistogramOptions) Histogram {
return &histogram{
&options, make([]float64, options.NumBuckets()), 0.0,
options.NumBuckets() - 1, 0}
}

// Simple bucket-based implementation of the Histogram interface. Each bucket
// holds the total weight of samples that belong to it.
// Percentile() returns the middle of the correspodning bucket.
// Resolution (bucket boundaries) of the histogram depends on the options.
// There's no interpolation within buckets (i.e. one sample falls to exactly one
// bucket).
// A bucket is considered empty if its weight is smaller than options.Epsilon().
type histogram struct {
// Bucketing scheme.
options *HistogramOptions
// Cumulative weight of samples in each bucket.
bucketWeight []float64
// Total cumulative weight of samples in all buckets.
totalWeight float64
// Index of the first non-empty bucket if there's any. Otherwise index
// of the last bucket.
minBucket int
// Index of the last non-empty bucket if there's any. Otherwise 0.
maxBucket int
}

func (h *histogram) AddSample(value float64, weight float64) {
if weight < 0.0 {
panic("sample weight must be non-negative")
}
bucket := (*h.options).FindBucket(value)
h.bucketWeight[bucket] += weight
h.totalWeight += weight
if bucket < h.minBucket {
h.minBucket = bucket
}
if bucket > h.maxBucket {
h.maxBucket = bucket
}
}
func (h *histogram) SubtractSample(value float64, weight float64) {
if weight < 0.0 {
panic("sample weight must be non-negative")
}
bucket := (*h.options).FindBucket(value)
epsilon := (*h.options).Epsilon()
if weight > h.bucketWeight[bucket]-epsilon {
weight = h.bucketWeight[bucket]
}
h.totalWeight -= weight
h.bucketWeight[bucket] -= weight
lastBucket := (*h.options).NumBuckets() - 1
for h.bucketWeight[h.minBucket] < epsilon && h.minBucket < lastBucket {
h.minBucket++
}
for h.bucketWeight[h.maxBucket] < epsilon && h.maxBucket > 0 {
h.maxBucket--
}
}

func (h *histogram) Percentile(percentile float64) float64 {
if h.IsEmpty() {
return 0.0
}
partialSum := 0.0
threshold := percentile * h.totalWeight
bucket := h.minBucket
for ; bucket < h.maxBucket; bucket++ {
partialSum += h.bucketWeight[bucket]
if partialSum >= threshold {
break
}
}
bucketStart := (*h.options).GetBucketStart(bucket)
if bucket < (*h.options).NumBuckets()-1 {
// Return the middle point between the bucket boundaries.
bucketEnd := (*h.options).GetBucketStart(bucket + 1)
return (bucketStart + bucketEnd) / 2.0
}
// Return the start of the last bucket (note that the last bucket
// doesn't have an upper bound).
return bucketStart
}

func (h *histogram) IsEmpty() bool {
return h.bucketWeight[h.minBucket] < (*h.options).Epsilon()
}
135 changes: 135 additions & 0 deletions vertical-pod-autoscaler/recommender/util/histogram_options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package util

import (
"errors"
"fmt"
"math"
)

// HistogramOptions define the number and size of buckets of a histogram.
type HistogramOptions interface {
// Returns the number of buckets in the histogram.
NumBuckets() int
// Returns the index of the bucket to which the given value falls.
// If the value is outside of the range covered by the histogram, it
// returns the closest bucket (either the first or the last one).
FindBucket(value float64) int
// Returns the start of the bucket with a given index. If the index is
// outside the [0..NumBuckets() - 1] range, the result is undefined.
GetBucketStart(bucket int) float64
// Returns the minimum weight for a bucket to be considered non-empty.
Epsilon() float64
}

// NewLinearHistogramOptions returns HistogramOptions describing a histogram
// with a given number of fixed-size buckets, with the first bucket start at 0.0
// and the last bucket start larger or equal to maxValue.
// Requires maxValue > 0, bucketSize > 0, epsilon > 0.
func NewLinearHistogramOptions(
maxValue float64, bucketSize float64, epsilon float64) (HistogramOptions, error) {
if maxValue <= 0.0 || bucketSize <= 0.0 || epsilon <= 0.0 {
return nil, errors.New("maxValue and bucketSize must both be positive")
}
numBuckets := int(math.Ceil(maxValue/bucketSize)) + 1
return &linearHistogramOptions{numBuckets, bucketSize, epsilon}, nil
}

// NewExponentialHistogramOptions returns HistogramOptions describing a
// histogram with exponentially growing bucket boundaries. The first bucket
// covers the range [0..firstBucketSize). Consecutive buckets are of the form
// [x(n)..x(n) * ratio) for n = 1 .. numBuckets - 1.
// The last bucket start is larger or equal to maxValue.
// Requires maxValue > 0, firstBucketSize > 0, ratio > 1, epsilon > 0.
func NewExponentialHistogramOptions(
maxValue float64, firstBucketSize float64, ratio float64, epsilon float64) (HistogramOptions, error) {
if maxValue <= 0.0 || firstBucketSize <= 0.0 || ratio <= 1.0 || epsilon <= 0.0 {
return nil, errors.New(
"maxValue, firstBucketSize and epsilon must be > 0.0, ratio must be > 1.0")
}
numBuckets := int(math.Ceil(math.Log(maxValue/firstBucketSize)/math.Log(ratio))) + 2
return &exponentialHistogramOptions{numBuckets, firstBucketSize, ratio, epsilon}, nil
}

type linearHistogramOptions struct {
numBuckets int
bucketSize float64
epsilon float64
}

type exponentialHistogramOptions struct {
numBuckets int
firstBucketSize float64
ratio float64
epsilon float64
}

func (o *linearHistogramOptions) NumBuckets() int {
return o.numBuckets
}

func (o *linearHistogramOptions) FindBucket(value float64) int {
bucket := int(value / o.bucketSize)
if bucket < 0 {
return 0
}
if bucket >= o.numBuckets {
return o.numBuckets - 1
}
return bucket
}

func (o *linearHistogramOptions) GetBucketStart(bucket int) float64 {
if bucket < 0 || bucket >= o.numBuckets {
panic(fmt.Sprintf("index %d out of range [0..%d]", bucket, o.numBuckets-1))
}
return float64(bucket) * o.bucketSize
}

func (o *linearHistogramOptions) Epsilon() float64 {
return o.epsilon
}

func (o *exponentialHistogramOptions) NumBuckets() int {
return o.numBuckets
}

func (o *exponentialHistogramOptions) FindBucket(value float64) int {
if value < o.firstBucketSize {
return 0
}
bucket := int(math.Log(value/o.firstBucketSize)/math.Log(o.ratio)) + 1
if bucket >= o.numBuckets {
return o.numBuckets - 1
}
return bucket
}

func (o *exponentialHistogramOptions) GetBucketStart(bucket int) float64 {
if bucket < 0 || bucket >= o.numBuckets {
panic(fmt.Sprintf("index %d out of range [0..%d]", bucket, o.numBuckets-1))
}
if bucket == 0 {
return 0.0
}
return o.firstBucketSize * math.Pow(o.ratio, float64(bucket-1))
}

func (o *exponentialHistogramOptions) Epsilon() float64 {
return o.epsilon
}
64 changes: 64 additions & 0 deletions vertical-pod-autoscaler/recommender/util/histogram_options_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package util

import (
"testing"

"github.com/stretchr/testify/assert"
)

var (
epsilon = 0.001
)

// Test all methods of LinearHistogramOptions using a sample bucketing scheme.
func TestLinearHistogramOptions(t *testing.T) {
o, err := NewLinearHistogramOptions(5.0, 0.3, epsilon)
assert.Nil(t, err)
assert.Equal(t, epsilon, o.Epsilon())
assert.Equal(t, 18, o.NumBuckets())

assert.Equal(t, 0.0, o.GetBucketStart(0))
assert.Equal(t, 5.1, o.GetBucketStart(17))

assert.Equal(t, 0, o.FindBucket(-1.0))
assert.Equal(t, 0, o.FindBucket(0.0))
assert.Equal(t, 4, o.FindBucket(1.3))
assert.Equal(t, 17, o.FindBucket(100.0))
}

// Test all methods of ExponentialHistogramOptions using a sample bucketing scheme.
func TestExponentialHistogramOptions(t *testing.T) {
o, err := NewExponentialHistogramOptions(100.0, 10.0, 2.0, epsilon)
assert.Nil(t, err)
assert.Equal(t, epsilon, o.Epsilon())
assert.Equal(t, 6, o.NumBuckets())

assert.Equal(t, 0.0, o.GetBucketStart(0))
assert.Equal(t, 10.0, o.GetBucketStart(1))
assert.Equal(t, 20.0, o.GetBucketStart(2))
assert.Equal(t, 40.0, o.GetBucketStart(3))
assert.Equal(t, 80.0, o.GetBucketStart(4))
assert.Equal(t, 160.0, o.GetBucketStart(5))

assert.Equal(t, 0, o.FindBucket(-1.0))
assert.Equal(t, 0, o.FindBucket(9.99))
assert.Equal(t, 1, o.FindBucket(10.0))
assert.Equal(t, 2, o.FindBucket(20.0))
assert.Equal(t, 5, o.FindBucket(200.0))
}
Loading

0 comments on commit 7432121

Please sign in to comment.