github.com/grafana/pyroscope@v1.18.0/pkg/model/tree_test.go (about) 1 package model 2 3 import ( 4 "bytes" 5 "math" 6 "testing" 7 8 "github.com/stretchr/testify/assert" 9 "github.com/stretchr/testify/require" 10 11 profilev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1" 12 ) 13 14 func Test_Tree(t *testing.T) { 15 for _, tc := range []struct { 16 name string 17 stacks []stacktraces 18 expected func() *Tree 19 }{ 20 { 21 "empty", 22 []stacktraces{}, 23 func() *Tree { return &Tree{} }, 24 }, 25 { 26 "double node single stack", 27 []stacktraces{ 28 { 29 locations: []string{"buz", "bar"}, 30 value: 1, 31 }, 32 { 33 locations: []string{"buz", "bar"}, 34 value: 1, 35 }, 36 }, 37 func() *Tree { 38 tr := emptyTree() 39 tr.add("bar", 0, 2).add("buz", 2, 2) 40 return tr 41 }, 42 }, 43 { 44 "double node double stack", 45 []stacktraces{ 46 { 47 locations: []string{"blip", "buz", "bar"}, 48 value: 1, 49 }, 50 { 51 locations: []string{"blap", "blop", "buz", "bar"}, 52 value: 2, 53 }, 54 }, 55 func() *Tree { 56 tr := emptyTree() 57 buz := tr.add("bar", 0, 3).add("buz", 0, 3) 58 buz.add("blip", 1, 1) 59 buz.add("blop", 0, 2).add("blap", 2, 2) 60 return tr 61 }, 62 }, 63 { 64 "multiple stacks and duplicates nodes", 65 []stacktraces{ 66 { 67 locations: []string{"buz", "bar"}, 68 value: 1, 69 }, 70 { 71 locations: []string{"buz", "bar"}, 72 value: 1, 73 }, 74 { 75 locations: []string{"buz"}, 76 value: 1, 77 }, 78 { 79 locations: []string{"foo", "buz", "bar"}, 80 value: 1, 81 }, 82 { 83 locations: []string{"blop", "buz", "bar"}, 84 value: 2, 85 }, 86 { 87 locations: []string{"blip", "bar"}, 88 value: 4, 89 }, 90 }, 91 func() *Tree { 92 tr := emptyTree() 93 94 bar := tr.add("bar", 0, 9) 95 bar.add("blip", 4, 4) 96 97 buz := bar.add("buz", 2, 5) 98 buz.add("blop", 2, 2) 99 buz.add("foo", 1, 1) 100 101 tr.add("buz", 1, 1) 102 return tr 103 }, 104 }, 105 } { 106 t.Run(tc.name, func(t *testing.T) { 107 expected := tc.expected().String() 108 tr := newTree(tc.stacks).String() 109 require.Equal(t, tr, expected, "tree should be equal got:%s\n expected:%s\n", tr, expected) 110 }) 111 } 112 } 113 114 func Test_TreeMerge(t *testing.T) { 115 type testCase struct { 116 description string 117 src, dst, expected *Tree 118 } 119 120 testCases := func() []testCase { 121 return []testCase{ 122 { 123 description: "empty src", 124 dst: newTree([]stacktraces{ 125 {locations: []string{"c", "b", "a"}, value: 1}, 126 }), 127 src: new(Tree), 128 expected: newTree([]stacktraces{ 129 {locations: []string{"c", "b", "a"}, value: 1}, 130 }), 131 }, 132 { 133 description: "empty dst", 134 dst: new(Tree), 135 src: newTree([]stacktraces{ 136 {locations: []string{"c", "b", "a"}, value: 1}, 137 }), 138 expected: newTree([]stacktraces{ 139 {locations: []string{"c", "b", "a"}, value: 1}, 140 }), 141 }, 142 { 143 description: "empty both", 144 dst: new(Tree), 145 src: new(Tree), 146 expected: new(Tree), 147 }, 148 { 149 description: "missing nodes in dst", 150 dst: newTree([]stacktraces{ 151 {locations: []string{"c", "b", "a"}, value: 1}, 152 }), 153 src: newTree([]stacktraces{ 154 {locations: []string{"c", "b", "a"}, value: 1}, 155 {locations: []string{"c", "b", "a1"}, value: 1}, 156 {locations: []string{"c", "b1", "a"}, value: 1}, 157 {locations: []string{"c1", "b", "a"}, value: 1}, 158 }), 159 expected: newTree([]stacktraces{ 160 {locations: []string{"c", "b", "a"}, value: 2}, 161 {locations: []string{"c", "b", "a1"}, value: 1}, 162 {locations: []string{"c", "b1", "a"}, value: 1}, 163 {locations: []string{"c1", "b", "a"}, value: 1}, 164 }), 165 }, 166 { 167 description: "missing nodes in src", 168 dst: newTree([]stacktraces{ 169 {locations: []string{"c", "b", "a"}, value: 1}, 170 {locations: []string{"c", "b", "a1"}, value: 1}, 171 {locations: []string{"c", "b1", "a"}, value: 1}, 172 {locations: []string{"c1", "b", "a"}, value: 1}, 173 }), 174 src: newTree([]stacktraces{ 175 {locations: []string{"c", "b", "a"}, value: 1}, 176 }), 177 expected: newTree([]stacktraces{ 178 {locations: []string{"c", "b", "a"}, value: 2}, 179 {locations: []string{"c", "b", "a1"}, value: 1}, 180 {locations: []string{"c", "b1", "a"}, value: 1}, 181 {locations: []string{"c1", "b", "a"}, value: 1}, 182 }), 183 }, 184 } 185 } 186 187 t.Run("Tree.Merge", func(t *testing.T) { 188 for _, tc := range testCases() { 189 tc := tc 190 t.Run(tc.description, func(t *testing.T) { 191 tc.dst.Merge(tc.src) 192 require.Equal(t, tc.expected.String(), tc.dst.String()) 193 }) 194 } 195 }) 196 } 197 198 func Test_Tree_minValue(t *testing.T) { 199 x := newTree([]stacktraces{ 200 {locations: []string{"c", "b", "a"}, value: 1}, 201 {locations: []string{"c", "b", "a"}, value: 1}, 202 {locations: []string{"c1", "b", "a"}, value: 1}, 203 {locations: []string{"c", "b1", "a"}, value: 1}, 204 }) 205 206 type testCase struct { 207 desc string 208 maxNodes int64 209 expected int64 210 } 211 212 testCases := []*testCase{ 213 {desc: "tree greater than max nodes", maxNodes: 2, expected: 3}, 214 {desc: "tree less than max nodes", maxNodes: math.MaxInt64, expected: 0}, 215 {desc: "zero max nodes", maxNodes: 0, expected: 0}, 216 {desc: "negative max nodes", maxNodes: -1, expected: 0}, 217 } 218 for _, tc := range testCases { 219 t.Run(tc.desc, func(t *testing.T) { 220 assert.Equal(t, tc.expected, x.minValue(tc.maxNodes)) 221 }) 222 } 223 } 224 225 func Test_Tree_MarshalUnmarshal(t *testing.T) { 226 t.Run("empty tree", func(t *testing.T) { 227 expected := new(Tree) 228 var buf bytes.Buffer 229 require.NoError(t, expected.MarshalTruncate(&buf, -1)) 230 actual, err := UnmarshalTree(buf.Bytes()) 231 require.NoError(t, err) 232 require.Equal(t, expected.String(), actual.String()) 233 }) 234 235 t.Run("non-empty tree", func(t *testing.T) { 236 expected := newTree([]stacktraces{ 237 {locations: []string{"c", "b", "a"}, value: 1}, 238 {locations: []string{"c", "b", "a"}, value: 1}, 239 {locations: []string{"c1", "b", "a"}, value: 1}, 240 {locations: []string{"c", "b1", "a"}, value: 1}, 241 {locations: []string{"c1", "b1", "a"}, value: 1}, 242 {locations: []string{"c", "b", "a1"}, value: 1}, 243 {locations: []string{"c1", "b", "a1"}, value: 1}, 244 {locations: []string{"c", "b1", "a1"}, value: 1}, 245 {locations: []string{"c1", "b1", "a1"}, value: 1}, 246 }) 247 248 var buf bytes.Buffer 249 require.NoError(t, expected.MarshalTruncate(&buf, -1)) 250 actual, err := UnmarshalTree(buf.Bytes()) 251 require.NoError(t, err) 252 require.Equal(t, expected.String(), actual.String()) 253 }) 254 255 t.Run("truncation", func(t *testing.T) { 256 fullTree := newTree([]stacktraces{ 257 {locations: []string{"c", "b", "a"}, value: 1}, 258 {locations: []string{"c", "b", "a"}, value: 1}, 259 {locations: []string{"c1", "b", "a"}, value: 1}, 260 {locations: []string{"c", "b1", "a"}, value: 1}, 261 {locations: []string{"c1", "b1", "a"}, value: 1}, 262 {locations: []string{"c", "b", "a1"}, value: 1}, 263 {locations: []string{"c1", "b", "a1"}, value: 1}, 264 {locations: []string{"c", "b1", "a1"}, value: 1}, 265 {locations: []string{"c1", "b1", "a1"}, value: 1}, 266 }) 267 268 var buf bytes.Buffer 269 require.NoError(t, fullTree.MarshalTruncate(&buf, 3)) 270 271 actual, err := UnmarshalTree(buf.Bytes()) 272 require.NoError(t, err) 273 274 expected := newTree([]stacktraces{ 275 {locations: []string{"other", "b", "a"}, value: 3}, 276 {locations: []string{"other", "a"}, value: 2}, 277 {locations: []string{"other", "a1"}, value: 4}, 278 }) 279 280 require.Equal(t, expected.String(), actual.String()) 281 }) 282 } 283 284 func Test_FormatNames(t *testing.T) { 285 x := newTree([]stacktraces{ 286 {locations: []string{"c0", "b0", "a0"}, value: 3}, 287 {locations: []string{"c1", "b0", "a0"}, value: 3}, 288 {locations: []string{"d0", "b1", "a0"}, value: 2}, 289 {locations: []string{"e1", "c1", "a1"}, value: 4}, 290 {locations: []string{"e2", "c1", "a2"}, value: 4}, 291 }) 292 x.FormatNodeNames(func(n string) string { 293 if len(n) > 0 { 294 n = n[:1] 295 } 296 return n 297 }) 298 expected := newTree([]stacktraces{ 299 {locations: []string{"c", "b", "a"}, value: 3}, 300 {locations: []string{"c", "b", "a"}, value: 3}, 301 {locations: []string{"d", "b", "a"}, value: 2}, 302 {locations: []string{"e", "c", "a"}, value: 4}, 303 {locations: []string{"e", "c", "a"}, value: 4}, 304 }) 305 require.Equal(t, expected.String(), x.String()) 306 } 307 308 func emptyTree() *Tree { 309 return &Tree{} 310 } 311 312 func newTree(stacks []stacktraces) *Tree { 313 t := emptyTree() 314 for _, stack := range stacks { 315 if stack.value == 0 { 316 continue 317 } 318 if t == nil { 319 t = stackToTree(stack) 320 continue 321 } 322 t.Merge(stackToTree(stack)) 323 } 324 return t 325 } 326 327 type stacktraces struct { 328 locations []string 329 value int64 330 } 331 332 func (t *Tree) add(name string, self, total int64) *node { 333 new := &node{ 334 name: name, 335 self: self, 336 total: total, 337 } 338 t.root = append(t.root, new) 339 return new 340 } 341 342 func (n *node) add(name string, self, total int64) *node { 343 new := &node{ 344 parent: n, 345 name: name, 346 self: self, 347 total: total, 348 } 349 n.children = append(n.children, new) 350 return new 351 } 352 353 func stackToTree(stack stacktraces) *Tree { 354 t := emptyTree() 355 if len(stack.locations) == 0 { 356 return t 357 } 358 current := &node{ 359 self: stack.value, 360 total: stack.value, 361 name: stack.locations[0], 362 } 363 if len(stack.locations) == 1 { 364 t.root = append(t.root, current) 365 return t 366 } 367 remaining := stack.locations[1:] 368 for len(remaining) > 0 { 369 370 location := remaining[0] 371 name := location 372 remaining = remaining[1:] 373 374 // This pack node with the same name as the next location 375 // Disable for now but we might want to introduce it if we find it useful. 376 // for len(remaining) != 0 { 377 // if remaining[0].function == name { 378 // remaining = remaining[1:] 379 // continue 380 // } 381 // break 382 // } 383 384 parent := &node{ 385 children: []*node{current}, 386 total: current.total, 387 name: name, 388 } 389 current.parent = parent 390 current = parent 391 } 392 t.root = []*node{current} 393 return t 394 } 395 396 func Test_TreeFromBackendProfileSampleType(t *testing.T) { 397 profile := &profilev1.Profile{ 398 SampleType: []*profilev1.ValueType{ 399 {Type: 1, Unit: 2}, 400 {Type: 3, Unit: 4}, 401 }, 402 StringTable: []string{ 403 "", 404 "samples", 405 "count", 406 "cpu", 407 "nanoseconds", 408 "main", 409 "foo", 410 "bar", 411 }, 412 Sample: []*profilev1.Sample{ 413 { 414 LocationId: []uint64{1, 2}, 415 Value: []int64{10, 20}, 416 }, 417 { 418 LocationId: []uint64{2, 3}, 419 Value: []int64{30, 60}, 420 }, 421 }, 422 Location: []*profilev1.Location{ 423 {Id: 1, Line: []*profilev1.Line{{FunctionId: 1}}}, 424 {Id: 2, Line: []*profilev1.Line{{FunctionId: 2}}}, 425 {Id: 3, Line: []*profilev1.Line{{FunctionId: 3}}}, 426 }, 427 Function: []*profilev1.Function{ 428 {Id: 1, Name: 5}, 429 {Id: 2, Name: 6}, 430 {Id: 3, Name: 7}, 431 }, 432 } 433 434 t.Run("using first sample type (index 0)", func(t *testing.T) { 435 treeBytes, err := TreeFromBackendProfileSampleType(profile, -1, 0) 436 require.NoError(t, err) 437 tree := MustUnmarshalTree(treeBytes) 438 assert.Equal(t, int64(40), tree.Total()) 439 }) 440 441 t.Run("using second sample type (index 1)", func(t *testing.T) { 442 treeBytes, err := TreeFromBackendProfileSampleType(profile, -1, 1) 443 require.NoError(t, err) 444 tree := MustUnmarshalTree(treeBytes) 445 assert.Equal(t, int64(80), tree.Total()) 446 }) 447 448 t.Run("validates sample type index bounds", func(t *testing.T) { 449 _, err := TreeFromBackendProfileSampleType(profile, -1, 99) 450 require.Error(t, err) 451 }) 452 453 t.Run("handles profile with no samples", func(t *testing.T) { 454 emptyProfile := &profilev1.Profile{ 455 SampleType: []*profilev1.ValueType{ 456 {Type: 1, Unit: 2}, 457 }, 458 StringTable: []string{"", "samples", "count"}, 459 Sample: []*profilev1.Sample{}, 460 } 461 treeBytes, err := TreeFromBackendProfileSampleType(emptyProfile, -1, 0) 462 require.NoError(t, err) 463 tree := MustUnmarshalTree(treeBytes) 464 assert.Equal(t, int64(0), tree.Total()) 465 }) 466 467 t.Run("handles profile with no sample types", func(t *testing.T) { 468 noSampleTypesProfile := &profilev1.Profile{ 469 SampleType: []*profilev1.ValueType{}, 470 StringTable: []string{""}, 471 Sample: []*profilev1.Sample{ 472 { 473 LocationId: []uint64{1}, 474 Value: []int64{10}, 475 }, 476 }, 477 } 478 _, err := TreeFromBackendProfileSampleType(noSampleTypesProfile, -1, 0) 479 require.Error(t, err) 480 }) 481 482 t.Run("handles locations without line information (using addresses)", func(t *testing.T) { 483 addressProfile := &profilev1.Profile{ 484 StringTable: []string{ 485 "", 486 "samples", 487 "count", 488 }, 489 SampleType: []*profilev1.ValueType{ 490 {Type: 1, Unit: 2}, 491 }, 492 Location: []*profilev1.Location{ 493 {Id: 1, Address: 0x1000, Line: []*profilev1.Line{}}, 494 {Id: 2, Address: 0x2000, Line: []*profilev1.Line{}}, 495 }, 496 Sample: []*profilev1.Sample{ 497 { 498 LocationId: []uint64{1, 2}, // 0x1000 -> 0x2000 499 Value: []int64{100}, // 100 samples 500 }, 501 }, 502 } 503 originalLen := len(addressProfile.StringTable) 504 505 treeBytes, err := TreeFromBackendProfileSampleType(addressProfile, -1, 0) 506 require.NoError(t, err) 507 508 tree := MustUnmarshalTree(treeBytes) 509 assert.Equal(t, int64(100), tree.Total()) 510 511 assert.Greater(t, len(addressProfile.StringTable), originalLen) 512 513 // Check for specific address strings in the string table 514 foundAddresses := 0 515 for _, s := range addressProfile.StringTable { 516 if s == "1000" || s == "2000" { 517 foundAddresses++ 518 } 519 } 520 assert.Equal(t, 2, foundAddresses) 521 }) 522 }