From 977a7e9a5564321322dddcc2d092df7bc48c64ea Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Fri, 31 Mar 2023 15:27:14 +0200 Subject: [PATCH] Phlare: Rollback pprof code (#65689) --- pkg/tsdb/phlare/query.go | 316 +++++++++--------------- pkg/tsdb/phlare/query_test.go | 453 +++++++--------------------------- 2 files changed, 202 insertions(+), 567 deletions(-) diff --git a/pkg/tsdb/phlare/query.go b/pkg/tsdb/phlare/query.go index b660a292309..9e55e037f5c 100644 --- a/pkg/tsdb/phlare/query.go +++ b/pkg/tsdb/phlare/query.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "math" - "sort" "strings" "time" @@ -15,7 +14,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/live" "github.com/grafana/grafana/pkg/tsdb/phlare/kinds/dataquery" - googlev1 "github.com/grafana/phlare/api/gen/proto/go/google/v1" querierv1 "github.com/grafana/phlare/api/gen/proto/go/querier/v1" "github.com/xlab/treeprint" ) @@ -84,10 +82,10 @@ func (d *PhlareDatasource) query(ctx context.Context, pCtx backend.PluginContext if query.QueryType == queryTypeProfile || query.QueryType == queryTypeBoth { req := makeRequest(qm, query) - logger.Debug("Sending SelectMergeProfile", "request", req, "queryModel", qm) - resp, err := d.client.SelectMergeProfile(ctx, req) + logger.Debug("Sending SelectMergeStacktracesRequest", "request", req, "queryModel", qm) + resp, err := d.client.SelectMergeStacktraces(ctx, req) if err != nil { - logger.Error("Querying SelectMergeProfile()", "err", err) + logger.Error("Querying SelectMergeStacktraces()", "err", err) response.Error = err return response } @@ -110,9 +108,9 @@ func (d *PhlareDatasource) query(ctx context.Context, pCtx backend.PluginContext return response } -func makeRequest(qm queryModel, query backend.DataQuery) *connect.Request[querierv1.SelectMergeProfileRequest] { - return &connect.Request[querierv1.SelectMergeProfileRequest]{ - Msg: &querierv1.SelectMergeProfileRequest{ +func makeRequest(qm queryModel, query backend.DataQuery) *connect.Request[querierv1.SelectMergeStacktracesRequest] { + return &connect.Request[querierv1.SelectMergeStacktracesRequest]{ + Msg: &querierv1.SelectMergeStacktracesRequest{ ProfileTypeID: qm.ProfileTypeId, LabelSelector: qm.LabelSelector, Start: query.TimeRange.From.UnixMilli(), @@ -124,21 +122,114 @@ func makeRequest(qm queryModel, query backend.DataQuery) *connect.Request[querie // responseToDataFrames turns Phlare response to data.Frame. We encode the data into a nested set format where we have // [level, value, label] columns and by ordering the items in a depth first traversal order we can recreate the whole // tree back. -func responseToDataFrames(prof *googlev1.Profile, profileTypeID string) *data.Frame { - tree := profileAsTree(prof) +func responseToDataFrames(resp *querierv1.SelectMergeStacktracesResponse, profileTypeID string) *data.Frame { + tree := levelsToTree(resp.Flamegraph.Levels, resp.Flamegraph.Names) return treeToNestedSetDataFrame(tree, profileTypeID) } +// START_OFFSET is offset of the bar relative to previous sibling +const START_OFFSET = 0 + +// VALUE_OFFSET is value or width of the bar +const VALUE_OFFSET = 1 + +// SELF_OFFSET is self value of the bar +const SELF_OFFSET = 2 + +// NAME_OFFSET is index into the names array +const NAME_OFFSET = 3 + +// ITEM_OFFSET Next bar. Each bar of the profile is represented by 4 number in a flat array. +const ITEM_OFFSET = 4 + type ProfileTree struct { - Level int - Value int64 - Self int64 - Function *Function - Inlined []*Function - locationID uint64 - - Nodes []*ProfileTree - Parent *ProfileTree + Start int64 + Value int64 + Self int64 + Level int + Name string + Nodes []*ProfileTree +} + +// levelsToTree converts flamebearer format into a tree. This is needed to then convert it into nested set format +// dataframe. This should be temporary, and ideally we should get some sort of tree struct directly from Phlare API. +func levelsToTree(levels []*querierv1.Level, names []string) *ProfileTree { + tree := &ProfileTree{ + Start: 0, + Value: levels[0].Values[VALUE_OFFSET], + Self: levels[0].Values[SELF_OFFSET], + Level: 0, + Name: names[levels[0].Values[0]], + } + + parentsStack := []*ProfileTree{tree} + currentLevel := 1 + + // Cycle through each level + for { + if currentLevel >= len(levels) { + break + } + + // If we still have levels to go, this should not happen. Something is probably wrong with the flamebearer data. + if len(parentsStack) == 0 { + logger.Error("parentsStack is empty but we are not at the the last level", "currentLevel", currentLevel) + break + } + + var nextParentsStack []*ProfileTree + currentParent := parentsStack[:1][0] + parentsStack = parentsStack[1:] + itemIndex := 0 + // cumulative offset as items in flamebearer format have just relative to prev item + offset := int64(0) + + // Cycle through bar in a level + for { + if itemIndex >= len(levels[currentLevel].Values) { + break + } + + itemStart := levels[currentLevel].Values[itemIndex+START_OFFSET] + offset + itemValue := levels[currentLevel].Values[itemIndex+VALUE_OFFSET] + selfValue := levels[currentLevel].Values[itemIndex+SELF_OFFSET] + itemEnd := itemStart + itemValue + parentEnd := currentParent.Start + currentParent.Value + + if itemStart >= currentParent.Start && itemEnd <= parentEnd { + // We have an item that is in the bounds of current parent item, so it should be its child + treeItem := &ProfileTree{ + Start: itemStart, + Value: itemValue, + Self: selfValue, + Level: currentLevel, + Name: names[levels[currentLevel].Values[itemIndex+NAME_OFFSET]], + } + // Add to parent + currentParent.Nodes = append(currentParent.Nodes, treeItem) + // Add this item as parent for the next level + nextParentsStack = append(nextParentsStack, treeItem) + itemIndex += ITEM_OFFSET + + // Update offset for next item. This is changing relative offset to absolute one. + offset = itemEnd + } else { + // We went out of parents bounds so lets move to next parent. We will evaluate the same item again, but + // we will check if it is a child of the next parent item in line. + if len(parentsStack) == 0 { + logger.Error("parentsStack is empty but there are still items in current level", "currentLevel", currentLevel, "itemIndex", itemIndex) + break + } + currentParent = parentsStack[:1][0] + parentsStack = parentsStack[1:] + continue + } + } + parentsStack = nextParentsStack + currentLevel++ + } + + return tree } type Function struct { @@ -158,7 +249,7 @@ func (pt *ProfileTree) String() string { } tree := treeprint.New() for _, n := range []*ProfileTree{pt} { - b := tree.AddBranch(fmt.Sprintf("%s: level %d self %d total %d", n.Function, n.Level, n.Self, n.Value)) + b := tree.AddBranch(fmt.Sprintf("%s: level %d self %d total %d", n.Name, n.Level, n.Self, n.Value)) remaining := append([]*branch{}, &branch{nodes: n.Nodes, Tree: b}) for len(remaining) > 0 { current := remaining[0] @@ -167,11 +258,11 @@ func (pt *ProfileTree) String() string { if len(n.Nodes) > 0 { remaining = append(remaining, &branch{ - nodes: n.Nodes, Tree: current.Tree.AddBranch(fmt.Sprintf("%s: level %d self %d total %d", n.Function, n.Level, n.Self, n.Value)), + nodes: n.Nodes, Tree: current.Tree.AddBranch(fmt.Sprintf("%s: level %d self %d total %d", n.Name, n.Level, n.Self, n.Value)), }, ) } else { - current.Tree.AddNode(fmt.Sprintf("%s: level %d self %d total %d", n.Function, n.Level, n.Self, n.Value)) + current.Tree.AddNode(fmt.Sprintf("%s: level %d self %d total %d", n.Name, n.Level, n.Self, n.Value)) } } } @@ -179,175 +270,6 @@ func (pt *ProfileTree) String() string { return tree.String() } -// addSample adds a sample to the tree. As sample is just a single stack we just have to traverse the tree until it -// starts to differ from the sample and add a new branch if needed. For example if we have a tree: -// -// root --> func1 -> func2 -> func3 -// \-> func4 -// -// And we add a sample: -// -// func1 -> func2 -> func5 -// -// We will get: -// -// root --> func1 --> func2 --> func3 -// \ \-> func5 -// \-> func4 -// -// While we add the current sample value to root -> func1 -> func2. -func (pt *ProfileTree) addSample(profile *googlev1.Profile, sample *googlev1.Sample) { - if len(sample.LocationId) == 0 { - return - } - - locations := getReversedLocations(profile, sample) - - // Extend root - pt.Value = pt.Value + sample.Value[0] - current := pt - - for index, location := range locations { - if len(current.Nodes) > 0 { - var foundNode *ProfileTree - for _, node := range current.Nodes { - if node.locationID == location.Id { - foundNode = node - } - } - - if foundNode != nil { - // We found node with the same locationID so just add the value it - foundNode.Value = foundNode.Value + sample.Value[0] - current = foundNode - // Continue to next locationID in the sample - continue - } - } - // Either current has no children we can compare to or we have location that does not exist yet in the tree. - - // Create sample with only the locations we did not already attributed to the tree. - subSample := &googlev1.Sample{ - LocationId: sample.LocationId[:len(sample.LocationId)-index], - Value: sample.Value, - Label: sample.Label, - } - newTree := treeFromSample(profile, subSample, index) - // Append the new subtree in the correct place in the tree - current.Nodes = append(current.Nodes, newTree.Nodes[0]) - sort.SliceStable(current.Nodes, func(i, j int) bool { - return current.Nodes[i].Function.String() < current.Nodes[j].Function.String() - }) - newTree.Nodes[0].Parent = current - break - } - - // Adjust self of the current node as we may need to add value to its self if we just extended it and did not - // add children - var childrenVal int64 = 0 - for _, node := range current.Nodes { - childrenVal += node.Value - } - current.Self = current.Value - childrenVal -} - -// treeFromSample creates a linked tree form a single pprof sample. As a single sample is just a single stack the tree -// will also be just a simple linked list at this point. -func treeFromSample(profile *googlev1.Profile, sample *googlev1.Sample, startLevel int) *ProfileTree { - root := &ProfileTree{ - Value: sample.Value[0], - Level: startLevel, - locationID: 0, - Function: &Function{ - FunctionName: "root", - }, - } - - if len(sample.LocationId) == 0 { - // Empty profile - return root - } - - locations := getReversedLocations(profile, sample) - parent := root - - // Loop over locations and add a node to the tree for each location - for index, location := range locations { - node := &ProfileTree{ - Self: 0, - Value: sample.Value[0], - Level: index + startLevel + 1, - locationID: location.Id, - Parent: parent, - } - - parent.Nodes = []*ProfileTree{node} - parent = node - - functions := getFunctions(profile, location) - // Last in the list is the main function - node.Function = functions[len(functions)-1] - // If there are more, other are inlined functions - if len(functions) > 1 { - node.Inlined = functions[:len(functions)-1] - } - } - // Last parent is a leaf and as it does not have any children it's value is also self - parent.Self = sample.Value[0] - return root -} - -func profileAsTree(profile *googlev1.Profile) *ProfileTree { - if profile == nil { - return nil - } - if len(profile.Sample) == 0 { - return nil - } - n := treeFromSample(profile, profile.Sample[0], 0) - for _, sample := range profile.Sample[1:] { - n.addSample(profile, sample) - } - return n -} - -// getReversedLocations returns all locations from a sample. Location is a one level in the stack trace so single row in -// flamegraph. Returned locations are reversed (so root is 0, leaf is len - 1) which makes it easier to the use with -// tree structure starting from root. -func getReversedLocations(profile *googlev1.Profile, sample *googlev1.Sample) []*googlev1.Location { - locations := make([]*googlev1.Location, len(sample.LocationId)) - for index, locationId := range sample.LocationId { - // profile.Location[locationId-1] is because locationId (and other IDs) is 1 based, so - // locationId == array index + 1 - locations[len(sample.LocationId)-1-index] = profile.Location[locationId-1] - } - return locations -} - -// getFunctions returns all functions for a location. First one is the main function and the rest are inlined functions. -// If there is no info it just returns single placeholder function. -func getFunctions(profile *googlev1.Profile, location *googlev1.Location) []*Function { - if len(location.Line) == 0 { - return []*Function{{ - FunctionName: "", - FileName: "", - Line: 0, - }} - } - functions := make([]*Function, len(location.Line)) - - for index, line := range location.Line { - function := profile.Function[line.FunctionId-1] - - functions[index] = &Function{ - FunctionName: profile.StringTable[function.Name], - FileName: profile.StringTable[function.Filename], - Line: line.Line, - } - } - return functions -} - type CustomMeta struct { ProfileTypeID string } @@ -368,11 +290,9 @@ func treeToNestedSetDataFrame(tree *ProfileTree, profileTypeID string) *data.Fra parts := strings.Split(profileTypeID, ":") valueField.Config = &data.FieldConfig{Unit: normalizeUnit(parts[2])} selfField.Config = &data.FieldConfig{Unit: normalizeUnit(parts[2])} - lineNumberField := data.NewField("line", nil, []int64{}) - frame.Fields = data.Fields{levelField, valueField, selfField, lineNumberField} + frame.Fields = data.Fields{levelField, valueField, selfField} labelField := NewEnumField("label", nil) - fileNameField := NewEnumField("fileName", nil) // Tree can be nil if profile was empty, we can still send empty frame in that case if tree != nil { @@ -380,15 +300,11 @@ func treeToNestedSetDataFrame(tree *ProfileTree, profileTypeID string) *data.Fra levelField.Append(int64(tree.Level)) valueField.Append(tree.Value) selfField.Append(tree.Self) - // todo: inline functions - // tree.Inlined - lineNumberField.Append(tree.Function.Line) - labelField.Append(tree.Function.FunctionName) - fileNameField.Append(tree.Function.FileName) + labelField.Append(tree.Name) }) } - frame.Fields = append(frame.Fields, labelField.GetField(), fileNameField.GetField()) + frame.Fields = append(frame.Fields, labelField.GetField()) return frame } diff --git a/pkg/tsdb/phlare/query_test.go b/pkg/tsdb/phlare/query_test.go index cad5026d05d..35183fb6286 100644 --- a/pkg/tsdb/phlare/query_test.go +++ b/pkg/tsdb/phlare/query_test.go @@ -2,8 +2,6 @@ package phlare import ( "context" - "encoding/json" - "os" "testing" "time" @@ -108,15 +106,96 @@ func makeDataQuery() *backend.DataQuery { } } +func fieldValues[T any](field *data.Field) []T { + values := make([]T, field.Len()) + for i := 0; i < field.Len(); i++ { + values[i] = field.At(i).(T) + } + return values +} + +// This is where the tests for the datasource backend live. +func Test_profileToDataFrame(t *testing.T) { + resp := &connect.Response[querierv1.SelectMergeStacktracesResponse]{ + Msg: &querierv1.SelectMergeStacktracesResponse{ + Flamegraph: &querierv1.FlameGraph{ + Names: []string{"func1", "func2", "func3"}, + Levels: []*querierv1.Level{ + {Values: []int64{0, 20, 1, 2}}, + {Values: []int64{0, 10, 3, 1, 4, 5, 5, 2}}, + }, + Total: 987, + MaxSelf: 123, + }, + }, + } + frame := responseToDataFrames(resp.Msg, "memory:alloc_objects:count:space:bytes") + require.Equal(t, 4, len(frame.Fields)) + require.Equal(t, data.NewField("level", nil, []int64{0, 1, 1}), frame.Fields[0]) + require.Equal(t, data.NewField("value", nil, []int64{20, 10, 5}).SetConfig(&data.FieldConfig{Unit: "short"}), frame.Fields[1]) + require.Equal(t, data.NewField("self", nil, []int64{1, 3, 5}).SetConfig(&data.FieldConfig{Unit: "short"}), frame.Fields[2]) + require.Equal(t, "label", frame.Fields[3].Name) + require.Equal(t, []int64{0, 1, 2}, fieldValues[int64](frame.Fields[3])) + require.Equal(t, []string{"func1", "func2", "func3"}, frame.Fields[3].Config.TypeConfig.Enum.Text) +} + +// This is where the tests for the datasource backend live. +func Test_levelsToTree(t *testing.T) { + t.Run("simple", func(t *testing.T) { + levels := []*querierv1.Level{ + {Values: []int64{0, 100, 0, 0}}, + {Values: []int64{0, 40, 0, 1, 0, 30, 0, 2}}, + {Values: []int64{0, 15, 0, 3}}, + } + + tree := levelsToTree(levels, []string{"root", "func1", "func2", "func1:func3"}) + require.Equal(t, &ProfileTree{ + Start: 0, Value: 100, Level: 0, Name: "root", Nodes: []*ProfileTree{ + { + Start: 0, Value: 40, Level: 1, Name: "func1", Nodes: []*ProfileTree{ + {Start: 0, Value: 15, Level: 2, Name: "func1:func3"}, + }, + }, + {Start: 40, Value: 30, Level: 1, Name: "func2"}, + }, + }, tree) + }) + + t.Run("medium", func(t *testing.T) { + levels := []*querierv1.Level{ + {Values: []int64{0, 100, 0, 0}}, + {Values: []int64{0, 40, 0, 1, 0, 30, 0, 2, 0, 30, 0, 3}}, + {Values: []int64{0, 20, 0, 4, 50, 10, 0, 5}}, + } + + tree := levelsToTree(levels, []string{"root", "func1", "func2", "func3", "func1:func4", "func3:func5"}) + require.Equal(t, &ProfileTree{ + Start: 0, Value: 100, Level: 0, Name: "root", Nodes: []*ProfileTree{ + { + Start: 0, Value: 40, Level: 1, Name: "func1", Nodes: []*ProfileTree{ + {Start: 0, Value: 20, Level: 2, Name: "func1:func4"}, + }, + }, + {Start: 40, Value: 30, Level: 1, Name: "func2"}, + { + Start: 70, Value: 30, Level: 1, Name: "func3", Nodes: []*ProfileTree{ + {Start: 70, Value: 10, Level: 2, Name: "func3:func5"}, + }, + }, + }, + }, tree) + }) +} + func Test_treeToNestedDataFrame(t *testing.T) { t.Run("sample profile tree", func(t *testing.T) { tree := &ProfileTree{ - Value: 100, Level: 0, Self: 1, Function: &Function{FunctionName: "root"}, Nodes: []*ProfileTree{ + Value: 100, Level: 0, Self: 1, Name: "root", Nodes: []*ProfileTree{ { - Value: 40, Level: 1, Self: 2, Function: &Function{FunctionName: "func1", FileName: "1", Line: 1}, + Value: 40, Level: 1, Self: 2, Name: "func1", }, - {Value: 30, Level: 1, Self: 3, Function: &Function{FunctionName: "func2", FileName: "2", Line: 2}, Nodes: []*ProfileTree{ - {Value: 15, Level: 2, Self: 4, Function: &Function{FunctionName: "func1:func3", FileName: "3", Line: 3}}, + {Value: 30, Level: 1, Self: 3, Name: "func2", Nodes: []*ProfileTree{ + {Value: 15, Level: 2, Self: 4, Name: "func1:func3"}, }}, }, } @@ -130,382 +209,22 @@ func Test_treeToNestedDataFrame(t *testing.T) { }, }, } - filenameConfig := &data.FieldConfig{ - TypeConfig: &data.FieldTypeConfig{ - Enum: &data.EnumFieldConfig{ - Text: []string{"", "1", "2", "3"}, - }, - }, - } require.Equal(t, []*data.Field{ data.NewField("level", nil, []int64{0, 1, 1, 2}), data.NewField("value", nil, []int64{100, 40, 30, 15}).SetConfig(&data.FieldConfig{Unit: "short"}), data.NewField("self", nil, []int64{1, 2, 3, 4}).SetConfig(&data.FieldConfig{Unit: "short"}), - data.NewField("line", nil, []int64{0, 1, 2, 3}), data.NewField("label", nil, []int64{0, 1, 2, 3}).SetConfig(labelConfig), - data.NewField("fileName", nil, []int64{0, 1, 2, 3}).SetConfig(filenameConfig), }, frame.Fields) }) t.Run("nil profile tree", func(t *testing.T) { frame := treeToNestedSetDataFrame(nil, "memory:alloc_objects:count:space:bytes") - require.Equal(t, 6, len(frame.Fields)) + require.Equal(t, 4, len(frame.Fields)) require.Equal(t, 0, frame.Fields[0].Len()) }) } -var fooProfile = &googlev1.Profile{ - Location: []*googlev1.Location{ - {Id: 1, Line: []*googlev1.Line{{Line: 5, FunctionId: 4}, {Line: 1, FunctionId: 1}}}, - {Id: 2, Line: []*googlev1.Line{{Line: 2, FunctionId: 2}}}, - {Id: 3, Line: []*googlev1.Line{{Line: 3, FunctionId: 3}}}, - }, - Function: []*googlev1.Function{ - {Id: 1, Name: 1, Filename: 4}, - {Id: 2, Name: 2, Filename: 4}, - {Id: 3, Name: 3, Filename: 5}, - {Id: 4, Name: 6, Filename: 5}, - }, - StringTable: []string{"", "foo", "bar", "baz", "file1", "file2", "inline"}, -} - -func Test_treeFromSample(t *testing.T) { - for _, tc := range []struct { - name string - s *googlev1.Sample - p *googlev1.Profile - want *ProfileTree - }{ - { - name: "empty lines", - s: &googlev1.Sample{LocationId: []uint64{1, 2}, Value: []int64{10}}, - p: &googlev1.Profile{ - Location: []*googlev1.Location{ - {Id: 1, Line: []*googlev1.Line{}}, - {Id: 2, Line: []*googlev1.Line{}}, - }, - Function: []*googlev1.Function{}, - }, - want: &ProfileTree{ - Value: 10, - Function: &Function{ - FunctionName: "root", - }, - Nodes: []*ProfileTree{ - { - Value: 10, - Function: &Function{ - FunctionName: "", - }, - Level: 1, - locationID: 2, - Nodes: []*ProfileTree{ - { - Value: 10, - Function: &Function{ - FunctionName: "", - }, - Level: 2, - Self: 10, - locationID: 1, - }, - }, - }, - }, - }, - }, - { - name: "empty locations", - s: &googlev1.Sample{LocationId: []uint64{}, Value: []int64{10}}, - want: &ProfileTree{ - Value: 10, - Function: &Function{ - FunctionName: "root", - }, - }, - }, - { - name: "multiple locations and inlines", - s: &googlev1.Sample{LocationId: []uint64{3, 2, 1}, Value: []int64{10}}, - p: fooProfile, - want: &ProfileTree{ - Value: 10, - Function: &Function{ - FunctionName: "root", - }, - Nodes: []*ProfileTree{ - { - Value: 10, - locationID: 1, - Level: 1, - Function: &Function{ - FunctionName: "foo", - FileName: "file1", - Line: 1, - }, - Inlined: []*Function{ - { - FunctionName: "inline", - FileName: "file2", - Line: 5, - }, - }, - Nodes: []*ProfileTree{ - { - Value: 10, - locationID: 2, - Level: 2, - Function: &Function{ - FunctionName: "bar", - FileName: "file1", - Line: 2, - }, - Nodes: []*ProfileTree{ - { - Value: 10, - Self: 10, - locationID: 3, - Level: 3, - Function: &Function{ - FunctionName: "baz", - FileName: "file2", - Line: 3, - }, - }, - }, - }, - }, - }, - }, - }, - }, - } { - t.Run(tc.name, func(t *testing.T) { - setParents(tc.want) - actual := treeFromSample(tc.p, tc.s, 0) - require.Equal(t, tc.want, actual, "want\n%s\n got\n%s", tc.want, actual) - }) - } -} - -func Test_TreeString(t *testing.T) { - t.Log(treeFromSample(fooProfile, &googlev1.Sample{LocationId: []uint64{3, 2, 1}, Value: []int64{10}}, 0)) -} - -func Test_profileAsTree(t *testing.T) { - for _, tc := range []struct { - name string - want *ProfileTree - in *googlev1.Profile - }{ - {name: "empty"}, - {name: "no sample", in: &googlev1.Profile{}}, - { - name: "same locations", - in: &googlev1.Profile{ - Sample: []*googlev1.Sample{ - {LocationId: []uint64{3, 2, 1}, Value: []int64{10}}, - {LocationId: []uint64{3, 2, 1}, Value: []int64{30}}, - }, - Location: fooProfile.Location, - Function: fooProfile.Function, - StringTable: fooProfile.StringTable, - }, - want: &ProfileTree{ - Value: 40, - Function: &Function{ - FunctionName: "root", - }, - Nodes: []*ProfileTree{ - { - Value: 40, - locationID: 1, - Level: 1, - Function: &Function{ - FunctionName: "foo", - FileName: "file1", - Line: 1, - }, - Inlined: []*Function{ - { - FunctionName: "inline", - FileName: "file2", - Line: 5, - }, - }, - Nodes: []*ProfileTree{ - { - Value: 40, - locationID: 2, - Level: 2, - Function: &Function{ - FunctionName: "bar", - FileName: "file1", - Line: 2, - }, - Nodes: []*ProfileTree{ - { - Value: 40, - Self: 40, - locationID: 3, - Level: 3, - Function: &Function{ - FunctionName: "baz", - FileName: "file2", - Line: 3, - }, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "different locations", - in: &googlev1.Profile{ - Sample: []*googlev1.Sample{ - {LocationId: []uint64{3, 2, 1}, Value: []int64{15}}, // foo -> bar -> baz - {LocationId: []uint64{3, 2, 1}, Value: []int64{30}}, // foo -> bar -> baz - {LocationId: []uint64{1, 2, 1}, Value: []int64{20}}, // foo -> bar -> foo - {LocationId: []uint64{3, 2}, Value: []int64{20}}, // bar -> baz - {LocationId: []uint64{2, 1}, Value: []int64{40}}, // foo -> bar - {LocationId: []uint64{1}, Value: []int64{5}}, // foo - {LocationId: []uint64{}, Value: []int64{5}}, - }, - Location: fooProfile.Location, - Function: fooProfile.Function, - StringTable: fooProfile.StringTable, - }, - want: &ProfileTree{ - Value: 130, - Function: &Function{ - FunctionName: "root", - }, - Nodes: []*ProfileTree{ - { - locationID: 2, - Value: 20, - Self: 0, - Level: 1, - Function: &Function{ - FunctionName: "bar", - FileName: "file1", - Line: 2, - }, - Nodes: []*ProfileTree{ - { - locationID: 3, - Value: 20, - Self: 20, - Level: 2, - Function: &Function{ - FunctionName: "baz", - FileName: "file2", - Line: 3, - }, - }, - }, - }, - { - Value: 110, - Self: 5, - locationID: 1, - Level: 1, - Function: &Function{ - FunctionName: "foo", - FileName: "file1", - Line: 1, - }, - Inlined: []*Function{ - { - FunctionName: "inline", - FileName: "file2", - Line: 5, - }, - }, - Nodes: []*ProfileTree{ - { - Value: 105, - Self: 40, - locationID: 2, - Level: 2, - Function: &Function{ - FunctionName: "bar", - FileName: "file1", - Line: 2, - }, - Nodes: []*ProfileTree{ - { - Value: 20, - Self: 20, - locationID: 1, - Level: 3, - Function: &Function{ - FunctionName: "foo", - FileName: "file1", - Line: 1, - }, - Inlined: []*Function{ - { - FunctionName: "inline", - FileName: "file2", - Line: 5, - }, - }, - }, - { - Value: 45, - Self: 45, - locationID: 3, - Level: 3, - Function: &Function{ - FunctionName: "baz", - FileName: "file2", - Line: 3, - }, - }, - }, - }, - }, - }, - }, - }, - }, - } { - t.Run(tc.name, func(t *testing.T) { - if tc.want != nil { - setParents(tc.want) - } - actual := profileAsTree(tc.in) - require.Equal(t, tc.want, actual, "want\n%s\n got\n%s", tc.want, actual) - }) - } -} - -func Benchmark_profileAsTree(b *testing.B) { - profJson, err := os.ReadFile("./testdata/profile_response.json") - require.NoError(b, err) - var prof *googlev1.Profile - err = json.Unmarshal(profJson, &prof) - require.NoError(b, err) - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - profileAsTree(prof) - } -} - -func setParents(root *ProfileTree) { - for _, n := range root.Nodes { - n.Parent = root - setParents(n) - } -} - func Test_seriesToDataFrame(t *testing.T) { t.Run("single series", func(t *testing.T) { resp := &connect.Response[querierv1.SelectSeriesResponse]{