Skip to content

Commit

Permalink
draw flamegraph
Browse files Browse the repository at this point in the history
  • Loading branch information
HDT3213 committed Mar 21, 2022
1 parent a64ad06 commit 9201ef8
Show file tree
Hide file tree
Showing 10 changed files with 428 additions and 0 deletions.
Binary file added cases/tree.rdb
Binary file not shown.
6 changes: 6 additions & 0 deletions d3flame/bootstrap.min.css

Large diffs are not rendered by default.

92 changes: 92 additions & 0 deletions d3flame/d3-flamegraph.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
.d3-flame-graph rect {
stroke: #EEEEEE;
fill-opacity: .8;
}

.d3-flame-graph rect:hover {
stroke: #474747;
stroke-width: 0.5;
cursor: pointer;
}

.d3-flame-graph-label {
pointer-events: none;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
font-size: 12px;
font-family: Verdana;
margin-left: 4px;
margin-right: 4px;
line-height: 1.5;
padding: 0 0 0;
font-weight: 400;
color: black;
text-align: left;
}

.d3-flame-graph .fade {
opacity: 0.6 !important;
}

.d3-flame-graph .title {
font-size: 20px;
font-family: Verdana;
}

.d3-flame-graph-tip {
line-height: 1;
font-family: Verdana;
font-size: 12px;
padding: 12px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
border-radius: 2px;
pointer-events: none;
}

/* Creates a small triangle extender for the tooltip */
.d3-flame-graph-tip:after {
box-sizing: border-box;
display: inline;
font-size: 10px;
width: 100%;
line-height: 1;
color: rgba(0, 0, 0, 0.8);
position: absolute;
pointer-events: none;
}

/* Northward tooltips */
.d3-flame-graph-tip.n:after {
content: "\25BC";
margin: -1px 0 0 0;
top: 100%;
left: 0;
text-align: center;
}

/* Eastward tooltips */
.d3-flame-graph-tip.e:after {
content: "\25C0";
margin: -4px 0 0 0;
top: 50%;
left: -8px;
}

/* Southward tooltips */
.d3-flame-graph-tip.s:after {
content: "\25B2";
margin: 0 0 1px 0;
top: -8px;
left: 0;
text-align: center;
}

/* Westward tooltips */
.d3-flame-graph-tip.w:after {
content: "\25B6";
margin: -4px 0 0 -1px;
top: 50%;
left: 100%;
}
1 change: 1 addition & 0 deletions d3flame/d3-flamegraph.min.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions d3flame/d3-tip.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions d3flame/d3.v4.min.js

Large diffs are not rendered by default.

164 changes: 164 additions & 0 deletions d3flame/template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package d3flame

import (
_ "embed"
)

// D3.js is a JavaScript library for manipulating documents based on data.
// https://github.com/d3/d3

// d3-flame-graph is a D3.js plugin that produces flame graphs from hierarchical data.
// https://github.com/spiermar/d3-flame-graph

// https://cdn.jsdelivr.net/gh/spiermar/d3-flame-graph@2.0.3/dist/d3-flamegraph.css
//go:embed d3-flamegraph.css
var d3Css string

// https://d3js.org/d3.v4.min.js
//go:embed d3.v4.min.js
var d3Js string

// https://cdn.jsdelivr.net/gh/spiermar/d3-flame-graph@2.0.3/dist/d3-flamegraph.min.js
//go:embed d3-flamegraph.min.js
var d3FlameGraphJs string

// https://cdnjs.cloudflare.com/ajax/libs/d3-tip/0.9.1/d3-tip.min.js
//go:embed d3-tip.min.js
var d3TipJs string

// https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css
//go:embed bootstrap.min.css
var bootstrapCss string

