|
|
|
|
@ -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]{ |
|
|
|
|
|