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 }