github.com/grafana/pyroscope@v1.18.0/pkg/model/time_series_merger_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/testhelper" 8 ) 9 10 func Test_SeriesMerger(t *testing.T) { 11 for _, tc := range []struct { 12 name string 13 in [][]*typesv1.Series 14 out []*typesv1.Series 15 }{ 16 { 17 name: "empty", 18 in: [][]*typesv1.Series{}, 19 out: []*typesv1.Series(nil), 20 }, 21 { 22 name: "merge two series", 23 in: [][]*typesv1.Series{ 24 { 25 {Labels: LabelsFromStrings("foor", "bar"), Points: []*typesv1.Point{{Timestamp: 1, Value: 1}}}, 26 }, 27 { 28 {Labels: LabelsFromStrings("foor", "bar"), Points: []*typesv1.Point{{Timestamp: 2, Value: 2}}}, 29 }, 30 }, 31 out: []*typesv1.Series{ 32 {Labels: LabelsFromStrings("foor", "bar"), Points: []*typesv1.Point{{Timestamp: 1, Value: 1}, {Timestamp: 2, Value: 2}}}, 33 }, 34 }, 35 { 36 name: "merge multiple series", 37 in: [][]*typesv1.Series{ 38 { 39 {Labels: LabelsFromStrings("foor", "bar"), Points: []*typesv1.Point{{Timestamp: 1, Value: 1}}}, 40 {Labels: LabelsFromStrings("foor", "buzz"), Points: []*typesv1.Point{{Timestamp: 1, Value: 1}}}, 41 }, 42 { 43 {Labels: LabelsFromStrings("foor", "bar"), Points: []*typesv1.Point{{Timestamp: 2, Value: 2}}}, 44 {Labels: LabelsFromStrings("foor", "buzz"), Points: []*typesv1.Point{{Timestamp: 3, Value: 3}}}, 45 }, 46 }, 47 out: []*typesv1.Series{ 48 {Labels: LabelsFromStrings("foor", "bar"), Points: []*typesv1.Point{{Timestamp: 1, Value: 1}, {Timestamp: 2, Value: 2}}}, 49 {Labels: LabelsFromStrings("foor", "buzz"), Points: []*typesv1.Point{{Timestamp: 1, Value: 1}, {Timestamp: 3, Value: 3}}}, 50 }, 51 }, 52 } { 53 t.Run(tc.name, func(t *testing.T) { 54 testhelper.EqualProto(t, tc.out, MergeSeries(nil, tc.in...)) 55 }) 56 } 57 } 58 59 func Test_SeriesMerger_Annotations(t *testing.T) { 60 for _, tc := range []struct { 61 name string 62 in [][]*typesv1.Series 63 out []*typesv1.Series 64 }{ 65 { 66 name: "merge two distinct annotations", 67 in: [][]*typesv1.Series{ 68 { 69 { 70 Labels: LabelsFromStrings("foo", "bar"), 71 Points: []*typesv1.Point{ 72 { 73 Timestamp: 1, 74 Value: 1, 75 Annotations: []*typesv1.ProfileAnnotation{ 76 {Key: "key1", Value: "value1"}, 77 }, 78 }, 79 }, 80 }, 81 }, 82 { 83 { 84 Labels: LabelsFromStrings("foo", "bar"), 85 Points: []*typesv1.Point{ 86 { 87 Timestamp: 1, 88 Value: 2, 89 Annotations: []*typesv1.ProfileAnnotation{ 90 {Key: "key1", Value: "value2"}, 91 }, 92 }, 93 }, 94 }, 95 }, 96 }, 97 out: []*typesv1.Series{ 98 { 99 Labels: LabelsFromStrings("foo", "bar"), 100 Points: []*typesv1.Point{ 101 { 102 Timestamp: 1, 103 Value: 3, 104 Annotations: []*typesv1.ProfileAnnotation{ 105 {Key: "key1", Value: "value1"}, 106 {Key: "key1", Value: "value2"}, 107 }, 108 }, 109 }, 110 }, 111 }, 112 }, 113 { 114 name: "merge duplicate annotations", 115 in: [][]*typesv1.Series{ 116 { 117 { 118 Labels: LabelsFromStrings("foo", "bar"), 119 Points: []*typesv1.Point{ 120 { 121 Timestamp: 1, 122 Value: 1, 123 Annotations: []*typesv1.ProfileAnnotation{ 124 {Key: "key1", Value: "value1"}, 125 {Key: "key2", Value: "value2"}, 126 }, 127 }, 128 }, 129 }, 130 }, 131 { 132 { 133 Labels: LabelsFromStrings("foo", "bar"), 134 Points: []*typesv1.Point{ 135 { 136 Timestamp: 1, 137 Value: 2, 138 Annotations: []*typesv1.ProfileAnnotation{ 139 {Key: "key1", Value: "value1"}, 140 {Key: "key3", Value: "value3"}, 141 }, 142 }, 143 }, 144 }, 145 }, 146 }, 147 out: []*typesv1.Series{ 148 { 149 Labels: LabelsFromStrings("foo", "bar"), 150 Points: []*typesv1.Point{ 151 { 152 Timestamp: 1, 153 Value: 3, 154 Annotations: []*typesv1.ProfileAnnotation{ 155 {Key: "key1", Value: "value1"}, 156 {Key: "key2", Value: "value2"}, 157 {Key: "key3", Value: "value3"}, 158 }, 159 }, 160 }, 161 }, 162 }, 163 }, 164 { 165 name: "merge all duplicate annotations", 166 in: [][]*typesv1.Series{ 167 { 168 { 169 Labels: LabelsFromStrings("foo", "bar"), 170 Points: []*typesv1.Point{ 171 { 172 Timestamp: 1, 173 Value: 1, 174 Annotations: []*typesv1.ProfileAnnotation{ 175 {Key: "key1", Value: "value1"}, 176 {Key: "key2", Value: "value2"}, 177 }, 178 }, 179 }, 180 }, 181 }, 182 { 183 { 184 Labels: LabelsFromStrings("foo", "bar"), 185 Points: []*typesv1.Point{ 186 { 187 Timestamp: 1, 188 Value: 2, 189 Annotations: []*typesv1.ProfileAnnotation{ 190 {Key: "key1", Value: "value1"}, 191 {Key: "key2", Value: "value2"}, 192 }, 193 }, 194 }, 195 }, 196 }, 197 }, 198 out: []*typesv1.Series{ 199 { 200 Labels: LabelsFromStrings("foo", "bar"), 201 Points: []*typesv1.Point{ 202 { 203 Timestamp: 1, 204 Value: 3, 205 Annotations: []*typesv1.ProfileAnnotation{ 206 {Key: "key1", Value: "value1"}, 207 {Key: "key2", Value: "value2"}, 208 }, 209 }, 210 }, 211 }, 212 }, 213 }, 214 { 215 name: "annotations sorted by key then value", 216 in: [][]*typesv1.Series{ 217 { 218 { 219 Labels: LabelsFromStrings("foo", "bar"), 220 Points: []*typesv1.Point{ 221 { 222 Timestamp: 1, 223 Value: 1, 224 Annotations: []*typesv1.ProfileAnnotation{ 225 {Key: "z", Value: "last"}, 226 {Key: "a", Value: "first"}, 227 }, 228 }, 229 }, 230 }, 231 }, 232 { 233 { 234 Labels: LabelsFromStrings("foo", "bar"), 235 Points: []*typesv1.Point{ 236 { 237 Timestamp: 1, 238 Value: 2, 239 Annotations: []*typesv1.ProfileAnnotation{ 240 {Key: "m", Value: "middle"}, 241 }, 242 }, 243 }, 244 }, 245 }, 246 }, 247 out: []*typesv1.Series{ 248 { 249 Labels: LabelsFromStrings("foo", "bar"), 250 Points: []*typesv1.Point{ 251 { 252 Timestamp: 1, 253 Value: 3, 254 Annotations: []*typesv1.ProfileAnnotation{ 255 {Key: "a", Value: "first"}, 256 {Key: "m", Value: "middle"}, 257 {Key: "z", Value: "last"}, 258 }, 259 }, 260 }, 261 }, 262 }, 263 }, 264 { 265 name: "empty annotations on one side", 266 in: [][]*typesv1.Series{ 267 { 268 { 269 Labels: LabelsFromStrings("foo", "bar"), 270 Points: []*typesv1.Point{ 271 { 272 Timestamp: 1, 273 Value: 1, 274 Annotations: []*typesv1.ProfileAnnotation{}, 275 }, 276 }, 277 }, 278 }, 279 { 280 { 281 Labels: LabelsFromStrings("foo", "bar"), 282 Points: []*typesv1.Point{ 283 { 284 Timestamp: 1, 285 Value: 2, 286 Annotations: []*typesv1.ProfileAnnotation{ 287 {Key: "key1", Value: "value1"}, 288 }, 289 }, 290 }, 291 }, 292 }, 293 }, 294 out: []*typesv1.Series{ 295 { 296 Labels: LabelsFromStrings("foo", "bar"), 297 Points: []*typesv1.Point{ 298 { 299 Timestamp: 1, 300 Value: 3, 301 Annotations: []*typesv1.ProfileAnnotation{ 302 {Key: "key1", Value: "value1"}, 303 }, 304 }, 305 }, 306 }, 307 }, 308 }, 309 } { 310 t.Run(tc.name, func(t *testing.T) { 311 testhelper.EqualProto(t, tc.out, MergeSeries(nil, tc.in...)) 312 }) 313 } 314 } 315 316 func Test_SeriesMerger_Overlap_Sum(t *testing.T) { 317 for _, tc := range []struct { 318 name string 319 in [][]*typesv1.Series 320 out []*typesv1.Series 321 }{ 322 { 323 name: "merge deduplicate overlapping series", 324 in: [][]*typesv1.Series{ 325 { 326 {Labels: LabelsFromStrings("foo", "bar"), Points: []*typesv1.Point{{Timestamp: 2, Value: 1}, {Timestamp: 3, Value: 1}}}, 327 {Labels: LabelsFromStrings("foo", "baz"), Points: []*typesv1.Point{{Timestamp: 1, Value: 1}, {Timestamp: 2, Value: 1}}}, 328 }, 329 { 330 {Labels: LabelsFromStrings("foo", "bar"), Points: []*typesv1.Point{{Timestamp: 1, Value: 1}, {Timestamp: 2, Value: 1}}}, 331 {Labels: LabelsFromStrings("foo", "baz"), Points: []*typesv1.Point{{Timestamp: 2, Value: 1}, {Timestamp: 3, Value: 1}}}, 332 }, 333 }, 334 out: []*typesv1.Series{ 335 {Labels: LabelsFromStrings("foo", "bar"), Points: []*typesv1.Point{{Timestamp: 1, Value: 1}, {Timestamp: 2, Value: 2}, {Timestamp: 3, Value: 1}}}, 336 {Labels: LabelsFromStrings("foo", "baz"), Points: []*typesv1.Point{{Timestamp: 1, Value: 1}, {Timestamp: 2, Value: 2}, {Timestamp: 3, Value: 1}}}, 337 }, 338 }, 339 } { 340 t.Run(tc.name, func(t *testing.T) { 341 testhelper.EqualProto(t, tc.out, MergeSeries(nil, tc.in...)) 342 }) 343 } 344 } 345 346 func Test_SeriesMerger_Top(t *testing.T) { 347 for _, tc := range []struct { 348 name string 349 in [][]*typesv1.Series 350 out []*typesv1.Series 351 top int 352 }{ 353 { 354 name: "top == len", 355 in: [][]*typesv1.Series{ 356 { 357 {Labels: LabelsFromStrings("foo", "bar"), Points: []*typesv1.Point{{Timestamp: 2, Value: 1}, {Timestamp: 3, Value: 1}}}, 358 {Labels: LabelsFromStrings("foo", "baz"), Points: []*typesv1.Point{{Timestamp: 1, Value: 1}, {Timestamp: 2, Value: 1}}}, 359 }, 360 { 361 {Labels: LabelsFromStrings("foo", "bar"), Points: []*typesv1.Point{{Timestamp: 1, Value: 1}, {Timestamp: 2, Value: 1}}}, 362 {Labels: LabelsFromStrings("foo", "baz"), Points: []*typesv1.Point{{Timestamp: 2, Value: 1}, {Timestamp: 3, Value: 2}}}, 363 }, 364 }, 365 top: 2, 366 out: []*typesv1.Series{ 367 {Labels: LabelsFromStrings("foo", "baz"), Points: []*typesv1.Point{{Timestamp: 1, Value: 1}, {Timestamp: 2, Value: 2}, {Timestamp: 3, Value: 2}}}, 368 {Labels: LabelsFromStrings("foo", "bar"), Points: []*typesv1.Point{{Timestamp: 1, Value: 1}, {Timestamp: 2, Value: 2}, {Timestamp: 3, Value: 1}}}, 369 }, 370 }, 371 { 372 name: "top < len", 373 in: [][]*typesv1.Series{ 374 { 375 {Labels: LabelsFromStrings("foo", "bar"), Points: []*typesv1.Point{{Timestamp: 2, Value: 1}, {Timestamp: 3, Value: 1}}}, 376 {Labels: LabelsFromStrings("foo", "baz"), Points: []*typesv1.Point{{Timestamp: 1, Value: 1}, {Timestamp: 2, Value: 1}}}, 377 }, 378 { 379 {Labels: LabelsFromStrings("foo", "bar"), Points: []*typesv1.Point{{Timestamp: 1, Value: 1}, {Timestamp: 2, Value: 1}}}, 380 {Labels: LabelsFromStrings("foo", "baz"), Points: []*typesv1.Point{{Timestamp: 2, Value: 1}, {Timestamp: 3, Value: 2}}}, 381 }, 382 }, 383 top: 1, 384 out: []*typesv1.Series{ 385 {Labels: LabelsFromStrings("foo", "baz"), Points: []*typesv1.Point{{Timestamp: 1, Value: 1}, {Timestamp: 2, Value: 2}, {Timestamp: 3, Value: 2}}}, 386 }, 387 }, 388 { 389 name: "top > len", 390 in: [][]*typesv1.Series{ 391 { 392 {Labels: LabelsFromStrings("foo", "bar"), Points: []*typesv1.Point{{Timestamp: 2, Value: 1}, {Timestamp: 3, Value: 1}}}, 393 {Labels: LabelsFromStrings("foo", "baz"), Points: []*typesv1.Point{{Timestamp: 1, Value: 1}, {Timestamp: 2, Value: 1}}}, 394 }, 395 { 396 {Labels: LabelsFromStrings("foo", "bar"), Points: []*typesv1.Point{{Timestamp: 1, Value: 1}, {Timestamp: 2, Value: 1}}}, 397 {Labels: LabelsFromStrings("foo", "baz"), Points: []*typesv1.Point{{Timestamp: 2, Value: 1}, {Timestamp: 3, Value: 2}}}, 398 }, 399 }, 400 top: 3, 401 out: []*typesv1.Series{ 402 {Labels: LabelsFromStrings("foo", "baz"), Points: []*typesv1.Point{{Timestamp: 1, Value: 1}, {Timestamp: 2, Value: 2}, {Timestamp: 3, Value: 2}}}, 403 {Labels: LabelsFromStrings("foo", "bar"), Points: []*typesv1.Point{{Timestamp: 1, Value: 1}, {Timestamp: 2, Value: 2}, {Timestamp: 3, Value: 1}}}, 404 }, 405 }, 406 { 407 name: "order", 408 in: [][]*typesv1.Series{ 409 { 410 {Labels: LabelsFromStrings("foo", "d"), Points: []*typesv1.Point{{Timestamp: 1, Value: 1}, {Timestamp: 2, Value: 1}}}, 411 {Labels: LabelsFromStrings("foo", "e"), Points: []*typesv1.Point{{Timestamp: 2, Value: 1}, {Timestamp: 3, Value: 1}}}, 412 {Labels: LabelsFromStrings("foo", "c"), Points: []*typesv1.Point{{Timestamp: 1, Value: 1}, {Timestamp: 2, Value: 1}}}, 413 {Labels: LabelsFromStrings("foo", "a"), Points: []*typesv1.Point{{Timestamp: 2, Value: 1}, {Timestamp: 3, Value: 2}}}, 414 {Labels: LabelsFromStrings("foo", "b"), Points: []*typesv1.Point{{Timestamp: 2, Value: 1}, {Timestamp: 3, Value: 3}}}, 415 }, 416 }, 417 top: 4, 418 out: []*typesv1.Series{ 419 {Labels: LabelsFromStrings("foo", "b"), Points: []*typesv1.Point{{Timestamp: 2, Value: 1}, {Timestamp: 3, Value: 3}}}, 420 {Labels: LabelsFromStrings("foo", "a"), Points: []*typesv1.Point{{Timestamp: 2, Value: 1}, {Timestamp: 3, Value: 2}}}, 421 {Labels: LabelsFromStrings("foo", "c"), Points: []*typesv1.Point{{Timestamp: 1, Value: 1}, {Timestamp: 2, Value: 1}}}, 422 {Labels: LabelsFromStrings("foo", "d"), Points: []*typesv1.Point{{Timestamp: 1, Value: 1}, {Timestamp: 2, Value: 1}}}, 423 }, 424 }, 425 { 426 name: "k == 0", 427 in: [][]*typesv1.Series{ 428 { 429 {Labels: LabelsFromStrings("foo", "d"), Points: []*typesv1.Point{{Timestamp: 1, Value: 1}, {Timestamp: 2, Value: 1}}}, 430 {Labels: LabelsFromStrings("foo", "c"), Points: []*typesv1.Point{{Timestamp: 1, Value: 1}, {Timestamp: 2, Value: 1}}}, 431 {Labels: LabelsFromStrings("foo", "a"), Points: []*typesv1.Point{{Timestamp: 2, Value: 1}, {Timestamp: 3, Value: 2}}}, 432 {Labels: LabelsFromStrings("foo", "b"), Points: []*typesv1.Point{{Timestamp: 2, Value: 1}, {Timestamp: 3, Value: 3}}}, 433 }, 434 }, 435 top: 0, 436 out: []*typesv1.Series{ 437 {Labels: LabelsFromStrings("foo", "b"), Points: []*typesv1.Point{{Timestamp: 2, Value: 1}, {Timestamp: 3, Value: 3}}}, 438 {Labels: LabelsFromStrings("foo", "a"), Points: []*typesv1.Point{{Timestamp: 2, Value: 1}, {Timestamp: 3, Value: 2}}}, 439 {Labels: LabelsFromStrings("foo", "c"), Points: []*typesv1.Point{{Timestamp: 1, Value: 1}, {Timestamp: 2, Value: 1}}}, 440 {Labels: LabelsFromStrings("foo", "d"), Points: []*typesv1.Point{{Timestamp: 1, Value: 1}, {Timestamp: 2, Value: 1}}}, 441 }, 442 }, 443 } { 444 t.Run(tc.name, func(t *testing.T) { 445 m := NewTimeSeriesMerger(true) 446 for _, s := range tc.in { 447 m.MergeTimeSeries(s) 448 } 449 testhelper.EqualProto(t, tc.out, m.Top(tc.top)) 450 }) 451 } 452 } 453 454 func Test_SeriesMerger_WithExemplars(t *testing.T) { 455 for _, tc := range []struct { 456 name string 457 in [][]*typesv1.Series 458 out []*typesv1.Series 459 }{ 460 { 461 name: "merge keeps highest value exemplar per profile ID", 462 in: [][]*typesv1.Series{ 463 { 464 { 465 Labels: LabelsFromStrings("foo", "bar"), 466 Points: []*typesv1.Point{ 467 { 468 Timestamp: 1, 469 Value: 10, 470 Exemplars: []*typesv1.Exemplar{ 471 {ProfileId: "prof-1", Value: 100, Timestamp: 1}, 472 }, 473 }, 474 }, 475 }, 476 }, 477 { 478 { 479 Labels: LabelsFromStrings("foo", "bar"), 480 Points: []*typesv1.Point{ 481 { 482 Timestamp: 1, 483 Value: 20, 484 Exemplars: []*typesv1.Exemplar{ 485 {ProfileId: "prof-1", Value: 500, Timestamp: 1}, 486 {ProfileId: "prof-2", Value: 200, Timestamp: 1}, 487 }, 488 }, 489 }, 490 }, 491 }, 492 }, 493 out: []*typesv1.Series{ 494 { 495 Labels: LabelsFromStrings("foo", "bar"), 496 Points: []*typesv1.Point{ 497 { 498 Timestamp: 1, 499 Value: 30, 500 Exemplars: []*typesv1.Exemplar{ 501 {ProfileId: "prof-1", Value: 500, Timestamp: 1}, 502 {ProfileId: "prof-2", Value: 200, Timestamp: 1}, 503 }, 504 }, 505 }, 506 }, 507 }, 508 }, 509 { 510 name: "merge preserves exemplar labels", 511 in: [][]*typesv1.Series{ 512 { 513 { 514 Labels: LabelsFromStrings("service_name", "api"), 515 Points: []*typesv1.Point{ 516 { 517 Timestamp: 1000, 518 Value: 100, 519 Exemplars: []*typesv1.Exemplar{ 520 { 521 ProfileId: "prof-1", 522 Value: 100, 523 Timestamp: 1000, 524 Labels: []*typesv1.LabelPair{{Name: "pod", Value: "pod-123"}}, 525 }, 526 }, 527 }, 528 }, 529 }, 530 }, 531 }, 532 out: []*typesv1.Series{ 533 { 534 Labels: LabelsFromStrings("service_name", "api"), 535 Points: []*typesv1.Point{ 536 { 537 Timestamp: 1000, 538 Value: 100, 539 Exemplars: []*typesv1.Exemplar{ 540 { 541 ProfileId: "prof-1", 542 Value: 100, 543 Timestamp: 1000, 544 Labels: []*typesv1.LabelPair{{Name: "pod", Value: "pod-123"}}, 545 }, 546 }, 547 }, 548 }, 549 }, 550 }, 551 }, 552 } { 553 t.Run(tc.name, func(t *testing.T) { 554 testhelper.EqualProto(t, tc.out, MergeSeries(nil, tc.in...)) 555 }) 556 } 557 }