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  }