github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/query/functions/linear/histogram_quantile_test.go (about) 1 // Copyright (c) 2019 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package linear 22 23 import ( 24 "math" 25 "testing" 26 "time" 27 28 "github.com/m3db/m3/src/query/block" 29 "github.com/m3db/m3/src/query/executor/transform" 30 "github.com/m3db/m3/src/query/models" 31 "github.com/m3db/m3/src/query/parser" 32 "github.com/m3db/m3/src/query/test" 33 "github.com/m3db/m3/src/query/test/compare" 34 "github.com/m3db/m3/src/query/test/executor" 35 xtime "github.com/m3db/m3/src/x/time" 36 37 "github.com/stretchr/testify/assert" 38 "github.com/stretchr/testify/require" 39 ) 40 41 func TestGatherSeriesToBuckets(t *testing.T) { 42 name := []byte("name") 43 bucket := []byte("bucket") 44 tagOpts := models.NewTagOptions(). 45 SetIDSchemeType(models.TypeQuoted). 46 SetMetricName(name). 47 SetBucketName(bucket) 48 49 tags := models.NewTags(3, tagOpts).SetName([]byte("foo")).AddTag(models.Tag{ 50 Name: []byte("bar"), 51 Value: []byte("baz"), 52 }) 53 54 noBucketMeta := block.SeriesMeta{Tags: tags} 55 invalidBucketMeta := block.SeriesMeta{Tags: tags.Clone().SetBucket([]byte("string"))} 56 validMeta := block.SeriesMeta{Tags: tags.Clone().SetBucket([]byte("0.1"))} 57 validMeta2 := block.SeriesMeta{Tags: tags.Clone().SetBucket([]byte("0.1"))} 58 validMeta3 := block.SeriesMeta{Tags: tags.Clone().SetBucket([]byte("10"))} 59 infMeta := block.SeriesMeta{Tags: tags.Clone().SetBucket([]byte("Inf"))} 60 validMetaMoreTags := block.SeriesMeta{Tags: tags.Clone().SetBucket([]byte("0.1")).AddTag(models.Tag{ 61 Name: []byte("qux"), 62 Value: []byte("qar"), 63 })} 64 65 metas := []block.SeriesMeta{ 66 validMeta, noBucketMeta, invalidBucketMeta, validMeta2, validMetaMoreTags, validMeta3, infMeta, 67 } 68 69 actual := gatherSeriesToBuckets(metas) 70 expected := bucketedSeries{ 71 `{bar="baz"}`: indexedBuckets{ 72 buckets: []indexedBucket{ 73 {upperBound: 0.1, idx: 0}, 74 {upperBound: 0.1, idx: 3}, 75 {upperBound: 10, idx: 5}, 76 {upperBound: math.Inf(1), idx: 6}, 77 }, 78 tags: models.NewTags(1, tagOpts).AddTag(models.Tag{ 79 Name: []byte("bar"), 80 Value: []byte("baz"), 81 }), 82 }, 83 `{bar="baz",qux="qar"}`: indexedBuckets{ 84 buckets: []indexedBucket{ 85 {upperBound: 0.1, idx: 4}, 86 }, 87 tags: models.NewTags(1, tagOpts).AddTag(models.Tag{ 88 Name: []byte("bar"), 89 Value: []byte("baz"), 90 }).AddTag(models.Tag{ 91 Name: []byte("qux"), 92 Value: []byte("qar"), 93 }), 94 }, 95 } 96 97 assert.Equal(t, sanitizeBuckets(expected), actual) 98 } 99 100 func TestSanitizeBuckets(t *testing.T) { 101 bucketed := bucketedSeries{ 102 `{bar="baz"}`: indexedBuckets{ 103 buckets: []indexedBucket{ 104 {upperBound: 10, idx: 5}, 105 {upperBound: math.Inf(1), idx: 6}, 106 {upperBound: 1, idx: 0}, 107 {upperBound: 2, idx: 3}, 108 }, 109 }, 110 `{with="neginf"}`: indexedBuckets{ 111 buckets: []indexedBucket{ 112 {upperBound: 10, idx: 5}, 113 {upperBound: math.Inf(-1), idx: 6}, 114 {upperBound: 1, idx: 0}, 115 {upperBound: 2, idx: 3}, 116 }, 117 }, 118 `{no="infinity"}`: indexedBuckets{ 119 buckets: []indexedBucket{ 120 {upperBound: 0.1, idx: 4}, 121 {upperBound: 0.2, idx: 14}, 122 {upperBound: 0.3, idx: 114}, 123 }, 124 }, 125 `{just="infinity"}`: indexedBuckets{ 126 buckets: []indexedBucket{ 127 {upperBound: math.Inf(1), idx: 4}, 128 }, 129 }, 130 `{just="neg-infinity"}`: indexedBuckets{ 131 buckets: []indexedBucket{ 132 {upperBound: math.Inf(-1), idx: 4}, 133 }, 134 }, 135 } 136 137 expected := validSeriesBuckets{ 138 indexedBuckets{ 139 buckets: []indexedBucket{ 140 {upperBound: 1, idx: 0}, 141 {upperBound: 2, idx: 3}, 142 {upperBound: 10, idx: 5}, 143 {upperBound: math.Inf(1), idx: 6}, 144 }, 145 }, 146 } 147 148 assert.Equal(t, expected, sanitizeBuckets(bucketed)) 149 } 150 151 func TestEnsureMonotonic(t *testing.T) { 152 tests := []struct { 153 name string 154 data []bucketValue 155 want []bucketValue 156 }{ 157 { 158 "empty", 159 []bucketValue{}, 160 []bucketValue{}, 161 }, 162 { 163 "one", 164 []bucketValue{{upperBound: 1, value: 5}}, 165 []bucketValue{{upperBound: 1, value: 5}}, 166 }, 167 { 168 "two monotonic", 169 []bucketValue{{upperBound: 1, value: 5}, {upperBound: 2, value: 6}}, 170 []bucketValue{{upperBound: 1, value: 5}, {upperBound: 2, value: 6}}, 171 }, 172 { 173 "two nonmonotonic", 174 []bucketValue{{upperBound: 1, value: 5}, {upperBound: 2, value: 4}}, 175 []bucketValue{{upperBound: 1, value: 5}, {upperBound: 2, value: 5}}, 176 }, 177 { 178 "three monotonic", 179 []bucketValue{{upperBound: 1, value: 5}, {upperBound: 2, value: 6}, {upperBound: 3, value: 7}}, 180 []bucketValue{{upperBound: 1, value: 5}, {upperBound: 2, value: 6}, {upperBound: 3, value: 7}}, 181 }, 182 { 183 "three nonmonotonic", 184 []bucketValue{{upperBound: 1, value: 5}, {upperBound: 2, value: 3}, {upperBound: 3, value: 4}}, 185 []bucketValue{{upperBound: 1, value: 5}, {upperBound: 2, value: 5}, {upperBound: 3, value: 5}}, 186 }, 187 { 188 "four nonmonotonic", 189 []bucketValue{{upperBound: 1, value: 5}, {upperBound: 2, value: 3}, {upperBound: 3, value: 6}, {upperBound: 4, value: 3}}, 190 []bucketValue{{upperBound: 1, value: 5}, {upperBound: 2, value: 5}, {upperBound: 3, value: 6}, {upperBound: 4, value: 6}}, 191 }, 192 } 193 194 for _, tt := range tests { 195 t.Run(tt.name, func(t *testing.T) { 196 ensureMonotonic(tt.data) 197 assert.Equal(t, tt.want, tt.data) 198 }) 199 } 200 } 201 202 func TestEnsureMonotonicPreserveNaN(t *testing.T) { 203 data := []bucketValue{ 204 {upperBound: 1, value: 5}, 205 {upperBound: 2, value: 3}, 206 {upperBound: 3, value: math.NaN()}, 207 {upperBound: 4, value: 0}, 208 } 209 ensureMonotonic(data) 210 assert.Equal(t, data[0], bucketValue{upperBound: 1, value: 5}) 211 assert.Equal(t, data[1], bucketValue{upperBound: 2, value: 5}) 212 assert.Equal(t, data[2].upperBound, float64(3)) 213 assert.True(t, math.IsNaN(data[2].value)) 214 assert.Equal(t, data[3], bucketValue{upperBound: 4, value: 5}) 215 } 216 217 func TestBucketQuantile(t *testing.T) { 218 // single bucket returns nan 219 actual := bucketQuantile(0.5, []bucketValue{{upperBound: 1, value: 1}}) 220 assert.True(t, math.IsNaN(actual)) 221 222 // bucket with no infinity returns nan 223 actual = bucketQuantile(0.5, []bucketValue{ 224 {upperBound: 1, value: 1}, 225 {upperBound: 2, value: 2}, 226 }) 227 assert.True(t, math.IsNaN(actual)) 228 229 // bucket with negative infinity bound returns nan 230 actual = bucketQuantile(0.5, []bucketValue{ 231 {upperBound: 1, value: 1}, 232 {upperBound: 2, value: 2}, 233 {upperBound: math.Inf(-1), value: 22}, 234 }) 235 assert.True(t, math.IsNaN(actual)) 236 237 actual = bucketQuantile(0.5, []bucketValue{ 238 {upperBound: 1, value: 1}, 239 {upperBound: math.Inf(1), value: 22}, 240 }) 241 assert.Equal(t, float64(1), actual) 242 243 actual = bucketQuantile(0.8, []bucketValue{ 244 {upperBound: 2, value: 13}, 245 {upperBound: math.Inf(1), value: 71}, 246 }) 247 assert.Equal(t, float64(2), actual) 248 249 // NB: tested against Prom 250 buckets := []bucketValue{ 251 {upperBound: 1, value: 1}, 252 {upperBound: 2, value: 2}, 253 {upperBound: 5, value: 5}, 254 {upperBound: 10, value: 10}, 255 {upperBound: 20, value: 15}, 256 {upperBound: math.Inf(1), value: 16}, 257 } 258 259 actual = bucketQuantile(0, buckets) 260 assert.InDelta(t, float64(0), actual, 0.0001) 261 262 actual = bucketQuantile(0.15, buckets) 263 assert.InDelta(t, 2.4, actual, 0.0001) 264 265 actual = bucketQuantile(0.2, buckets) 266 assert.InDelta(t, float64(3.2), actual, 0.0001) 267 268 actual = bucketQuantile(0.5, buckets) 269 assert.InDelta(t, float64(8), actual, 0.0001) 270 271 actual = bucketQuantile(0.8, buckets) 272 assert.InDelta(t, float64(15.6), actual, 0.0001) 273 274 actual = bucketQuantile(1, buckets) 275 assert.InDelta(t, float64(20), actual, 0.0001) 276 } 277 278 func TestNewOp(t *testing.T) { 279 args := make([]interface{}, 0, 1) 280 _, err := NewHistogramQuantileOp(args, HistogramQuantileType) 281 assert.Error(t, err) 282 283 args = append(args, "invalid") 284 _, err = NewHistogramQuantileOp(args, HistogramQuantileType) 285 assert.Error(t, err) 286 287 args[0] = 2.0 288 _, err = NewHistogramQuantileOp(args, ClampMaxType) 289 assert.Error(t, err) 290 291 op, err := NewHistogramQuantileOp(args, HistogramQuantileType) 292 assert.NoError(t, err) 293 294 assert.Equal(t, HistogramQuantileType, op.OpType()) 295 assert.Equal(t, "type: histogram_quantile", op.String()) 296 } 297 298 func testQuantileFunctionWithQ(t *testing.T, q float64) [][]float64 { 299 args := make([]interface{}, 0, 1) 300 args = append(args, q) 301 op, err := NewHistogramQuantileOp(args, HistogramQuantileType) 302 require.NoError(t, err) 303 304 name := []byte("name") 305 bucket := []byte("bucket") 306 tagOpts := models.NewTagOptions(). 307 SetIDSchemeType(models.TypeQuoted). 308 SetMetricName(name). 309 SetBucketName(bucket) 310 311 tags := models.NewTags(3, tagOpts).SetName([]byte("foo")).AddTag(models.Tag{ 312 Name: []byte("bar"), 313 Value: []byte("baz"), 314 }) 315 316 seriesMetas := []block.SeriesMeta{ 317 {Tags: tags.Clone().SetBucket([]byte("1"))}, 318 {Tags: tags.Clone().SetBucket([]byte("2"))}, 319 {Tags: tags.Clone().SetBucket([]byte("5"))}, 320 {Tags: tags.Clone().SetBucket([]byte("10"))}, 321 {Tags: tags.Clone().SetBucket([]byte("20"))}, 322 {Tags: tags.Clone().SetBucket([]byte("Inf"))}, 323 // this series should not be part of the output, since it has no bucket tag. 324 {Tags: tags.Clone()}, 325 } 326 327 v := [][]float64{ 328 {1, 1, 11, math.NaN(), math.NaN()}, 329 {2, 2, 12, 13, math.NaN()}, 330 {5, 5, 15, math.NaN(), math.NaN()}, 331 {10, 10, 20, math.NaN(), math.NaN()}, 332 {15, 15, 25, math.NaN(), math.NaN()}, 333 {16, 19, math.NaN(), 71, 1}, 334 } 335 336 bounds := models.Bounds{ 337 Start: xtime.Now(), 338 Duration: time.Minute * 5, 339 StepSize: time.Minute, 340 } 341 342 bl := test.NewBlockFromValuesWithSeriesMeta(bounds, seriesMetas, v) 343 c, sink := executor.NewControllerWithSink(parser.NodeID(rune(1))) 344 node := op.(histogramQuantileOp).Node(c, transform.Options{}) 345 err = node.Process(models.NoopQueryContext(), parser.NodeID(rune(0)), bl) 346 require.NoError(t, err) 347 348 return sink.Values 349 } 350 351 var ( 352 inf = math.Inf(+1) 353 ninf = math.Inf(-1) 354 ) 355 356 func TestQuantileFunctionForInvalidQValues(t *testing.T) { 357 actual := testQuantileFunctionWithQ(t, -1) 358 assert.Equal(t, [][]float64{{ninf, ninf, ninf, ninf, ninf}}, actual) 359 actual = testQuantileFunctionWithQ(t, 1.1) 360 assert.Equal(t, [][]float64{{inf, inf, inf, inf, inf}}, actual) 361 362 actual = testQuantileFunctionWithQ(t, 0.8) 363 compare.EqualsWithNansWithDelta(t, [][]float64{{15.6, 20, math.NaN(), 2, math.NaN()}}, actual, 0.00001) 364 } 365 366 func testWithMultipleBuckets(t *testing.T, q float64) [][]float64 { 367 args := make([]interface{}, 0, 1) 368 args = append(args, q) 369 op, err := NewHistogramQuantileOp(args, HistogramQuantileType) 370 require.NoError(t, err) 371 372 name := []byte("name") 373 bucket := []byte("bucket") 374 tagOpts := models.NewTagOptions(). 375 SetIDSchemeType(models.TypeQuoted). 376 SetMetricName(name). 377 SetBucketName(bucket) 378 379 tags := models.NewTags(3, tagOpts).SetName([]byte("foo")).AddTag(models.Tag{ 380 Name: []byte("bar"), 381 Value: []byte("baz"), 382 }) 383 384 tagsTwo := models.NewTags(3, tagOpts).SetName([]byte("qux")).AddTag(models.Tag{ 385 Name: []byte("quaz"), 386 Value: []byte("quail"), 387 }) 388 389 seriesMetas := []block.SeriesMeta{ 390 {Tags: tags.Clone().SetBucket([]byte("1"))}, 391 {Tags: tags.Clone().SetBucket([]byte("2"))}, 392 {Tags: tags.Clone().SetBucket([]byte("5"))}, 393 {Tags: tags.Clone().SetBucket([]byte("10"))}, 394 {Tags: tags.Clone().SetBucket([]byte("20"))}, 395 {Tags: tags.Clone().SetBucket([]byte("Inf"))}, 396 {Tags: tagsTwo.Clone().SetBucket([]byte("1"))}, 397 {Tags: tagsTwo.Clone().SetBucket([]byte("2"))}, 398 {Tags: tagsTwo.Clone().SetBucket([]byte("5"))}, 399 {Tags: tagsTwo.Clone().SetBucket([]byte("10"))}, 400 {Tags: tagsTwo.Clone().SetBucket([]byte("20"))}, 401 {Tags: tagsTwo.Clone().SetBucket([]byte("Inf"))}, 402 } 403 404 v := [][]float64{ 405 {1, 1, 11, math.NaN(), math.NaN()}, 406 {2, 2, 12, 13, math.NaN()}, 407 {5, 5, 15, math.NaN(), math.NaN()}, 408 {10, 10, 20, math.NaN(), math.NaN()}, 409 {15, 15, 25, math.NaN(), math.NaN()}, 410 {16, 19, math.NaN(), 71, 1}, 411 {21, 31, 411, math.NaN(), math.NaN()}, 412 {22, 32, 412, 513, math.NaN()}, 413 {25, 35, 415, math.NaN(), math.NaN()}, 414 {210, 310, 420, math.NaN(), math.NaN()}, 415 {215, 315, 425, math.NaN(), math.NaN()}, 416 {216, 319, math.NaN(), 571, 601}, 417 } 418 419 bounds := models.Bounds{ 420 Start: xtime.Now(), 421 Duration: time.Minute * 5, 422 StepSize: time.Minute, 423 } 424 425 bl := test.NewBlockFromValuesWithSeriesMeta(bounds, seriesMetas, v) 426 c, sink := executor.NewControllerWithSink(parser.NodeID(rune(1))) 427 node := op.(histogramQuantileOp).Node(c, transform.Options{}) 428 err = node.Process(models.NoopQueryContext(), parser.NodeID(rune(0)), bl) 429 require.NoError(t, err) 430 431 return sink.Values 432 } 433 434 func TestQuantileFunctionForMultipleBuckets(t *testing.T) { 435 for i := 0; i < 100; i++ { 436 actual := testWithMultipleBuckets(t, 0.8) 437 expected := [][]float64{ 438 {15.6, 20, math.NaN(), 2, math.NaN()}, 439 {8.99459, 9.00363, math.NaN(), 1.78089, math.NaN()}, 440 } 441 442 compare.EqualsWithNansWithDelta(t, expected, actual, 0.00001) 443 } 444 }