mirror of https://github.com/grafana/grafana.git
Phlare: Rollback pprof code (#65689)
This commit is contained in:
parent
d7c9dc4730
commit
977a7e9a55
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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]{
|
||||
|
|
|
|||
Loading…
Reference in New Issue