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  }