1// Copyright 2015 The Prometheus Authors
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14package prometheus
15
16import (
17	"math"
18	"math/rand"
19	"reflect"
20	"runtime"
21	"sort"
22	"sync"
23	"testing"
24	"testing/quick"
25
26	dto "github.com/prometheus/client_model/go"
27)
28
29func benchmarkHistogramObserve(w int, b *testing.B) {
30	b.StopTimer()
31
32	wg := new(sync.WaitGroup)
33	wg.Add(w)
34
35	g := new(sync.WaitGroup)
36	g.Add(1)
37
38	s := NewHistogram(HistogramOpts{})
39
40	for i := 0; i < w; i++ {
41		go func() {
42			g.Wait()
43
44			for i := 0; i < b.N; i++ {
45				s.Observe(float64(i))
46			}
47
48			wg.Done()
49		}()
50	}
51
52	b.StartTimer()
53	g.Done()
54	wg.Wait()
55}
56
57func BenchmarkHistogramObserve1(b *testing.B) {
58	benchmarkHistogramObserve(1, b)
59}
60
61func BenchmarkHistogramObserve2(b *testing.B) {
62	benchmarkHistogramObserve(2, b)
63}
64
65func BenchmarkHistogramObserve4(b *testing.B) {
66	benchmarkHistogramObserve(4, b)
67}
68
69func BenchmarkHistogramObserve8(b *testing.B) {
70	benchmarkHistogramObserve(8, b)
71}
72
73func benchmarkHistogramWrite(w int, b *testing.B) {
74	b.StopTimer()
75
76	wg := new(sync.WaitGroup)
77	wg.Add(w)
78
79	g := new(sync.WaitGroup)
80	g.Add(1)
81
82	s := NewHistogram(HistogramOpts{})
83
84	for i := 0; i < 1000000; i++ {
85		s.Observe(float64(i))
86	}
87
88	for j := 0; j < w; j++ {
89		outs := make([]dto.Metric, b.N)
90
91		go func(o []dto.Metric) {
92			g.Wait()
93
94			for i := 0; i < b.N; i++ {
95				s.Write(&o[i])
96			}
97
98			wg.Done()
99		}(outs)
100	}
101
102	b.StartTimer()
103	g.Done()
104	wg.Wait()
105}
106
107func BenchmarkHistogramWrite1(b *testing.B) {
108	benchmarkHistogramWrite(1, b)
109}
110
111func BenchmarkHistogramWrite2(b *testing.B) {
112	benchmarkHistogramWrite(2, b)
113}
114
115func BenchmarkHistogramWrite4(b *testing.B) {
116	benchmarkHistogramWrite(4, b)
117}
118
119func BenchmarkHistogramWrite8(b *testing.B) {
120	benchmarkHistogramWrite(8, b)
121}
122
123func TestHistogramNonMonotonicBuckets(t *testing.T) {
124	testCases := map[string][]float64{
125		"not strictly monotonic":  {1, 2, 2, 3},
126		"not monotonic at all":    {1, 2, 4, 3, 5},
127		"have +Inf in the middle": {1, 2, math.Inf(+1), 3},
128	}
129	for name, buckets := range testCases {
130		func() {
131			defer func() {
132				if r := recover(); r == nil {
133					t.Errorf("Buckets %v are %s but NewHistogram did not panic.", buckets, name)
134				}
135			}()
136			_ = NewHistogram(HistogramOpts{
137				Name:    "test_histogram",
138				Help:    "helpless",
139				Buckets: buckets,
140			})
141		}()
142	}
143}
144
145// Intentionally adding +Inf here to test if that case is handled correctly.
146// Also, getCumulativeCounts depends on it.
147var testBuckets = []float64{-2, -1, -0.5, 0, 0.5, 1, 2, math.Inf(+1)}
148
149func TestHistogramConcurrency(t *testing.T) {
150	if testing.Short() {
151		t.Skip("Skipping test in short mode.")
152	}
153
154	rand.Seed(42)
155
156	it := func(n uint32) bool {
157		mutations := int(n%1e4 + 1e4)
158		concLevel := int(n%5 + 1)
159		total := mutations * concLevel
160
161		var start, end sync.WaitGroup
162		start.Add(1)
163		end.Add(concLevel)
164
165		sum := NewHistogram(HistogramOpts{
166			Name:    "test_histogram",
167			Help:    "helpless",
168			Buckets: testBuckets,
169		})
170
171		allVars := make([]float64, total)
172		var sampleSum float64
173		for i := 0; i < concLevel; i++ {
174			vals := make([]float64, mutations)
175			for j := 0; j < mutations; j++ {
176				v := rand.NormFloat64()
177				vals[j] = v
178				allVars[i*mutations+j] = v
179				sampleSum += v
180			}
181
182			go func(vals []float64) {
183				start.Wait()
184				for _, v := range vals {
185					sum.Observe(v)
186				}
187				end.Done()
188			}(vals)
189		}
190		sort.Float64s(allVars)
191		start.Done()
192		end.Wait()
193
194		m := &dto.Metric{}
195		sum.Write(m)
196		if got, want := int(*m.Histogram.SampleCount), total; got != want {
197			t.Errorf("got sample count %d, want %d", got, want)
198		}
199		if got, want := *m.Histogram.SampleSum, sampleSum; math.Abs((got-want)/want) > 0.001 {
200			t.Errorf("got sample sum %f, want %f", got, want)
201		}
202
203		wantCounts := getCumulativeCounts(allVars)
204
205		if got, want := len(m.Histogram.Bucket), len(testBuckets)-1; got != want {
206			t.Errorf("got %d buckets in protobuf, want %d", got, want)
207		}
208		for i, wantBound := range testBuckets {
209			if i == len(testBuckets)-1 {
210				break // No +Inf bucket in protobuf.
211			}
212			if gotBound := *m.Histogram.Bucket[i].UpperBound; gotBound != wantBound {
213				t.Errorf("got bound %f, want %f", gotBound, wantBound)
214			}
215			if gotCount, wantCount := *m.Histogram.Bucket[i].CumulativeCount, wantCounts[i]; gotCount != wantCount {
216				t.Errorf("got count %d, want %d", gotCount, wantCount)
217			}
218		}
219		return true
220	}
221
222	if err := quick.Check(it, nil); err != nil {
223		t.Error(err)
224	}
225}
226
227func TestHistogramVecConcurrency(t *testing.T) {
228	if testing.Short() {
229		t.Skip("Skipping test in short mode.")
230	}
231
232	rand.Seed(42)
233
234	it := func(n uint32) bool {
235		mutations := int(n%1e4 + 1e4)
236		concLevel := int(n%7 + 1)
237		vecLength := int(n%3 + 1)
238
239		var start, end sync.WaitGroup
240		start.Add(1)
241		end.Add(concLevel)
242
243		his := NewHistogramVec(
244			HistogramOpts{
245				Name:    "test_histogram",
246				Help:    "helpless",
247				Buckets: []float64{-2, -1, -0.5, 0, 0.5, 1, 2, math.Inf(+1)},
248			},
249			[]string{"label"},
250		)
251
252		allVars := make([][]float64, vecLength)
253		sampleSums := make([]float64, vecLength)
254		for i := 0; i < concLevel; i++ {
255			vals := make([]float64, mutations)
256			picks := make([]int, mutations)
257			for j := 0; j < mutations; j++ {
258				v := rand.NormFloat64()
259				vals[j] = v
260				pick := rand.Intn(vecLength)
261				picks[j] = pick
262				allVars[pick] = append(allVars[pick], v)
263				sampleSums[pick] += v
264			}
265
266			go func(vals []float64) {
267				start.Wait()
268				for i, v := range vals {
269					his.WithLabelValues(string('A' + picks[i])).Observe(v)
270				}
271				end.Done()
272			}(vals)
273		}
274		for _, vars := range allVars {
275			sort.Float64s(vars)
276		}
277		start.Done()
278		end.Wait()
279
280		for i := 0; i < vecLength; i++ {
281			m := &dto.Metric{}
282			s := his.WithLabelValues(string('A' + i))
283			s.(Histogram).Write(m)
284
285			if got, want := len(m.Histogram.Bucket), len(testBuckets)-1; got != want {
286				t.Errorf("got %d buckets in protobuf, want %d", got, want)
287			}
288			if got, want := int(*m.Histogram.SampleCount), len(allVars[i]); got != want {
289				t.Errorf("got sample count %d, want %d", got, want)
290			}
291			if got, want := *m.Histogram.SampleSum, sampleSums[i]; math.Abs((got-want)/want) > 0.001 {
292				t.Errorf("got sample sum %f, want %f", got, want)
293			}
294
295			wantCounts := getCumulativeCounts(allVars[i])
296
297			for j, wantBound := range testBuckets {
298				if j == len(testBuckets)-1 {
299					break // No +Inf bucket in protobuf.
300				}
301				if gotBound := *m.Histogram.Bucket[j].UpperBound; gotBound != wantBound {
302					t.Errorf("got bound %f, want %f", gotBound, wantBound)
303				}
304				if gotCount, wantCount := *m.Histogram.Bucket[j].CumulativeCount, wantCounts[j]; gotCount != wantCount {
305					t.Errorf("got count %d, want %d", gotCount, wantCount)
306				}
307			}
308		}
309		return true
310	}
311
312	if err := quick.Check(it, nil); err != nil {
313		t.Error(err)
314	}
315}
316
317func getCumulativeCounts(vars []float64) []uint64 {
318	counts := make([]uint64, len(testBuckets))
319	for _, v := range vars {
320		for i := len(testBuckets) - 1; i >= 0; i-- {
321			if v > testBuckets[i] {
322				break
323			}
324			counts[i]++
325		}
326	}
327	return counts
328}
329
330func TestBuckets(t *testing.T) {
331	got := LinearBuckets(-15, 5, 6)
332	want := []float64{-15, -10, -5, 0, 5, 10}
333	if !reflect.DeepEqual(got, want) {
334		t.Errorf("linear buckets: got %v, want %v", got, want)
335	}
336
337	got = ExponentialBuckets(100, 1.2, 3)
338	want = []float64{100, 120, 144}
339	if !reflect.DeepEqual(got, want) {
340		t.Errorf("exponential buckets: got %v, want %v", got, want)
341	}
342}
343
344func TestHistogramAtomicObserve(t *testing.T) {
345	var (
346		quit = make(chan struct{})
347		his  = NewHistogram(HistogramOpts{
348			Buckets: []float64{0.5, 10, 20},
349		})
350	)
351
352	defer func() { close(quit) }()
353
354	observe := func() {
355		for {
356			select {
357			case <-quit:
358				return
359			default:
360				his.Observe(1)
361			}
362		}
363	}
364
365	go observe()
366	go observe()
367	go observe()
368
369	for i := 0; i < 100; i++ {
370		m := &dto.Metric{}
371		if err := his.Write(m); err != nil {
372			t.Fatal("unexpected error writing histogram:", err)
373		}
374		h := m.GetHistogram()
375		if h.GetSampleCount() != uint64(h.GetSampleSum()) ||
376			h.GetSampleCount() != h.GetBucket()[1].GetCumulativeCount() ||
377			h.GetSampleCount() != h.GetBucket()[2].GetCumulativeCount() {
378			t.Fatalf(
379				"inconsistent counts in histogram: count=%d sum=%f buckets=[%d, %d]",
380				h.GetSampleCount(), h.GetSampleSum(),
381				h.GetBucket()[1].GetCumulativeCount(), h.GetBucket()[2].GetCumulativeCount(),
382			)
383		}
384		runtime.Gosched()
385	}
386}
387