Merge pull request #1485 from eliothedeman/master

Adds holt-winters query function
This commit is contained in:
Brian Brazil 2016-03-28 20:53:01 +01:00
commit 24a3ad3d16
5 changed files with 276 additions and 0 deletions

45
promql/bench.go Normal file
View File

@ -0,0 +1,45 @@
// Copyright 2015 The Prometheus 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, softwar
// 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 promql
import "testing"
// A Benchmark holds context for running a unit test as a benchmark.
type Benchmark struct {
b *testing.B
t *Test
iterCount int
}
// NewBenchmark returns an initialized empty Benchmark.
func NewBenchmark(b *testing.B, input string) *Benchmark {
t, err := NewTest(b, input)
if err != nil {
b.Fatalf("Unable to run benchmark: %s", err)
}
return &Benchmark{
b: b,
t: t,
}
}
// Run runs the benchmark.
func (b *Benchmark) Run() {
b.b.ReportAllocs()
b.b.ResetTimer()
for i := 0; i < b.b.N; i++ {
b.t.RunAsBenchmark(b)
b.iterCount++
}
}

View File

@ -184,6 +184,102 @@ func funcIrate(ev *evaluator, args Expressions) model.Value {
return resultVector
}
// Calculate the trend value at the given index i in raw data d.
// This is somewhat analogous to the slope of the trend at the given index.
// The argument "s" is the set of computed smoothed values.
// The argument "b" is the set of computed trend factors.
// The argument "d" is the set of raw input values.
func calcTrendValue(i int, sf, tf float64, s, b, d []float64) float64 {
if i == 0 {
return b[0]
}
x := tf * (s[i] - s[i-1])
y := (1 - tf) * b[i-1]
// Cache the computed value.
b[i] = x + y
return b[i]
}
// Holt-Winters is similar to a weighted moving average, where historical data has exponentially less influence on the current data.
// Holt-Winter also accounts for trends in data. The smoothing factor (0 < sf < 1) effects how historical data will effect the current
// data. A lower smoothing factor increases the influence of historical data. The trend factor (0 < tf < 1) effects
// how trends in historical data will effect the current data. A higher trend factor increases the influence.
// of trends. Algorithm taken from https://en.wikipedia.org/wiki/Exponential_smoothing titled: "Double exponential smoothing".
func funcHoltWinters(ev *evaluator, args Expressions) model.Value {
mat := ev.evalMatrix(args[0])
// The smoothing factor argument.
sf := ev.evalFloat(args[1])
// The trend factor argument.
tf := ev.evalFloat(args[2])
// Sanity check the input.
if sf <= 0 || sf >= 1 {
ev.errorf("invalid smoothing factor. Expected: 0 < sf < 1 got: %f", sf)
}
if tf <= 0 || tf >= 1 {
ev.errorf("invalid trend factor. Expected: 0 < tf < 1 got: %f", sf)
}
// Make an output vector large enough to hold the entire result.
resultVector := make(vector, 0, len(mat))
// Create scratch values.
var s, b, d []float64
var l int
for _, samples := range mat {
l = len(samples.Values)
// Can't do the smoothing operation with less than two points.
if l < 2 {
continue
}
// Resize scratch values.
if l != len(s) {
s = make([]float64, l)
b = make([]float64, l)
d = make([]float64, l)
}
// Fill in the d values with the raw values from the input.
for i, v := range samples.Values {
d[i] = float64(v.Value)
}
// Set initial values.
s[0] = d[0]
b[0] = d[1] - d[0]
// Run the smoothing operation.
var x, y float64
for i := 1; i < len(d); i++ {
// Scale the raw value against the smoothing factor.
x = sf * d[i]
// Scale the last smoothed value with the trend at this point.
y = (1 - sf) * (s[i-1] + calcTrendValue(i-1, sf, tf, s, b, d))
s[i] = x + y
}
samples.Metric.Del(model.MetricNameLabel)
resultVector = append(resultVector, &sample{
Metric: samples.Metric,
Value: model.SampleValue(s[len(s)-1]), // The last value in the vector is the smoothed result.
Timestamp: ev.Timestamp,
})
}
return resultVector
}
// === sort(node model.ValVector) Vector ===
func funcSort(ev *evaluator, args Expressions) model.Value {
// NaN should sort to the bottom, so take descending sort with NaN first and
@ -848,6 +944,12 @@ var functions = map[string]*Function{
ReturnType: model.ValVector,
Call: funcHistogramQuantile,
},
"holt_winters": {
Name: "holt_winters",
ArgTypes: []model.ValueType{model.ValMatrix, model.ValScalar, model.ValScalar},
ReturnType: model.ValVector,
Call: funcHoltWinters,
},
"irate": {
Name: "irate",
ArgTypes: []model.ValueType{model.ValMatrix},

73
promql/functions_test.go Normal file
View File

@ -0,0 +1,73 @@
// Copyright 2015 The Prometheus 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 promql
import "testing"
func BenchmarkHoltWinters4Week5Min(b *testing.B) {
input := `
clear
load 5m
http_requests{path="/foo"} 0+10x8064
eval instant at 4w holt_winters(http_requests[4w], 0.3, 0.3)
{path="/foo"} 20160
`
bench := NewBenchmark(b, input)
bench.Run()
}
func BenchmarkHoltWinters1Week5Min(b *testing.B) {
input := `
clear
load 5m
http_requests{path="/foo"} 0+10x2016
eval instant at 1w holt_winters(http_requests[1w], 0.3, 0.3)
{path="/foo"} 20160
`
bench := NewBenchmark(b, input)
bench.Run()
}
func BenchmarkHoltWinters1Day1Min(b *testing.B) {
input := `
clear
load 1m
http_requests{path="/foo"} 0+10x1440
eval instant at 1d holt_winters(http_requests[1d], 0.3, 0.3)
{path="/foo"} 20160
`
bench := NewBenchmark(b, input)
bench.Run()
}
func BenchmarkChanges1Day1Min(b *testing.B) {
input := `
clear
load 1m
http_requests{path="/foo"} 0+10x1440
eval instant at 1d changes(http_requests[1d])
{path="/foo"} 20160
`
bench := NewBenchmark(b, input)
bench.Run()
}

View File

@ -411,6 +411,32 @@ func (cmd clearCmd) String() string {
return "clear"
}
// RunAsBenchmark runs the test in benchmark mode.
// This will not count any loads or non eval functions.
func (t *Test) RunAsBenchmark(b *Benchmark) error {
for _, cmd := range t.cmds {
switch cmd.(type) {
// Only time the "eval" command.
case *evalCmd:
err := t.exec(cmd)
if err != nil {
return err
}
default:
if b.iterCount == 0 {
b.b.StopTimer()
err := t.exec(cmd)
if err != nil {
return err
}
b.b.StartTimer()
}
}
}
return nil
}
// Run executes the command sequence of the test. Until the maximum error number
// is reached, evaluation errors do not terminate execution.
func (t *Test) Run() error {

View File

@ -288,3 +288,33 @@ eval_ordered instant at 50m sort_desc(http_requests)
http_requests{group="production", instance="1", job="api-server"} 200
http_requests{group="production", instance="0", job="api-server"} 100
http_requests{group="canary", instance="2", job="api-server"} NaN
# Tests for holt_winters
clear
# positive trends
load 10s
http_requests{job="api-server", instance="0", group="production"} 0+10x1000 100+30x1000
http_requests{job="api-server", instance="1", group="production"} 0+20x1000 200+30x1000
http_requests{job="api-server", instance="0", group="canary"} 0+30x1000 300+80x1000
http_requests{job="api-server", instance="1", group="canary"} 0+40x2000
eval instant at 8000s holt_winters(http_requests[1m], 0.01, 0.1)
{job="api-server", instance="0", group="production"} 8000
{job="api-server", instance="1", group="production"} 16000
{job="api-server", instance="0", group="canary"} 24000
{job="api-server", instance="1", group="canary"} 32000
# negative trends
clear
load 10s
http_requests{job="api-server", instance="0", group="production"} 8000-10x1000
http_requests{job="api-server", instance="1", group="production"} 0-20x1000
http_requests{job="api-server", instance="0", group="canary"} 0+30x1000 300-80x1000
http_requests{job="api-server", instance="1", group="canary"} 0-40x1000 0+40x1000
eval instant at 8000s holt_winters(http_requests[1m], 0.01, 0.1)
{job="api-server", instance="0", group="production"} 0
{job="api-server", instance="1", group="production"} -16000
{job="api-server", instance="0", group="canary"} 24000
{job="api-server", instance="1", group="canary"} -32000