diff --git a/promql/bench.go b/promql/bench.go new file mode 100644 index 000000000..a60091e31 --- /dev/null +++ b/promql/bench.go @@ -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++ + } +} diff --git a/promql/functions.go b/promql/functions.go index 433c06f02..9de0ba98e 100644 --- a/promql/functions.go +++ b/promql/functions.go @@ -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}, diff --git a/promql/functions_test.go b/promql/functions_test.go new file mode 100644 index 000000000..bbb51df37 --- /dev/null +++ b/promql/functions_test.go @@ -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() +} diff --git a/promql/test.go b/promql/test.go index 2ca105efa..60acb9285 100644 --- a/promql/test.go +++ b/promql/test.go @@ -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 { diff --git a/promql/testdata/functions.test b/promql/testdata/functions.test index 28233033e..a568595a7 100644 --- a/promql/testdata/functions.test +++ b/promql/testdata/functions.test @@ -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