Skip to content
This repository has been archived by the owner on Jul 19, 2023. It is now read-only.

Change format to nested set for the dataframe #215

Merged
merged 7 commits into from
Sep 13, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 123 additions & 19 deletions grafana/fire-datasource/pkg/plugin/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,7 @@ func (d *FireDatasource) query(ctx context.Context, pCtx backend.PluginContext,
response.Error = err
return response
}
frame, err := responseToDataFrames(resp)
if err != nil {
response.Error = err
return response
}
frame := responseToDataFrames(resp)

// If query called with streaming on then return a channel
// to subscribe on a client-side and consume updates from a plugin.
Expand Down Expand Up @@ -100,24 +96,132 @@ type CustomMeta struct {
// can have variable number of values but in data.Frame each column needs to have the same number of values.
// In addition, Names, Total, MaxSelf is added to Meta.Custom which may not be the best practice so needs to be
// evaluated later on
func responseToDataFrames(resp *connect.Response[querierv1.SelectMergeStacktracesResponse]) (*data.Frame, error) {
func responseToDataFrames(resp *connect.Response[querierv1.SelectMergeStacktracesResponse]) *data.Frame {
for index, level := range resp.Msg.Flamegraph.Levels {
values, _ := json.Marshal(level.Values)
log.DefaultLogger.Debug(fmt.Sprintf("------- %d %s \n", index, values))
}
tree := levelsToTree(resp.Msg.Flamegraph.Levels, resp.Msg.Flamegraph.Names)
return treeToNestedSetDataFrame(tree)
}

const START_OFFSET = 0
const VALUE_OFFSET = 1
const NAME_OFFSET = 3
const ITEM_OFFSET = 4

type ProfileTree struct {
Start int64
Copy link

@leeoniya leeoniya Sep 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is here to reconstruct the flamebearer struct (for now), but will eventually get dropped once we swap out renderers?

Copy link
Member Author

@aocenas aocenas Sep 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes did not think about direct transform from flamebearer to nested set format so needed the intermediate tree structure but hope to get rid of it eventually. Removing this needs change in the Fire API rather than change on the frontend though.

Value int64
Level int
Name string
Nodes []*ProfileTree
}

func levelsToTree(levels []*querierv1.Level, names []string) *ProfileTree {
tree := &ProfileTree{
Start: 0,
Value: levels[0].Values[VALUE_OFFSET],
Level: 0,
Name: names[levels[0].Values[NAME_OFFSET]],
}

parentsStack := []*ProfileTree{tree}
currentLevel := 1

for {
if currentLevel >= len(levels) {
break
}

// If we still have levels to go this should not happen
if len(parentsStack) == 0 {
log.DefaultLogger.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 have just relative to prev item
offset := int64(0)

for {
if itemIndex >= len(levels[currentLevel].Values) {
break
}

itemStart := levels[currentLevel].Values[itemIndex+START_OFFSET] + offset
itemValue := levels[currentLevel].Values[itemIndex+VALUE_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,
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
offset = itemEnd
} else {
// We went out of parents bounds so lets move to next parent
if len(parentsStack) == 0 {
log.DefaultLogger.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
}

func treeToNestedSetDataFrame(tree *ProfileTree) *data.Frame {
frame := data.NewFrame("response")
frame.Meta = &data.FrameMeta{PreferredVisualization: "flamegraph"}

levelsField := data.NewField("levels", nil, []string{})
levelField := data.NewField("level", nil, []int64{})
valueField := data.NewField("value", nil, []int64{})
labelField := data.NewField("label", nil, []string{})
frame.Fields = data.Fields{levelField, valueField, labelField}

walkTree(tree, func(tree *ProfileTree) {
levelField.Append(int64(tree.Level))
valueField.Append(tree.Value)
labelField.Append(tree.Name)
})
return frame
}

func walkTree(tree *ProfileTree, fn func(tree *ProfileTree)) {
fn(tree)
stack := tree.Nodes

for index, level := range resp.Msg.Flamegraph.Levels {
bytes, err := json.Marshal(level.Values)
if err != nil {
return nil, fmt.Errorf("error marshaling level %d with values %v: %v", index, level, err)
for {
if len(stack) == 0 {
break
}

fn(stack[0])
if stack[0].Nodes != nil {
stack = append(stack[0].Nodes, stack[1:]...)
} else {
stack = stack[1:]
}
levelsField.Append(string(bytes))
}
frame.Fields = []*data.Field{levelsField}
frame.Meta.Custom = CustomMeta{
Names: resp.Msg.Flamegraph.Names,
Total: resp.Msg.Flamegraph.Total,
MaxSelf: resp.Msg.Flamegraph.MaxSelf,
}
return frame, nil
}
72 changes: 70 additions & 2 deletions grafana/fire-datasource/pkg/plugin/query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,87 @@ func Test_responseToDataFrames(t *testing.T) {
},
},
}
frame, err := responseToDataFrames(resp)
require.NoError(t, err)
frame := responseToDataFrames(resp)
require.Equal(t, []string{"func1", "func2", "func3"}, frame.Meta.Custom.(CustomMeta).Names)
require.Equal(t, int64(123), frame.Meta.Custom.(CustomMeta).MaxSelf)
require.Equal(t, int64(987), frame.Meta.Custom.(CustomMeta).Total)
require.Equal(t, 1, len(frame.Fields))
require.Equal(t, data.NewField("levels", nil, []string{"[1,2,3,4]", "[5,6,7,8,9]"}), frame.Fields[0])
}

// 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) {
tree := &ProfileTree{
Start: 0, Value: 100, Level: 0, Name: "root", Nodes: []*ProfileTree{
{
Start: 10, Value: 40, Level: 1, Name: "func1",
},
{Start: 60, Value: 30, Level: 1, Name: "func2", Nodes: []*ProfileTree{
{Start: 61, Value: 15, Level: 2, Name: "func1:func3"},
}},
},
}

frame := treeToNestedSetDataFrame(tree)
require.Equal(t,
[]*data.Field{
data.NewField("level", nil, []int64{0, 1, 1, 2}),
data.NewField("value", nil, []int64{100, 40, 30, 15}),
data.NewField("label", nil, []string{"root", "func1", "func2", "func1:func3"}),
}, frame.Fields)

}

type FakeClient struct {
Req *connect.Request[querierv1.SelectMergeStacktracesRequest]
}

func (f FakeClient) LabelNames(ctx context.Context, c *connect.Request[querierv1.LabelNamesRequest]) (*connect.Response[querierv1.LabelNamesResponse], error) {
//TODO implement me
panic("implement me")
}

func (f FakeClient) ProfileTypes(ctx context.Context, c *connect.Request[querierv1.ProfileTypesRequest]) (*connect.Response[querierv1.ProfileTypesResponse], error) {
panic("implement me")
}
Expand Down
22 changes: 10 additions & 12 deletions grafana/fire-datasource/src/ConfigEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
import React from 'react';
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
import { MyDataSourceOptions } from './types';
import { DataSourceHttpSettings } from '@grafana/ui';


import { DataSourceHttpSettings } from '@grafana/ui';

interface Props extends DataSourcePluginOptionsEditorProps<MyDataSourceOptions> {}

export const ConfigEditor = (props: Props) => {
const { options, onOptionsChange } = props;

return (
<>
return (
<>
<DataSourceHttpSettings
defaultUrl={'http://localhost:4100'}
dataSourceConfig={options}
showAccessOptions={false}
onChange={onOptionsChange}
/>
defaultUrl={'http://localhost:4100'}
dataSourceConfig={options}
showAccessOptions={false}
onChange={onOptionsChange}
/>
</>
);
}
);
};
Loading