const html = `
<head>
<title>FlameGraph</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
/* Space out content a bit */
body {
padding-top: 20px;
padding-bottom: 20px;
}
/* Custom page header */
.header {
padding-bottom: 20px;
padding-right: 15px;
padding-left: 15px;
border-bottom: 1px solid #e5e5e5;
}
/* Make the masthead heading the same height as the navigation */
.header h3 {
margin-top: 0;
margin-bottom: 0;
line-height: 40px;
}
/* Customize container */
.container {
max-width: 990px;
}
</style>
</head>
<body>
<style type="text/css">{{.D3Css}}</style>
<style type="text/css">{{.BootstrapCss}}</style>
<script type="text/javascript">{{.D3Js}}</script>
<script type="text/javascript">{{.D3Flame}}</script>
<script type="text/javascript">{{.D3Tip}}</script>
<div class="container">
<div class="header clearfix">
<nav>
<div class="pull-right">
<form class="form-inline" id="form">
<a class="btn" href="javascript: resetZoom();">Reset zoom</a>
<a class="btn" href="javascript: clear();">Clear</a>
<div class="form-group">
<input type="text" class="form-control" id="term">
</div>
<a class="btn btn-primary" href="javascript: search();">Search</a>
</form>
</div>
</nav>
<h3 class="text-muted">d3-flame-graph</h3>
</div>
<div id="chart">
</div>
<hr>
<div id="details">
</div>
</div>
<script type="text/javascript">
var flameGraph = d3.flamegraph()
.width(960)
.cellHeight(18)
.transitionDuration(750)
.minFrameSize(5)
.transitionEase(d3.easeCubic)
.sort(true)
//Example to sort in reverse order
//.sort(function(a,b){ return d3.descending(a.name, b.name);})
.title("")
.onClick(onClick)
.differential(false)
.selfValue(false);
// Example on how to use custom tooltips using d3-tip.
// var tip = d3.tip()
// .direction("s")
// .offset([8, 0])
// .attr('class', 'd3-flame-graph-tip')
// .html(function(d) { return "name: " + d.data.name + ", value: " + d.data.value; });
// flameGraph.tooltip(tip);
var details = document.getElementById("details");
flameGraph.setDetailsElement(details);
// Example on how to use custom labels
// var label = function(d) {
// return "name: " + d.name + ", value: " + d.value;
// }
// flameGraph.label(label);
// Example of how to set fixed chart height
// flameGraph.height(540);
d3.json("stacks.json", function(error, data) {
if (error) return console.warn(error);
d3.select("#chart")
.datum(data)
.call(flameGraph);
});
document.getElementById("form").addEventListener("submit", function(event){
event.preventDefault();
search();
});
function search() {
var term = document.getElementById("term").value;
flameGraph.search(term);
}
function clear() {
document.getElementById('term').value = '';
flameGraph.clear();
}
function resetZoom() {
flameGraph.resetZoom();
}
function onClick(d) {
console.info("Clicked on " + d.data.name);
}
</script>
</script>
</body>
`
77 changes: 77 additions & 0 deletions d3flame/web.go

Large diffs are not rendered by default.

57 changes: 57 additions & 0 deletions helper/flamegraph.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package helper

import (
"encoding/json"
"errors"
"fmt"
"github.com/hdt3213/rdb/core"
"github.com/hdt3213/rdb/d3flame"
"github.com/hdt3213/rdb/model"
"os"
"strconv"
"strings"
)

func FlameGraph(rdbFilename string, port int, separator string, maxDepth int) (chan<- struct{}, error) {
if rdbFilename == "" {
return nil, errors.New("src file path is required")
}
if separator == "" {
separator = ":"
}
rdbFile, err := os.Open(rdbFilename)
if err != nil {
return nil, fmt.Errorf("open rdb %s failed, %v", rdbFilename, err)
}
defer func() {
_ = rdbFile.Close()
}()
p := core.NewDecoder(rdbFile)
root := &d3flame.FlameItem{
Children: make(map[string]*d3flame.FlameItem),
}
err = p.Parse(func(object model.RedisObject) bool {
parts := strings.Split(object.GetKey(), separator)
node := root
parts = append([]string{"db:" + strconv.Itoa(object.GetDBIndex())}, parts...)
for _, part := range parts {
if node.Children[part] == nil {
node.Children[part] = &d3flame.FlameItem{
Name: part,
Children: make(map[string]*d3flame.FlameItem),
}
}
node = node.Children[part]
node.Value += object.GetSize()
}
return true
})
if err != nil {
return nil, err
}
data, err := json.Marshal(root)
if err != nil {
return nil, err
}
return d3flame.Web(data, port), nil
}
28 changes: 28 additions & 0 deletions parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"bufio"
"github.com/hdt3213/rdb/helper"
"net/http"
"os"
"path/filepath"
"testing"
Expand Down Expand Up @@ -245,3 +246,30 @@ func TestFindLargestKeys(t *testing.T) {
t.Error("failed when empty output")
}
}

func TestFlameGraph(t *testing.T) {
srcRdb := filepath.Join("cases", "tree.rdb")
stop, err := helper.FlameGraph(srcRdb, 18888, "", 0)
if err != nil {
t.Errorf("FindLargestKeys failed: %v", err)
}
resp, err := http.Get("http://localhost:18888/flamegraph")
if err != nil {
t.Error(err)
return
}
if resp.StatusCode != http.StatusOK {
t.Errorf("http %d", resp.StatusCode)
return
}
resp, err = http.Get("http://localhost:18888/stacks.json")
if err != nil {
t.Error(err)
return
}
if resp.StatusCode != http.StatusOK {
t.Errorf("http %d", resp.StatusCode)
return
}
stop <- struct{}{}
}

0 comments on commit 9201ef8

Please sign in to comment.