Phlare: Rollback pprof code (#65689)
This commit is contained in:
+116
-200
@@ -5,7 +5,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -15,7 +14,6 @@ import (
|
|||||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/live"
|
"github.com/grafana/grafana-plugin-sdk-go/live"
|
||||||
"github.com/grafana/grafana/pkg/tsdb/phlare/kinds/dataquery"
|
"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"
|
querierv1 "github.com/grafana/phlare/api/gen/proto/go/querier/v1"
|
||||||
"github.com/xlab/treeprint"
|
"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 {
|
if query.QueryType == queryTypeProfile || query.QueryType == queryTypeBoth {
|
||||||
req := makeRequest(qm, query)
|
req := makeRequest(qm, query)
|
||||||
logger.Debug("Sending SelectMergeProfile", "request", req, "queryModel", qm)
|
logger.Debug("Sending SelectMergeStacktracesRequest", "request", req, "queryModel", qm)
|
||||||
resp, err := d.client.SelectMergeProfile(ctx, req)
|
resp, err := d.client.SelectMergeStacktraces(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Querying SelectMergeProfile()", "err", err)
|
logger.Error("Querying SelectMergeStacktraces()", "err", err)
|
||||||
response.Error = err
|
response.Error = err
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
@@ -110,9 +108,9 @@ func (d *PhlareDatasource) query(ctx context.Context, pCtx backend.PluginContext
|
|||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeRequest(qm queryModel, query backend.DataQuery) *connect.Request[querierv1.SelectMergeProfileRequest] {
|
func makeRequest(qm queryModel, query backend.DataQuery) *connect.Request[querierv1.SelectMergeStacktracesRequest] {
|
||||||
return &connect.Request[querierv1.SelectMergeProfileRequest]{
|
return &connect.Request[querierv1.SelectMergeStacktracesRequest]{
|
||||||
Msg: &querierv1.SelectMergeProfileRequest{
|
Msg: &querierv1.SelectMergeStacktracesRequest{
|
||||||
ProfileTypeID: qm.ProfileTypeId,
|
ProfileTypeID: qm.ProfileTypeId,
|
||||||
LabelSelector: qm.LabelSelector,
|
LabelSelector: qm.LabelSelector,
|
||||||
Start: query.TimeRange.From.UnixMilli(),
|
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
|
// 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
|
// [level, value, label] columns and by ordering the items in a depth first traversal order we can recreate the whole
|
||||||
// tree back.
|
// tree back.
|
||||||
func responseToDataFrames(prof *googlev1.Profile, profileTypeID string) *data.Frame {
|
func responseToDataFrames(resp *querierv1.SelectMergeStacktracesResponse, profileTypeID string) *data.Frame {
|
||||||
tree := profileAsTree(prof)
|
tree := levelsToTree(resp.Flamegraph.Levels, resp.Flamegraph.Names)
|
||||||
return treeToNestedSetDataFrame(tree, profileTypeID)
|
return treeToNestedSetDataFrame(tree, profileTypeID)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProfileTree struct {
|
// START_OFFSET is offset of the bar relative to previous sibling
|
||||||
Level int
|
const START_OFFSET = 0
|
||||||
Value int64
|
|
||||||
Self int64
|
|
||||||
Function *Function
|
|
||||||
Inlined []*Function
|
|
||||||
locationID uint64
|
|
||||||
|
|
||||||
Nodes []*ProfileTree
|
// VALUE_OFFSET is value or width of the bar
|
||||||
Parent *ProfileTree
|
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 {
|
type Function struct {
|
||||||
@@ -158,7 +249,7 @@ func (pt *ProfileTree) String() string {
|
|||||||
}
|
}
|
||||||
tree := treeprint.New()
|
tree := treeprint.New()
|
||||||
for _, n := range []*ProfileTree{pt} {
|
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})
|
remaining := append([]*branch{}, &branch{nodes: n.Nodes, Tree: b})
|
||||||
for len(remaining) > 0 {
|
for len(remaining) > 0 {
|
||||||
current := remaining[0]
|
current := remaining[0]
|
||||||
@@ -167,11 +258,11 @@ func (pt *ProfileTree) String() string {
|
|||||||
if len(n.Nodes) > 0 {
|
if len(n.Nodes) > 0 {
|
||||||
remaining = append(remaining,
|
remaining = append(remaining,
|
||||||
&branch{
|
&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 {
|
} 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()
|
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 {
|
type CustomMeta struct {
|
||||||
ProfileTypeID string
|
ProfileTypeID string
|
||||||
}
|
}
|
||||||
@@ -368,11 +290,9 @@ func treeToNestedSetDataFrame(tree *ProfileTree, profileTypeID string) *data.Fra
|
|||||||
parts := strings.Split(profileTypeID, ":")
|
parts := strings.Split(profileTypeID, ":")
|
||||||
valueField.Config = &data.FieldConfig{Unit: normalizeUnit(parts[2])}
|
valueField.Config = &data.FieldConfig{Unit: normalizeUnit(parts[2])}
|
||||||
selfField.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}
|
||||||
frame.Fields = data.Fields{levelField, valueField, selfField, lineNumberField}
|
|
||||||
|
|
||||||
labelField := NewEnumField("label", nil)
|
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
|
// Tree can be nil if profile was empty, we can still send empty frame in that case
|
||||||
if tree != nil {
|
if tree != nil {
|
||||||
@@ -380,15 +300,11 @@ func treeToNestedSetDataFrame(tree *ProfileTree, profileTypeID string) *data.Fra
|
|||||||
levelField.Append(int64(tree.Level))
|
levelField.Append(int64(tree.Level))
|
||||||
valueField.Append(tree.Value)
|
valueField.Append(tree.Value)
|
||||||
selfField.Append(tree.Self)
|
selfField.Append(tree.Self)
|
||||||
// todo: inline functions
|
labelField.Append(tree.Name)
|
||||||
// tree.Inlined
|
|
||||||
lineNumberField.Append(tree.Function.Line)
|
|
||||||
labelField.Append(tree.Function.FunctionName)
|
|
||||||
fileNameField.Append(tree.Function.FileName)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
frame.Fields = append(frame.Fields, labelField.GetField(), fileNameField.GetField())
|
frame.Fields = append(frame.Fields, labelField.GetField())
|
||||||
return frame
|
return frame
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+86
-367
@@ -2,8 +2,6 @@ package phlare
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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) {
|
func Test_treeToNestedDataFrame(t *testing.T) {
|
||||||
t.Run("sample profile tree", func(t *testing.T) {
|
t.Run("sample profile tree", func(t *testing.T) {
|
||||||
tree := &ProfileTree{
|
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: 30, Level: 1, Self: 3, Name: "func2", Nodes: []*ProfileTree{
|
||||||
{Value: 15, Level: 2, Self: 4, Function: &Function{FunctionName: "func1:func3", FileName: "3", Line: 3}},
|
{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,
|
require.Equal(t,
|
||||||
[]*data.Field{
|
[]*data.Field{
|
||||||
data.NewField("level", nil, []int64{0, 1, 1, 2}),
|
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("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("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("label", nil, []int64{0, 1, 2, 3}).SetConfig(labelConfig),
|
||||||
data.NewField("fileName", nil, []int64{0, 1, 2, 3}).SetConfig(filenameConfig),
|
|
||||||
}, frame.Fields)
|
}, frame.Fields)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("nil profile tree", func(t *testing.T) {
|
t.Run("nil profile tree", func(t *testing.T) {
|
||||||
frame := treeToNestedSetDataFrame(nil, "memory:alloc_objects:count:space:bytes")
|
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())
|
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) {
|
func Test_seriesToDataFrame(t *testing.T) {
|
||||||
t.Run("single series", func(t *testing.T) {
|
t.Run("single series", func(t *testing.T) {
|
||||||
resp := &connect.Response[querierv1.SelectSeriesResponse]{
|
resp := &connect.Response[querierv1.SelectSeriesResponse]{
|
||||||
|
|||||||
Reference in New Issue
Block a user