github.com/grafana/pyroscope@v1.18.0/pkg/model/time_series_test.go (about)

     1  package model
     2  
     3  import (
     4  	"testing"
     5  
     6  	typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1"
     7  	"github.com/grafana/pyroscope/pkg/iter"
     8  	"github.com/grafana/pyroscope/pkg/testhelper"
     9  )
    10  
    11  func Test_RangeSeriesSum(t *testing.T) {
    12  	seriesA := NewLabelsBuilder(nil).Set("foo", "bar").Labels()
    13  	seriesB := NewLabelsBuilder(nil).Set("foo", "buzz").Labels()
    14  	for _, tc := range []struct {
    15  		name string
    16  		in   []TimeSeriesValue
    17  		out  []*typesv1.Series
    18  	}{
    19  		{
    20  			name: "single series",
    21  			in: []TimeSeriesValue{
    22  				{Ts: 1, Value: 1},
    23  				{Ts: 1, Value: 1},
    24  				{Ts: 2, Value: 2},
    25  				{Ts: 3, Value: 3},
    26  				{Ts: 4, Value: 4},
    27  				{Ts: 5, Value: 5, Annotations: []*typesv1.ProfileAnnotation{{Key: "foo", Value: "bar"}}},
    28  			},
    29  			out: []*typesv1.Series{
    30  				{
    31  					Points: []*typesv1.Point{
    32  						{Timestamp: 1, Value: 2, Annotations: []*typesv1.ProfileAnnotation{}},
    33  						{Timestamp: 2, Value: 2, Annotations: []*typesv1.ProfileAnnotation{}},
    34  						{Timestamp: 3, Value: 3, Annotations: []*typesv1.ProfileAnnotation{}},
    35  						{Timestamp: 4, Value: 4, Annotations: []*typesv1.ProfileAnnotation{}},
    36  						{Timestamp: 5, Value: 5, Annotations: []*typesv1.ProfileAnnotation{{Key: "foo", Value: "bar"}}},
    37  					},
    38  				},
    39  			},
    40  		},
    41  		{
    42  			name: "multiple series",
    43  			in: []TimeSeriesValue{
    44  				{Ts: 1, Value: 1, Lbs: seriesA, LabelsHash: seriesA.Hash()},
    45  				{Ts: 1, Value: 1, Lbs: seriesB, LabelsHash: seriesB.Hash()},
    46  				{Ts: 2, Value: 1, Lbs: seriesA, LabelsHash: seriesA.Hash()},
    47  				{Ts: 3, Value: 1, Lbs: seriesB, LabelsHash: seriesB.Hash()},
    48  				{Ts: 3, Value: 1, Lbs: seriesB, LabelsHash: seriesB.Hash()},
    49  				{Ts: 4, Value: 4, Lbs: seriesB, LabelsHash: seriesB.Hash(), Annotations: []*typesv1.ProfileAnnotation{{Key: "foo", Value: "bar"}}},
    50  				{Ts: 4, Value: 4, Lbs: seriesB, LabelsHash: seriesB.Hash(), Annotations: []*typesv1.ProfileAnnotation{{Key: "foo", Value: "buzz"}}},
    51  				{Ts: 4, Value: 4, Lbs: seriesA, LabelsHash: seriesA.Hash()},
    52  				{Ts: 5, Value: 5, Lbs: seriesA, LabelsHash: seriesA.Hash()},
    53  			},
    54  			out: []*typesv1.Series{
    55  				{
    56  					Labels: seriesA,
    57  					Points: []*typesv1.Point{
    58  						{Timestamp: 1, Value: 1, Annotations: []*typesv1.ProfileAnnotation{}},
    59  						{Timestamp: 2, Value: 1, Annotations: []*typesv1.ProfileAnnotation{}},
    60  						{Timestamp: 4, Value: 4, Annotations: []*typesv1.ProfileAnnotation{}},
    61  						{Timestamp: 5, Value: 5, Annotations: []*typesv1.ProfileAnnotation{}},
    62  					},
    63  				},
    64  				{
    65  					Labels: seriesB,
    66  					Points: []*typesv1.Point{
    67  						{Timestamp: 1, Value: 1, Annotations: []*typesv1.ProfileAnnotation{}},
    68  						{Timestamp: 3, Value: 2, Annotations: []*typesv1.ProfileAnnotation{}},
    69  						{Timestamp: 4, Value: 8, Annotations: []*typesv1.ProfileAnnotation{
    70  							{Key: "foo", Value: "bar"},
    71  							{Key: "foo", Value: "buzz"}}},
    72  					},
    73  				},
    74  			},
    75  		},
    76  	} {
    77  		t.Run(tc.name, func(t *testing.T) {
    78  			in := iter.NewSliceIterator(tc.in)
    79  			out := RangeSeries(in, 1, 5, 1, nil)
    80  			testhelper.EqualProto(t, tc.out, out)
    81  		})
    82  	}
    83  }
    84  
    85  func Test_RangeSeriesAvg(t *testing.T) {
    86  	seriesA := NewLabelsBuilder(nil).Set("foo", "bar").Labels()
    87  	seriesB := NewLabelsBuilder(nil).Set("foo", "buzz").Labels()
    88  	for _, tc := range []struct {
    89  		name string
    90  		in   []TimeSeriesValue
    91  		out  []*typesv1.Series
    92  	}{
    93  		{
    94  			name: "single series",
    95  			in: []TimeSeriesValue{
    96  				{Ts: 1, Value: 1},
    97  				{Ts: 1, Value: 2},
    98  				{Ts: 2, Value: 2},
    99  				{Ts: 2, Value: 3},
   100  				{Ts: 3, Value: 4},
   101  				{Ts: 4, Value: 5, Annotations: []*typesv1.ProfileAnnotation{{Key: "foo", Value: "bar"}}},
   102  			},
   103  			out: []*typesv1.Series{
   104  				{
   105  					Points: []*typesv1.Point{
   106  						{Timestamp: 1, Value: 1.5, Annotations: []*typesv1.ProfileAnnotation{}}, // avg of 1 and 2
   107  						{Timestamp: 2, Value: 2.5, Annotations: []*typesv1.ProfileAnnotation{}}, // avg of 2 and 3
   108  						{Timestamp: 3, Value: 4, Annotations: []*typesv1.ProfileAnnotation{}},
   109  						{Timestamp: 4, Value: 5, Annotations: []*typesv1.ProfileAnnotation{{Key: "foo", Value: "bar"}}},
   110  					},
   111  				},
   112  			},
   113  		},
   114  		{
   115  			name: "multiple series",
   116  			in: []TimeSeriesValue{
   117  				{Ts: 1, Value: 1, Lbs: seriesA, LabelsHash: seriesA.Hash()},
   118  				{Ts: 1, Value: 1, Lbs: seriesB, LabelsHash: seriesB.Hash()},
   119  				{Ts: 2, Value: 1, Lbs: seriesA, LabelsHash: seriesA.Hash()},
   120  				{Ts: 2, Value: 2, Lbs: seriesA, LabelsHash: seriesA.Hash()},
   121  				{Ts: 3, Value: 1, Lbs: seriesB, LabelsHash: seriesB.Hash()},
   122  				{Ts: 3, Value: 2, Lbs: seriesB, LabelsHash: seriesB.Hash()},
   123  				{Ts: 4, Value: 4, Lbs: seriesB, LabelsHash: seriesB.Hash()},
   124  				{Ts: 4, Value: 6, Lbs: seriesB, LabelsHash: seriesB.Hash()},
   125  				{Ts: 4, Value: 4, Lbs: seriesA, LabelsHash: seriesA.Hash()},
   126  				{Ts: 5, Value: 5, Lbs: seriesA, LabelsHash: seriesA.Hash()},
   127  			},
   128  			out: []*typesv1.Series{
   129  				{
   130  					Labels: seriesA,
   131  					Points: []*typesv1.Point{
   132  						{Timestamp: 1, Value: 1, Annotations: []*typesv1.ProfileAnnotation{}},
   133  						{Timestamp: 2, Value: 1.5, Annotations: []*typesv1.ProfileAnnotation{}}, // avg of 1 and 2
   134  						{Timestamp: 4, Value: 4, Annotations: []*typesv1.ProfileAnnotation{}},
   135  						{Timestamp: 5, Value: 5, Annotations: []*typesv1.ProfileAnnotation{}},
   136  					},
   137  				},
   138  				{
   139  					Labels: seriesB,
   140  					Points: []*typesv1.Point{
   141  						{Timestamp: 1, Value: 1, Annotations: []*typesv1.ProfileAnnotation{}},
   142  						{Timestamp: 3, Value: 1.5, Annotations: []*typesv1.ProfileAnnotation{}}, // avg of 1 and 2
   143  						{Timestamp: 4, Value: 5, Annotations: []*typesv1.ProfileAnnotation{}},   // avg of 4 and 6
   144  					},
   145  				},
   146  			},
   147  		},
   148  	} {
   149  		t.Run(tc.name, func(t *testing.T) {
   150  			in := iter.NewSliceIterator(tc.in)
   151  			aggregation := typesv1.TimeSeriesAggregationType_TIME_SERIES_AGGREGATION_TYPE_AVERAGE
   152  			out := RangeSeries(in, 1, 5, 1, &aggregation)
   153  			testhelper.EqualProto(t, tc.out, out)
   154  		})
   155  	}
   156  }
   157  
   158  func Test_RangeSeriesWithExemplars(t *testing.T) {
   159  	sum := typesv1.TimeSeriesAggregationType_TIME_SERIES_AGGREGATION_TYPE_SUM
   160  
   161  	for _, tc := range []struct {
   162  		name         string
   163  		series       []*typesv1.Series
   164  		start        int64
   165  		end          int64
   166  		step         int64
   167  		aggregation  *typesv1.TimeSeriesAggregationType
   168  		maxExemplars int // 0 means use default
   169  		out          []*typesv1.Series
   170  	}{
   171  		{
   172  			name: "exemplar timestamps preserved during aggregation",
   173  			series: []*typesv1.Series{{
   174  				Labels: []*typesv1.LabelPair{{Name: "service_name", Value: "api"}},
   175  				Points: []*typesv1.Point{
   176  					{Timestamp: 947, Value: 100.0, Exemplars: []*typesv1.Exemplar{{ProfileId: "prof-1", Value: 100, Timestamp: 947}}},
   177  					{Timestamp: 987, Value: 300.0, Exemplars: []*typesv1.Exemplar{{ProfileId: "prof-2", Value: 300, Timestamp: 987}}},
   178  					{Timestamp: 1847, Value: 200.0, Exemplars: []*typesv1.Exemplar{{ProfileId: "prof-3", Value: 200, Timestamp: 1847}}},
   179  				},
   180  			}},
   181  			start:       1000,
   182  			end:         3000,
   183  			step:        1000,
   184  			aggregation: &sum,
   185  			out: []*typesv1.Series{{
   186  				Labels: []*typesv1.LabelPair{{Name: "service_name", Value: "api"}},
   187  				Points: []*typesv1.Point{
   188  					{Timestamp: 1000, Value: 400.0, Annotations: []*typesv1.ProfileAnnotation{}, Exemplars: []*typesv1.Exemplar{{ProfileId: "prof-2", Value: 300, Timestamp: 987}}},
   189  					{Timestamp: 2000, Value: 200.0, Annotations: []*typesv1.ProfileAnnotation{}, Exemplars: []*typesv1.Exemplar{{ProfileId: "prof-3", Value: 200, Timestamp: 1847}}},
   190  				},
   191  			}},
   192  		},
   193  		{
   194  			name: "exemplar labels preserved through re-aggregation",
   195  			series: []*typesv1.Series{{
   196  				Labels: []*typesv1.LabelPair{{Name: "service_name", Value: "api"}},
   197  				Points: []*typesv1.Point{
   198  					{
   199  						Timestamp: 1000,
   200  						Value:     100.0,
   201  						Exemplars: []*typesv1.Exemplar{{
   202  							ProfileId: "prof-1",
   203  							Value:     100,
   204  							Timestamp: 1000,
   205  							Labels:    []*typesv1.LabelPair{{Name: "pod", Value: "pod-123"}, {Name: "region", Value: "us-east"}},
   206  						}},
   207  					},
   208  				},
   209  			}},
   210  			start:       1000,
   211  			end:         2000,
   212  			step:        1000,
   213  			aggregation: &sum,
   214  			out: []*typesv1.Series{{
   215  				Labels: []*typesv1.LabelPair{{Name: "service_name", Value: "api"}},
   216  				Points: []*typesv1.Point{
   217  					{
   218  						Timestamp:   1000,
   219  						Value:       100.0,
   220  						Annotations: []*typesv1.ProfileAnnotation{},
   221  						Exemplars: []*typesv1.Exemplar{{
   222  							ProfileId: "prof-1",
   223  							Value:     100,
   224  							Timestamp: 1000,
   225  							Labels:    []*typesv1.LabelPair{{Name: "pod", Value: "pod-123"}, {Name: "region", Value: "us-east"}},
   226  						}},
   227  					},
   228  				},
   229  			}},
   230  		},
   231  		{
   232  			name: "multi-block path supports top-2 exemplars",
   233  			series: []*typesv1.Series{{
   234  				Labels: []*typesv1.LabelPair{{Name: "service_name", Value: "api"}},
   235  				Points: []*typesv1.Point{
   236  					{
   237  						Timestamp: 1000,
   238  						Value:     100.0,
   239  						Exemplars: []*typesv1.Exemplar{
   240  							{ProfileId: "prof-1", Value: 100, Timestamp: 1000, Labels: []*typesv1.LabelPair{{Name: "pod", Value: "pod-1"}}},
   241  							{ProfileId: "prof-2", Value: 200, Timestamp: 1000, Labels: []*typesv1.LabelPair{{Name: "pod", Value: "pod-2"}}},
   242  						},
   243  					},
   244  					{
   245  						Timestamp: 1000,
   246  						Value:     150.0,
   247  						Exemplars: []*typesv1.Exemplar{
   248  							{ProfileId: "prof-3", Value: 300, Timestamp: 1000, Labels: []*typesv1.LabelPair{{Name: "pod", Value: "pod-3"}}},
   249  							{ProfileId: "prof-4", Value: 50, Timestamp: 1000, Labels: []*typesv1.LabelPair{{Name: "pod", Value: "pod-4"}}},
   250  						},
   251  					},
   252  				},
   253  			}},
   254  			start:        1000,
   255  			end:          2000,
   256  			step:         1000,
   257  			aggregation:  &sum,
   258  			maxExemplars: 2,
   259  			out: []*typesv1.Series{{
   260  				Labels: []*typesv1.LabelPair{{Name: "service_name", Value: "api"}},
   261  				Points: []*typesv1.Point{
   262  					{
   263  						Timestamp:   1000,
   264  						Value:       250.0,
   265  						Annotations: []*typesv1.ProfileAnnotation{},
   266  						Exemplars: []*typesv1.Exemplar{
   267  							{ProfileId: "prof-3", Value: 300, Timestamp: 1000, Labels: []*typesv1.LabelPair{{Name: "pod", Value: "pod-3"}}},
   268  							{ProfileId: "prof-2", Value: 200, Timestamp: 1000, Labels: []*typesv1.LabelPair{{Name: "pod", Value: "pod-2"}}},
   269  						},
   270  					},
   271  				},
   272  			}},
   273  		},
   274  		{
   275  			name: "same profileID across blocks - keeps highest value and intersects labels",
   276  			series: []*typesv1.Series{{
   277  				Labels: []*typesv1.LabelPair{{Name: "service_name", Value: "api"}},
   278  				Points: []*typesv1.Point{
   279  					{
   280  						Timestamp: 1000,
   281  						Value:     100.0,
   282  						Exemplars: []*typesv1.Exemplar{
   283  							{ProfileId: "Profile-X", Value: 100, Timestamp: 1000, Labels: []*typesv1.LabelPair{{Name: "block", Value: "A"}}},
   284  							{ProfileId: "Profile-Y", Value: 60, Timestamp: 1000, Labels: []*typesv1.LabelPair{{Name: "block", Value: "A"}}},
   285  							{ProfileId: "Profile-Z", Value: 40, Timestamp: 1000, Labels: []*typesv1.LabelPair{{Name: "block", Value: "A"}}},
   286  						},
   287  					},
   288  					{
   289  						Timestamp: 1000,
   290  						Value:     140.0,
   291  						Exemplars: []*typesv1.Exemplar{
   292  							{ProfileId: "Profile-X", Value: 20, Timestamp: 1000, Labels: []*typesv1.LabelPair{{Name: "block", Value: "B"}}},
   293  							{ProfileId: "Profile-Y", Value: 30, Timestamp: 1000, Labels: []*typesv1.LabelPair{{Name: "block", Value: "B"}}},
   294  							{ProfileId: "Profile-Z", Value: 90, Timestamp: 1000, Labels: []*typesv1.LabelPair{{Name: "block", Value: "B"}}},
   295  						},
   296  					},
   297  					{
   298  						Timestamp: 1000,
   299  						Value:     105.0,
   300  						Exemplars: []*typesv1.Exemplar{
   301  							{ProfileId: "Profile-X", Value: 10, Timestamp: 1000, Labels: []*typesv1.LabelPair{{Name: "block", Value: "C"}}},
   302  							{ProfileId: "Profile-Y", Value: 80, Timestamp: 1000, Labels: []*typesv1.LabelPair{{Name: "block", Value: "C"}}},
   303  							{ProfileId: "Profile-Z", Value: 15, Timestamp: 1000, Labels: []*typesv1.LabelPair{{Name: "block", Value: "C"}}},
   304  						},
   305  					},
   306  				},
   307  			}},
   308  			start:       1000,
   309  			end:         2000,
   310  			step:        1000,
   311  			aggregation: &sum,
   312  			out: []*typesv1.Series{{
   313  				Labels: []*typesv1.LabelPair{{Name: "service_name", Value: "api"}},
   314  				Points: []*typesv1.Point{
   315  					{
   316  						Timestamp:   1000,
   317  						Value:       345.0, // 100+140+105
   318  						Annotations: []*typesv1.ProfileAnnotation{},
   319  						// Profile-X has highest value (100 from block A), but labels differ across blocks (A/B/C), so intersection is nil
   320  						Exemplars: []*typesv1.Exemplar{
   321  							{ProfileId: "Profile-X", Value: 100, Timestamp: 1000},
   322  						},
   323  					},
   324  				},
   325  			}},
   326  		},
   327  	} {
   328  		t.Run(tc.name, func(t *testing.T) {
   329  			iter := NewTimeSeriesMergeIterator(tc.series)
   330  			var result []*typesv1.Series
   331  			if tc.maxExemplars > 0 {
   332  				result = rangeSeriesWithLimit(iter, tc.start, tc.end, tc.step, tc.aggregation, tc.maxExemplars)
   333  			} else {
   334  				result = RangeSeries(iter, tc.start, tc.end, tc.step, tc.aggregation)
   335  			}
   336  			testhelper.EqualProto(t, tc.out, result)
   337  		})
   338  	}
   339  }