Phlare: Rollback pprof code (#65689)

This commit is contained in:
Andrej Ocenas 2023-03-31 15:27:14 +02:00 committed by GitHub
parent d7c9dc4730
commit 977a7e9a55
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 202 additions and 567 deletions

View File

@ -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)
}
type ProfileTree struct {
Level int
Value int64
Self int64
Function *Function
Inlined []*Function
locationID uint64
// START_OFFSET is offset of the bar relative to previous sibling
const START_OFFSET = 0
Nodes []*ProfileTree
Parent *ProfileTree
// 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 {
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: "<unknown>",
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
}

View File

@ -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: "<unknown>",
},
Level: 1,
locationID: 2,
Nodes: []*ProfileTree{
{
Value: 10,
Function: &Function{
FunctionName: "<unknown>",
},
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]{