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