Skip to content

Commit

Permalink
trim node for flamegraph
Browse files Browse the repository at this point in the history
  • Loading branch information
HDT3213 committed Mar 27, 2022
1 parent 5a8a85a commit a9f2abf
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 30 deletions.
41 changes: 39 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/HDT3213/rdb)](https://goreportcard.com/report/github.com/HDT3213/rdb)
[![Go Reference](https://pkg.go.dev/badge/github.com/hdt3213/rdb.svg)](https://pkg.go.dev/github.com/hdt3213/rdb)

This project is a parser for Redis' RDB files.
This project is a parser for Redis' RDB files.

It provides utilities to:

Expand All @@ -19,22 +19,26 @@ Thanks sripathikrishnan for his [redis-rdb-tools](https://github.com/sripathikri
# Install

If you have installed `go` on your compute, just simply use:

```
go get github.com/hdt3213/rdb
```

Or, you can download executable binary file from [releases](https://github.com/HDT3213/rdb/releases) and put its path to PATH environment.
Or, you can download executable binary file from [releases](https://github.com/HDT3213/rdb/releases) and put its path to
PATH environment.

use `rdb` command in terminal, you can see it's manual

# Convert to Json

Usage:

```
rdb -c json -o <output_path> <source_path>
```

example:

```
rdb -c json -o intset_16.json cases/intset_16.rdb
```
Expand All @@ -57,16 +61,19 @@ The examples for json result:
# Generate Memory Report

RDB uses rdb encoded size to estimate redis memory usage.

```
rdb -c memory -o <output_path> <source_path>
```

Example:

```
rdb -c memory -o mem.csv cases/memory.rdb
```

The examples for csv result:

```csv
database,key,type,size,size_readable,element_count
0,hash,hash,64,64B,2
Expand All @@ -81,16 +88,19 @@ database,key,type,size,size_readable,element_count
# Find Biggest Keys

RDB can find biggest N keys in file

```
rdb -c bigkey -n <result_number> <source_path>
```

Example:

```
rdb -c bigkey -n 5 cases/memory.rdb
```

The examples for csv result:

```csv
database,key,type,size,size_readable,element_count
0,large,string,2056,2K,0
Expand All @@ -103,11 +113,13 @@ database,key,type,size,size_readable,element_count
# Convert to AOF

Usage:

```
rdb -c aof -o <output_path> <source_path>
```

Example:

```
rdb -c aof -o mem.aof cases/memory.rdb
```
Expand All @@ -124,6 +136,31 @@ $7
aaaaaaa
```

# Flame Graph

In many cases there is not a few very large key but lots of small keys that occupied most memory.

RDB tool could separate keys by given delimeter, then aggregate keys with same prefix.

Finally RDB tool presents the result as flame graph, with which you could find out which kind of keys consumed most
memory.

Usage:

```
rdb -c flamegraph [-port <port>] [-sep <separator>] <source_path>
```

Example:

```
rdb -c flamegraph -port 16379 -sep : dump.rdb
```

![](https://s2.loli.net/2022/03/27/eNGvVIdAuWp8EhT.png)

In the example, the keys of pattern `Comment:*` use 8.463% memory.

# Customize data usage

```go
Expand Down
4 changes: 2 additions & 2 deletions cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Examples:
4. get largest keys
rdb -c bigkey -o dump.aof dump.rdb
5. draw flamegraph
rdb -c flamegraph [-port <port>] [-sep <separator>]
rdb -c flamegraph -port 16379 -sep : dump.rdb
`

func main() {
Expand Down Expand Up @@ -75,7 +75,7 @@ func main() {
err = helper.FindBiggestKeys(src, n, outputFile)
}
case "flamegraph":
_, err = helper.FlameGraph(src, port, separator, 0)
_, err = helper.FlameGraph(src, port, separator)
<-make(chan struct{})
default:
println("unknown command")
Expand Down
5 changes: 3 additions & 2 deletions d3flame/template.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package d3flame

import (
// use go embed to load js and css source file
_ "embed"
)

Expand Down Expand Up @@ -28,7 +29,7 @@ var d3TipJs string

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

const html = `
<head>
Expand Down Expand Up @@ -66,7 +67,7 @@ const html = `
</head>
<body>
<style type="text/css">{{.D3Css}}</style>
<style type="text/css">{{.BootstrapCss}}</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>
Expand Down
25 changes: 16 additions & 9 deletions d3flame/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ var flameTmplData = &struct {
D3Js template.JS
D3Flame template.JS
D3Tip template.JS
BootstrapCss template.CSS
BootstrapCSS template.CSS
}{
D3Css: template.CSS(d3Css),
D3Js: template.JS(d3Js),
D3Flame: template.JS(d3FlameGraphJs),
D3Tip: template.JS(d3TipJs),
BootstrapCss: template.CSS(bootstrapCss),
BootstrapCSS: template.CSS(bootstrapCSS),
}

func flamegraph(w http.ResponseWriter, r *http.Request) {
Expand All @@ -38,8 +38,6 @@ type FlameItem struct {
Children children `json:"c,omitempty"`
}

type children map[string]*FlameItem

func (ch children) MarshalJSON() ([]byte, error) {
list := make([]*FlameItem, 0, len(ch))
for _, v := range ch {
Expand All @@ -48,16 +46,25 @@ func (ch children) MarshalJSON() ([]byte, error) {
return json.Marshal(list)
}

// AddChild add a child node into FlameItem
func (node *FlameItem) AddChild(n *FlameItem) {
node.Children[n.Name] = n
}

type children map[string]*FlameItem

// Web starts a web server to render flamegraph
func Web(data []byte, port int) chan<- struct{} {
server := &http.Server{
Addr: ":" + strconv.Itoa(port),
}
http.HandleFunc("/flamegraph", flamegraph)
http.HandleFunc("/stacks.json", func(w http.ResponseWriter, r *http.Request) {
mux := http.NewServeMux()
mux.HandleFunc("/flamegraph", flamegraph)
mux.HandleFunc("/stacks.json", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write(data)
})
server := &http.Server{
Addr: ":" + strconv.Itoa(port),
Handler: mux,
}
fmt.Printf("see http://localhost:%d/flamegraph\n", port)
stop := make(chan struct{})
go func() {
Expand Down
72 changes: 58 additions & 14 deletions helper/flamegraph.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import (
"strings"
)

func FlameGraph(rdbFilename string, port int, separator string, maxDepth int) (chan<- struct{}, error) {
// TrimThreshold is the min count of keys to enable trim
var TrimThreshold = 1000

// FlameGraph draws flamegraph in web page to analysis memory usage pattern
func FlameGraph(rdbFilename string, port int, separator string) (chan<- struct{}, error) {
if rdbFilename == "" {
return nil, errors.New("src file path is required")
}
Expand All @@ -33,28 +37,68 @@ func FlameGraph(rdbFilename string, port int, separator string, maxDepth int) (c
root := &d3flame.FlameItem{
Children: make(map[string]*d3flame.FlameItem),
}
var count int
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()
}
count++
addObject(root, separator, object)
return true
})
if err != nil {
return nil, err
}
if count >= TrimThreshold {
trimData(root)
}
data, err := json.Marshal(root)
if err != nil {
return nil, err
}
return d3flame.Web(data, port), nil
}

func addObject(root *d3flame.FlameItem, separator string, object model.RedisObject) {
node := root
parts := strings.Split(object.GetKey(), separator)
parts = append([]string{"db:" + strconv.Itoa(object.GetDBIndex())}, parts...)
for _, part := range parts {
if node.Children[part] == nil {
n := &d3flame.FlameItem{
Name: part,
Children: make(map[string]*d3flame.FlameItem),
}
node.AddChild(n)
}
node = node.Children[part]
node.Value += object.GetSize()
}
}

// bigNodeThreshold is the min size
var bigNodeThreshold = 1024 * 1024 // 1MB

func trimData(root *d3flame.FlameItem) {
// trim long tail
queue := []*d3flame.FlameItem{
root,
}
for len(queue) > 0 {
// Aggregate leaf nodes
node := queue[0]
queue = queue[1:]
leafSum := 0
for key, child := range node.Children {
if len(child.Children) == 0 && child.Value < bigNodeThreshold { // child is a leaf node
delete(node.Children, key) // remove small leaf keys
leafSum += child.Value
}
queue = append(queue, child) // reserve big key
}
if leafSum > 0 {
n := &d3flame.FlameItem{
Name: "others",
Value: leafSum,
}
node.AddChild(n)
}
}
}
14 changes: 13 additions & 1 deletion parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,9 @@ func TestFindLargestKeys(t *testing.T) {
}

func TestFlameGraph(t *testing.T) {
helper.TrimThreshold = 1
srcRdb := filepath.Join("cases", "tree.rdb")
stop, err := helper.FlameGraph(srcRdb, 18888, "", 0)
stop, err := helper.FlameGraph(srcRdb, 18888, "")
if err != nil {
t.Errorf("FindLargestKeys failed: %v", err)
}
Expand All @@ -272,4 +273,15 @@ func TestFlameGraph(t *testing.T) {
return
}
stop <- struct{}{}

stop, err = helper.FlameGraph(srcRdb, 0, "")
if err != nil {
t.Errorf("FindLargestKeys failed: %v", err)
}
stop <- struct{}{}

stop, err = helper.FlameGraph("", 0, "")
if err == nil || err.Error() != "src file path is required" {
t.Error("expect error: src file path is required")
}
}

0 comments on commit a9f2abf

Please sign in to comment.