├── .github ├── ISSUE_TEMPLATE.md └── fib.svg ├── .gitignore ├── .snapshots ├── TestBasicTypes ├── TestEmptyInterface ├── TestEmptyStruct ├── TestFib ├── TestPointerAliasing ├── TestPointerChain ├── TestSliceTree ├── TestTree ├── TestVariadicArguments └── memviz_test-TestMap ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── basic.go ├── collections.go ├── config.go ├── escape_test.go ├── example ├── README.md ├── example-tree.png └── main.go ├── go.mod ├── go.sum ├── memviz.go └── memviz_test.go /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Expected Behaviour 2 | What were you wanting/expecting to happen? 3 | 4 | ### Actual Behaviour 5 | What actually happens at the moment? 6 | 7 | ### Example input 8 | If possible, provide a small code sample which reproduces the issue (or, for a feature request, code that should work after the feature is implemented) 9 | 10 | ### Screenshot/Example output (if applicable) 11 | ```dot 12 | digraph { 13 | 14 | } 15 | ``` 16 | -------------------------------------------------------------------------------- /.github/fib.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | structs 4 | 5 | 6 | 6 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | fib 16 | 17 | index 18 | 19 | 0 20 | 21 | prev 22 | 23 | (nil) 24 | 25 | prevprev 26 | 27 | (nil) 28 | 29 | 30 | 5 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | fib 40 | 41 | index 42 | 43 | 1 44 | 45 | prev 46 | 47 | prevprev 48 | 49 | (nil) 50 | 51 | 52 | 5:f1->6:name 53 | 54 | 55 | 56 | 57 | 4 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | fib 67 | 68 | index 69 | 70 | 2 71 | 72 | prev 73 | 74 | prevprev 75 | 76 | 77 | 4:f2->6:name 78 | 79 | 80 | 81 | 82 | 4:f1->5:name 83 | 84 | 85 | 86 | 87 | 3 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | fib 97 | 98 | index 99 | 100 | 3 101 | 102 | prev 103 | 104 | prevprev 105 | 106 | 107 | 3:f2->5:name 108 | 109 | 110 | 111 | 112 | 3:f1->4:name 113 | 114 | 115 | 116 | 117 | 2 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | fib 127 | 128 | index 129 | 130 | 4 131 | 132 | prev 133 | 134 | prevprev 135 | 136 | 137 | 2:f2->4:name 138 | 139 | 140 | 141 | 142 | 2:f1->3:name 143 | 144 | 145 | 146 | 147 | 1 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | fib 157 | 158 | index 159 | 160 | 5 161 | 162 | prev 163 | 164 | prevprev 165 | 166 | 167 | 1:f2->3:name 168 | 169 | 170 | 171 | 172 | 1:f1->2:name 173 | 174 | 175 | 176 | 177 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | vendor/ 16 | -------------------------------------------------------------------------------- /.snapshots/TestBasicTypes: -------------------------------------------------------------------------------- 1 | digraph structs { 2 | node [shape=Mrecord]; 3 | 3 [label=" 0"]; 4 | 4 [label=" 0"]; 5 | 5 [label=" (0+0i)"]; 6 | 6 [label=" (0+0i)"]; 7 | 7 [label=" 0"]; 8 | 8 [label=" 0"]; 9 | 9 [label=" 0"]; 10 | 10 [label=" 0"]; 11 | 11 [label=" 0"]; 12 | 12 [label=" 0"]; 13 | 13 [label=" 0"]; 14 | 14 [label=" 0"]; 15 | 15 [label=" 0"]; 16 | 16 [label=" (0+0i)"]; 17 | 17 [label=" (0+0i)"]; 18 | 18 [label=" 0"]; 19 | 19 [label=" 0"]; 20 | 20 [label=" 0"]; 21 | 21 [label=" 0"]; 22 | 22 [label=" 0"]; 23 | 2 [label=" basicNumerics |{ uint8 | 0} |{ uint32 | 0} |{ uint64 | 0} |{ int8 | 0} |{ int16 | 0} |{ int32 | 0} |{ int64 | 0} | float32| float64| complex64| complex128|{ byte | 0} |{ rune | 0} |{ uint | 0} |{ int | 0} | uintptr| Ptruint32| Ptruint64| Ptrint8| Ptrint16| Ptrint32| Ptrint64| Ptrfloat32| Ptrfloat64| Ptrcomplex64| Ptrcomplex128| Ptrbyte| Ptrrune| Ptruint| Ptrint| Ptruintptr "]; 24 | 2:f7 -> 3:name; 25 | 2:f8 -> 4:name; 26 | 2:f9 -> 5:name; 27 | 2:f10 -> 6:name; 28 | 2:f15 -> 7:name; 29 | 2:f16 -> 8:name; 30 | 2:f17 -> 9:name; 31 | 2:f18 -> 10:name; 32 | 2:f19 -> 11:name; 33 | 2:f20 -> 12:name; 34 | 2:f21 -> 13:name; 35 | 2:f22 -> 14:name; 36 | 2:f23 -> 15:name; 37 | 2:f24 -> 16:name; 38 | 2:f25 -> 17:name; 39 | 2:f26 -> 18:name; 40 | 2:f27 -> 19:name; 41 | 2:f28 -> 20:name; 42 | 2:f29 -> 21:name; 43 | 2:f30 -> 22:name; 44 | 24 [label=" \"Hello\""]; 45 | 25 [label=" \"interfaceValue\""]; 46 | 1 [label=" basics | numerics|{ string | \"Hi\"} |{ slice | {{<23index0> 0|<23value0> \"Hello\"}|{<23index1> 1|<23value1> \"World\"}}} | ptr| iface "]; 47 | 1:f0 -> 2:name; 48 | 1:f3 -> 24:name; 49 | 1:f4 -> 25:name; 50 | } 51 | 52 | -------------------------------------------------------------------------------- /.snapshots/TestEmptyInterface: -------------------------------------------------------------------------------- 1 | digraph structs { 2 | node [shape=Mrecord]; 3 | 1 [label=" map[string]interface \{\} |{<1key0> \"hello world\"| <1value0> interface \{\}(nil)} "]; 4 | } 5 | 6 | -------------------------------------------------------------------------------- /.snapshots/TestEmptyStruct: -------------------------------------------------------------------------------- 1 | digraph structs { 2 | node [shape=Mrecord]; 3 | 2 [label=" "]; 4 | 1:<1index0> -> 2:name; 5 | 1:<1index1> -> 2:name; 6 | 1 [label=" []struct \{\} |<1index0> 0|<1index1> 1 "]; 7 | } 8 | 9 | -------------------------------------------------------------------------------- /.snapshots/TestFib: -------------------------------------------------------------------------------- 1 | digraph structs { 2 | node [shape=Mrecord]; 3 | 6 [label=" fib |{ index | 0} |{ prev | *memviz_test.fib(nil)} |{ prevprev | *memviz_test.fib(nil)} "]; 4 | 5 [label=" fib |{ index | 1} | prev|{ prevprev | *memviz_test.fib(nil)} "]; 5 | 5:f1 -> 6:name; 6 | 4 [label=" fib |{ index | 2} | prev| prevprev "]; 7 | 4:f1 -> 5:name; 8 | 4:f2 -> 6:name; 9 | 3 [label=" fib |{ index | 3} | prev| prevprev "]; 10 | 3:f1 -> 4:name; 11 | 3:f2 -> 5:name; 12 | 2 [label=" fib |{ index | 4} | prev| prevprev "]; 13 | 2:f1 -> 3:name; 14 | 2:f2 -> 4:name; 15 | 1 [label=" fib |{ index | 5} | prev| prevprev "]; 16 | 1:f1 -> 2:name; 17 | 1:f2 -> 3:name; 18 | } 19 | 20 | -------------------------------------------------------------------------------- /.snapshots/TestPointerAliasing: -------------------------------------------------------------------------------- 1 | digraph structs { 2 | node [shape=Mrecord]; 3 | 2 [label=" \"leaf\""]; 4 | 3 [label=" *string"]; 5 | 3:name -> 2:name; 6 | 1 [label=" | left| right "]; 7 | 1:f0 -> 3:name; 8 | 1:f1 -> 2:name; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /.snapshots/TestPointerChain: -------------------------------------------------------------------------------- 1 | digraph structs { 2 | node [shape=Mrecord]; 3 | 1 [label=" \"Hello world\""]; 4 | 2 [label=" *string"]; 5 | 2:name -> 1:name; 6 | 3 [label=" **string"]; 7 | 3:name -> 2:name; 8 | 4 [label=" ***string"]; 9 | 4:name -> 3:name; 10 | } 11 | 12 | -------------------------------------------------------------------------------- /.snapshots/TestSliceTree: -------------------------------------------------------------------------------- 1 | digraph structs { 2 | node [shape=Mrecord]; 3 | 4 [label=" tree |{ id | 3} |{ left | *memviz_test.tree(nil)} |{ right | *memviz_test.tree(nil)} "]; 4 | 3 [label=" tree |{ id | 1} |{ left | *memviz_test.tree(nil)} | right "]; 5 | 3:f2 -> 4:name; 6 | 5 [label=" tree |{ id | 2} | left|{ right | *memviz_test.tree(nil)} "]; 7 | 5:f1 -> 4:name; 8 | 2 [label=" tree |{ id | 0} | left| right "]; 9 | 2:f1 -> 3:name; 10 | 2:f2 -> 5:name; 11 | 1:<1index0> -> 2:name; 12 | 1:<1index1> -> 3:name; 13 | 1:<1index2> -> 5:name; 14 | 1:<1index3> -> 4:name; 15 | 1 [label=" []*memviz_test.tree |<1index0> 0|<1index1> 1|<1index2> 2|<1index3> 3 "]; 16 | } 17 | 18 | -------------------------------------------------------------------------------- /.snapshots/TestTree: -------------------------------------------------------------------------------- 1 | digraph structs { 2 | node [shape=Mrecord]; 3 | 3 [label=" tree |{ id | 3} |{ left | *memviz_test.tree(nil)} |{ right | *memviz_test.tree(nil)} "]; 4 | 2 [label=" tree |{ id | 1} |{ left | *memviz_test.tree(nil)} | right "]; 5 | 2:f2 -> 3:name; 6 | 4 [label=" tree |{ id | 2} | left|{ right | *memviz_test.tree(nil)} "]; 7 | 4:f1 -> 3:name; 8 | 1 [label=" tree |{ id | 0} | left| right "]; 9 | 1:f1 -> 2:name; 10 | 1:f2 -> 4:name; 11 | } 12 | 13 | -------------------------------------------------------------------------------- /.snapshots/TestVariadicArguments: -------------------------------------------------------------------------------- 1 | digraph structs { 2 | node [shape=Mrecord]; 3 | 3 [label=" tree |{ id | 0} |{ left | *memviz_test.tree(nil)} |{ right | *memviz_test.tree(nil)} "]; 4 | 2 [label=" tree |{ id | 1} |{ left | *memviz_test.tree(nil)} | right "]; 5 | 2:f2 -> 3:name; 6 | 4 [label=" tree |{ id | 2} | left|{ right | *memviz_test.tree(nil)} "]; 7 | 4:f1 -> 3:name; 8 | 1 [label=" tree |{ id | 3} | left| right "]; 9 | 1:f1 -> 2:name; 10 | 1:f2 -> 4:name; 11 | 5 [label=" tree |{ id | 4} | left|{ right | *memviz_test.tree(nil)} "]; 12 | 5:f1 -> 4:name; 13 | } 14 | 15 | -------------------------------------------------------------------------------- /.snapshots/memviz_test-TestMap: -------------------------------------------------------------------------------- 1 | digraph structs { 2 | node [shape=Mrecord]; 3 | 3:<4key0> -> 1:name; 4 | 3 [label=" structMap |{ id | \"leaf\"} |{ links | {{<4key0> *memviz_test.structMap| <4value0> true}}} "]; 5 | 5 [label=" structMap |{ id | \"leaf2\"} |{ links | map[*memviz_test.structMap]bool\{\}} "]; 6 | 2:<2key0> -> 3:name; 7 | 2:<2key1> -> 5:name; 8 | 2:<2key2> -> 1:name; 9 | 2 [label=" map[*memviz_test.structMap]bool |{<2key0> *memviz_test.structMap| <2value0> true}|{<2key1> *memviz_test.structMap| <2value1> true}|{<2key2> *memviz_test.structMap| <2value2> true} "]; 10 | 1 [label=" structMap |{ id | \"parent\"} | links "]; 11 | 1:f1 -> 2:name; 12 | } 13 | 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.x 5 | - master 6 | 7 | matrix: 8 | allow_failures: 9 | - go: master 10 | 11 | install: 12 | - make install 13 | 14 | script: 15 | - make test-ci 16 | 17 | notifications: 18 | email: false 19 | 20 | env: 21 | global: 22 | - MAKEFLAGS=" -j 2" 23 | - GO111MODULE=on 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Bradley Kemp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: install 2 | install: install_linters 3 | 4 | .PHONY: install_linters 5 | install_linters: 6 | curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(GOPATH)/bin v1.12.5 7 | 8 | .PHONY: test 9 | test: lint 10 | go test ./... 11 | 12 | .PHONY: test-ci 13 | test-ci: lint 14 | go run github.com/mattn/goveralls -v -service=travis-ci 15 | 16 | .PHONY: lint 17 | lint: 18 | $(GOPATH)/bin/golangci-lint run 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # memviz [![Build Status](https://travis-ci.org/bradleyjkemp/memviz.svg?branch=master)](https://travis-ci.org/bradleyjkemp/memviz) [![Coverage Status](https://coveralls.io/repos/github/bradleyjkemp/memviz/badge.svg)](https://coveralls.io/github/bradleyjkemp/memviz?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/bradleyjkemp/memviz)](https://goreportcard.com/report/github.com/bradleyjkemp/memviz) [![GoDoc](https://godoc.org/github.com/bradleyjkemp/memviz?status.svg)](https://godoc.org/github.com/bradleyjkemp/memviz) 2 | 3 | How would you rather debug a data structure? 4 | 5 | 6 | 7 | 8 | 9 | 10 | 39 | 40 | 41 |
"Pretty" printedVisual graph
11 |
12 | (*test.fib)(0xc04204a5a0)({
13 |  index: (int) 5,
14 |  prev: (*test.fib)(0xc04204a580)({
15 |   index: (int) 4,
16 |   prev: (*test.fib)(0xc04204a560)({
17 |    index: (int) 3,
18 |    prev: (*test.fib)(0xc04204a540)({
19 |     index: (int) 2,
20 |     prev: (*test.fib)(0xc04204a520)({
21 |      index: (int) 1,
22 |      prev: (*test.fib)(0xc04204a500)({
23 |       index: (int) 0,
24 |       prev: (*test.fib)(),
25 |       prevprev: (*test.fib)()
26 |      }),
27 |      prevprev: (*test.fib)()
28 |     }),
29 |     prevprev: (*test.fib)(0xc04204a500)({
30 |      index: (int) 0,
31 |      prev: (*test.fib)(),
32 |      prevprev: (*test.fib)()
33 |     })
34 |    }),
35 |    .
36 |    .
37 |    .
38 |
42 | 43 | ## Usage 44 | `memviz` takes a pointer to an arbitrary data structure and generates output that can be used to generate an easy to 45 | understand diagram using [graphviz](https://graphviz.org/about/). 46 | 47 | To generate a diagram, first you will need to install graphviz on your system following the instructions [here](https://graphviz.org/download/). 48 | 49 | Next, use ```memviz.Map(out, &data)``` to generate a graphviz [dot file](https://graphviz.org/doc/info/lang.html) and 50 | then pipe the output into graphviz. 51 | 52 | For examples of how to use `memviz`, see the code sample in the [example](https://github.com/bradleyjkemp/memviz/example) 53 | folder and the tests in [memviz_test.go](https://github.com/bradleyjkemp/memviz/blob/master/memviz_test.go). 54 | -------------------------------------------------------------------------------- /basic.go: -------------------------------------------------------------------------------- 1 | package memviz 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strconv" 7 | ) 8 | 9 | func (m *mapper) mapPtrIface(iVal reflect.Value, inlineable bool) (nodeID, string) { 10 | pointee := iVal.Elem() 11 | key := getNodeKey(iVal) 12 | 13 | // inlineable=false so an invalid parentID is fine 14 | pointeeNode, pointeeSummary := m.mapValue(pointee, 0, false) 15 | summary := escapeString(iVal.Type().String()) 16 | m.nodeSummaries[key] = summary 17 | 18 | if !pointee.IsValid() { 19 | m.nodeSummaries[key] += "(" + pointeeSummary + ")" 20 | return pointeeNode, m.nodeSummaries[key] 21 | } 22 | 23 | if !inlineable { 24 | id := m.newBasicNode(iVal, summary) 25 | fmt.Fprintf(m.writer, " %d:name -> %d:name;\n", id, pointeeNode) 26 | return id, summary 27 | } 28 | 29 | return pointeeNode, summary 30 | } 31 | 32 | func (m *mapper) mapString(stringVal reflect.Value, inlineable bool) (nodeID, string) { 33 | // We want the output to look like a Go quoted string literal. The first 34 | // Quote achieves that. The second is to quote it for graphviz itself. 35 | quoted := strconv.Quote(strconv.Quote(stringVal.String())) 36 | // Lastly, quoting adds quotation-marks around the string, but it is 37 | // inserted into a graphviz string literal, so we have to remove those. 38 | quoted = quoted[1 : len(quoted)-1] 39 | if inlineable { 40 | return 0, quoted 41 | } 42 | m.nodeSummaries[getNodeKey(stringVal)] = "string" 43 | return m.newBasicNode(stringVal, quoted), "string" 44 | } 45 | 46 | func (m *mapper) mapBool(stringVal reflect.Value, inlineable bool) (nodeID, string) { 47 | value := fmt.Sprintf("%t", stringVal.Bool()) 48 | if inlineable { 49 | return 0, value 50 | } 51 | m.nodeSummaries[getNodeKey(stringVal)] = "bool" 52 | return m.newBasicNode(stringVal, value), "bool" 53 | } 54 | 55 | func (m *mapper) mapInt(numVal reflect.Value, inlineable bool) (nodeID, string) { 56 | printed := strconv.Itoa(int(numVal.Int())) 57 | if inlineable { 58 | return 0, printed 59 | } 60 | m.nodeSummaries[getNodeKey(numVal)] = "int" 61 | return m.newBasicNode(numVal, printed), "int" 62 | } 63 | 64 | func (m *mapper) mapUint(numVal reflect.Value, inlineable bool) (nodeID, string) { 65 | printed := strconv.Itoa(int(numVal.Uint())) 66 | if inlineable { 67 | return 0, printed 68 | } 69 | m.nodeSummaries[getNodeKey(numVal)] = "uint" 70 | return m.newBasicNode(numVal, printed), "uint" 71 | } 72 | -------------------------------------------------------------------------------- /collections.go: -------------------------------------------------------------------------------- 1 | package memviz 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "unsafe" 7 | ) 8 | 9 | func (m *mapper) mapStruct(structVal reflect.Value) (nodeID, string) { 10 | uType := structVal.Type() 11 | id := m.getNodeID(structVal) 12 | key := getNodeKey(structVal) 13 | m.nodeSummaries[key] = escapeString(uType.String()) 14 | 15 | var fields string 16 | var links []string 17 | for index := 0; index < uType.NumField(); index++ { 18 | field := structVal.Field(index) 19 | if !field.CanAddr() { 20 | // TODO: when does this happen? Can we work around it? 21 | continue 22 | } 23 | field = reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem() 24 | fieldID, summary := m.mapValue(field, id, true) 25 | 26 | // if field was inlined (id == 0) then print summary, else just the name and a link to the actual 27 | if fieldID == 0 { 28 | fields += fmt.Sprintf("|{ %s | %s} ", index, uType.Field(index).Name, summary) 29 | } else { 30 | fields += fmt.Sprintf("| %s", index, uType.Field(index).Name) 31 | links = append(links, fmt.Sprintf(" %d:f%d -> %d:name;\n", id, index, fieldID)) 32 | } 33 | } 34 | 35 | node := fmt.Sprintf(" %d [label=\" %s %s \"];\n", id, structVal.Type().Name(), fields) 36 | 37 | fmt.Fprint(m.writer, node) 38 | for _, link := range links { 39 | fmt.Fprint(m.writer, link) 40 | } 41 | 42 | return id, m.nodeSummaries[key] 43 | } 44 | 45 | func (m *mapper) mapSlice(sliceVal reflect.Value, parentID nodeID, inlineable bool) (nodeID, string) { 46 | sliceID := m.getNodeID(sliceVal) 47 | key := getNodeKey(sliceVal) 48 | sliceType := escapeString(sliceVal.Type().String()) 49 | m.nodeSummaries[key] = sliceType 50 | 51 | if sliceVal.Len() == 0 { 52 | m.nodeSummaries[key] = sliceType + "\\{\\}" 53 | 54 | if inlineable { 55 | return 0, m.nodeSummaries[key] 56 | } 57 | 58 | return m.newBasicNode(sliceVal, m.nodeSummaries[key]), sliceType 59 | } 60 | 61 | // sourceID is the nodeID that links will start from 62 | // if inlined then these come from the parent 63 | // if not inlined then these come from this node 64 | sourceID := sliceID 65 | if inlineable && sliceVal.Len() <= m.inlineableItemLimit { 66 | sourceID = parentID 67 | } 68 | 69 | length := sliceVal.Len() 70 | var elements string 71 | var links []string 72 | for index := 0; index < length; index++ { 73 | indexID, summary := m.mapValue(sliceVal.Index(index), sliceID, true) 74 | if indexID != 0 { 75 | // need pointer to value 76 | elements += fmt.Sprintf("|<%dindex%d> %d", sliceID, index, index) 77 | links = append(links, fmt.Sprintf(" %d:<%dindex%d> -> %d:name;\n", sourceID, sliceID, index, indexID)) 78 | } else { 79 | // field was inlined so print summary 80 | elements += fmt.Sprintf("|{<%dindex%d> %d|<%dvalue%d> %s}", sliceID, index, index, sliceID, index, summary) 81 | } 82 | } 83 | 84 | for _, link := range links { 85 | fmt.Fprint(m.writer, link) 86 | } 87 | 88 | if inlineable && length <= m.inlineableItemLimit { 89 | // inline slice 90 | // remove stored summary so this gets regenerated every time 91 | // we need to do this so that we get a chance to print out the new links 92 | delete(m.nodeSummaries, key) 93 | 94 | // have to remove invalid leading | 95 | return 0, "{" + elements[1:] + "}" 96 | } 97 | 98 | // else create a new node 99 | node := fmt.Sprintf(" %d [label=\" %s %s \"];\n", sliceID, sliceType, elements) 100 | fmt.Fprint(m.writer, node) 101 | 102 | return sliceID, m.nodeSummaries[key] 103 | } 104 | 105 | func (m *mapper) mapMap(mapVal reflect.Value, parentID nodeID, inlineable bool) (nodeID, string) { 106 | // create a string type while escaping graphviz special characters 107 | mapType := escapeString(mapVal.Type().String()) 108 | 109 | nodeKey := getNodeKey(mapVal) 110 | 111 | if mapVal.Len() == 0 { 112 | m.nodeSummaries[nodeKey] = mapType + "\\{\\}" 113 | 114 | if inlineable { 115 | return 0, m.nodeSummaries[nodeKey] 116 | } 117 | 118 | return m.newBasicNode(mapVal, m.nodeSummaries[nodeKey]), mapType 119 | } 120 | 121 | mapID := m.getNodeID(mapVal) 122 | var id nodeID 123 | if inlineable && mapVal.Len() <= m.inlineableItemLimit { 124 | m.nodeSummaries[nodeKey] = mapType 125 | id = parentID 126 | } else { 127 | id = mapID 128 | } 129 | 130 | var links []string 131 | var fields string 132 | for index, mapKey := range mapVal.MapKeys() { 133 | keyID, keySummary := m.mapValue(mapKey, id, true) 134 | valueID, valueSummary := m.mapValue(mapVal.MapIndex(mapKey), id, true) 135 | fields += fmt.Sprintf("|{<%dkey%d> %s| <%dvalue%d> %s}", mapID, index, keySummary, mapID, index, valueSummary) 136 | if keyID != 0 { 137 | links = append(links, fmt.Sprintf(" %d:<%dkey%d> -> %d:name;\n", id, mapID, index, keyID)) 138 | } 139 | if valueID != 0 { 140 | links = append(links, fmt.Sprintf(" %d:<%dvalue%d> -> %d:name;\n", id, mapID, index, valueID)) 141 | } 142 | } 143 | 144 | for _, link := range links { 145 | fmt.Fprint(m.writer, link) 146 | } 147 | 148 | if inlineable && mapVal.Len() <= m.inlineableItemLimit { 149 | // inline map 150 | // remove stored summary so this gets regenerated every time 151 | // we need to do this so that we get a chance to print out the new links 152 | delete(m.nodeSummaries, nodeKey) 153 | 154 | // have to remove invalid leading | 155 | return 0, "{" + fields[1:] + "}" 156 | } 157 | 158 | // else create a new node 159 | node := fmt.Sprintf(" %d [label=\" %s %s \"];\n", id, mapType, fields) 160 | fmt.Fprint(m.writer, node) 161 | 162 | return id, m.nodeSummaries[nodeKey] 163 | } 164 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package memviz 2 | 3 | type Config struct{} 4 | 5 | type Configurator func(*Config) 6 | 7 | func defaultConfig() *Config { 8 | return &Config{} 9 | } 10 | 11 | func New(configurators ...Configurator) *Config { 12 | config := defaultConfig() 13 | for _, configurator := range configurators { 14 | configurator(config) 15 | } 16 | return config 17 | } 18 | -------------------------------------------------------------------------------- /escape_test.go: -------------------------------------------------------------------------------- 1 | package memviz 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/bradleyjkemp/cupaloy/v2" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | var cases = []struct { 13 | input string 14 | output string 15 | }{ 16 | {"Hello world", "Hello world"}, 17 | 18 | // double quotes are escaped 19 | {"\"Hello world\"", "\\\"Hello world\\\""}, 20 | 21 | // brackets not escaped 22 | {"map[string]bool", "map[string]bool"}, 23 | 24 | // braces escaped 25 | {"map[string]struct{}", "map[string]struct\\{\\}"}, 26 | } 27 | 28 | func TestEscapeString(t *testing.T) { 29 | for _, tc := range cases { 30 | assert.Equal(t, tc.output, escapeString(tc.input)) 31 | } 32 | } 33 | 34 | func TestEmptyStruct(t *testing.T) { 35 | set := make([]struct{}, 2) 36 | set[0] = struct{}{} 37 | set[1] = struct{}{} 38 | 39 | b := &bytes.Buffer{} 40 | Map(b, &set) 41 | fmt.Println(b.String()) 42 | cupaloy.SnapshotT(t, b.Bytes()) 43 | } 44 | 45 | func TestEmptyInterface(t *testing.T) { 46 | set := map[string]interface{}{} 47 | set["hello world"] = nil 48 | 49 | b := &bytes.Buffer{} 50 | Map(b, &set) 51 | fmt.Println(b.String()) 52 | cupaloy.SnapshotT(t, b.Bytes()) 53 | } 54 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Generating a diagram 2 | This example shows how to generate a visual diagram of a data structure using `memviz` and `graphviz`. 3 | 4 | ## Setup 5 | First, make sure you have installed `graphviz` on your system by following the instructions [here](https://graphviz.org/download/). 6 | 7 | ## Running the example 8 | After installing `graphviz`, run the example code to generate the dot file: 9 | ```bash 10 | go run main.go 11 | ``` 12 | 13 | Next, use the dot file with the graphviz `dot` command to generate a PNG diagram: 14 | ```bash 15 | dot -Tpng example-tree-data -o diagram.png 16 | ``` 17 | 18 | The resulting `diagram.png` image will look like this: 19 | 20 | ![Example tree diagram](./example-tree.png) 21 | 22 | For more examples see the tests in [memviz_test.go](https://github.com/bradleyjkemp/memviz/blob/master/memviz_test.go). 23 | -------------------------------------------------------------------------------- /example/example-tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradleyjkemp/memviz/8656e47d4247a056491ee13f7ce8d1406322fc84/example/example-tree.png -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "github.com/bradleyjkemp/memviz" 6 | "io/ioutil" 7 | ) 8 | 9 | type tree struct { 10 | id int 11 | left *tree 12 | right *tree 13 | } 14 | 15 | func main() { 16 | root := &tree{ 17 | id: 0, 18 | left: &tree{ 19 | id: 1, 20 | }, 21 | right: &tree{ 22 | id: 2, 23 | }, 24 | } 25 | leaf := &tree{ 26 | id: 3, 27 | } 28 | 29 | root.left.right = leaf 30 | root.right.left = leaf 31 | 32 | buf := &bytes.Buffer{} 33 | memviz.Map(buf, &root) 34 | err := ioutil.WriteFile("example-tree-data", buf.Bytes(), 0644) 35 | if err != nil { 36 | panic(err) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bradleyjkemp/memviz 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/bradleyjkemp/cupaloy/v2 v2.5.0 7 | github.com/stretchr/testify v1.6.1 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bradleyjkemp/cupaloy/v2 v2.5.0 h1:XI37Pqyl+msFaJDYL3JuPFKGUgnVxyJp+gQZQGiz2nA= 2 | github.com/bradleyjkemp/cupaloy/v2 v2.5.0/go.mod h1:TD5UU0rdYTbu/TtuwFuWrtiRARuN7mtRipvs/bsShSE= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 7 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 8 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 9 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 10 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 11 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 12 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 14 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 15 | -------------------------------------------------------------------------------- /memviz.go: -------------------------------------------------------------------------------- 1 | package memviz // import "github.com/bradleyjkemp/memviz" 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "reflect" 7 | "strings" 8 | ) 9 | 10 | //var spewer = &spew.ConfigState{ 11 | // Indent: " ", 12 | // SortKeys: true, // maps should be spewed in a deterministic order 13 | // DisablePointerAddresses: true, // don't spew the addresses of pointers 14 | // DisableCapacities: true, // don't spew capacities of collections 15 | // SpewKeys: true, // if unable to sort map keys then spew keys to strings and sort those 16 | // MaxDepth: 1, 17 | //} 18 | 19 | type nodeKey string 20 | type nodeID int 21 | 22 | var nilKey nodeKey = "nil0" 23 | 24 | type mapper struct { 25 | writer io.Writer 26 | nodeIDs map[nodeKey]nodeID 27 | nodeSummaries map[nodeKey]string 28 | inlineableItemLimit int 29 | } 30 | 31 | // Map prints the given datastructure using the default config 32 | func Map(w io.Writer, is ...interface{}) { 33 | defaultConfig().Map(w, is...) 34 | } 35 | 36 | // Map prints out a Graphviz digraph of the given datastructure to the given io.Writer 37 | func (c *Config) Map(w io.Writer, is ...interface{}) { 38 | var iVals []reflect.Value 39 | for _, i := range is { 40 | iVal := reflect.ValueOf(i) 41 | if !iVal.CanAddr() { 42 | if iVal.Kind() != reflect.Ptr && iVal.Kind() != reflect.Interface { 43 | fmt.Fprint(w, "error: cannot map unaddressable value") 44 | return 45 | } 46 | 47 | iVal = iVal.Elem() 48 | } 49 | iVals = append(iVals, iVal) 50 | } 51 | 52 | m := &mapper{ 53 | w, 54 | map[nodeKey]nodeID{nilKey: 0}, 55 | map[nodeKey]string{nilKey: "nil"}, 56 | 2, 57 | } 58 | 59 | fmt.Fprintln(w, "digraph structs {") 60 | fmt.Fprintln(w, " node [shape=Mrecord];") 61 | for _, iVal := range iVals { 62 | m.mapValue(iVal, 0, false) 63 | } 64 | fmt.Fprintln(w, "}") 65 | } 66 | 67 | // for values that aren't addressable keep an incrementing counter instead 68 | var keyCounter int 69 | 70 | func getNodeKey(val reflect.Value) nodeKey { 71 | if val.CanAddr() { 72 | return nodeKey(fmt.Sprint(val.Kind()) + fmt.Sprint(val.UnsafeAddr())) 73 | } 74 | 75 | // reverse order of type and "address" to prevent (incredibly unlikely) collisions 76 | keyCounter++ 77 | return nodeKey(fmt.Sprint(keyCounter) + fmt.Sprint(val.Kind())) 78 | } 79 | 80 | func (m *mapper) getNodeID(iVal reflect.Value) nodeID { 81 | // have to key on kind and address because a struct and its first element have the same UnsafeAddr() 82 | key := getNodeKey(iVal) 83 | var id nodeID 84 | var ok bool 85 | if id, ok = m.nodeIDs[key]; !ok { 86 | id = nodeID(len(m.nodeIDs)) 87 | m.nodeIDs[key] = id 88 | return id 89 | } 90 | 91 | return id 92 | } 93 | 94 | func (m *mapper) newBasicNode(iVal reflect.Value, text string) nodeID { 95 | id := m.getNodeID(iVal) 96 | fmt.Fprintf(m.writer, " %d [label=\" %s\"];\n", id, text) 97 | return id 98 | } 99 | 100 | func (m *mapper) mapValue(iVal reflect.Value, parentID nodeID, inlineable bool) (nodeID, string) { 101 | if !iVal.IsValid() { 102 | // zero value => probably result of nil pointer 103 | return m.nodeIDs[nilKey], m.nodeSummaries[nilKey] 104 | } 105 | 106 | key := getNodeKey(iVal) 107 | if summary, ok := m.nodeSummaries[key]; ok { 108 | // already seen this address so no need to map again 109 | return m.nodeIDs[key], summary 110 | } 111 | 112 | switch iVal.Kind() { 113 | // Indirections 114 | case reflect.Ptr, reflect.Interface: 115 | return m.mapPtrIface(iVal, inlineable) 116 | 117 | // Collections 118 | case reflect.Struct: 119 | return m.mapStruct(iVal) 120 | case reflect.Slice, reflect.Array: 121 | return m.mapSlice(iVal, parentID, inlineable) 122 | case reflect.Map: 123 | return m.mapMap(iVal, parentID, inlineable) 124 | 125 | // Simple types 126 | case reflect.Bool: 127 | return m.mapBool(iVal, inlineable) 128 | case reflect.String: 129 | return m.mapString(iVal, inlineable) 130 | case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: 131 | return m.mapInt(iVal, inlineable) 132 | case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: 133 | return m.mapUint(iVal, inlineable) 134 | 135 | // If we've missed anything then just fmt.Sprint it 136 | default: 137 | return m.newBasicNode(iVal, fmt.Sprint(iVal.Interface())), iVal.Kind().String() 138 | } 139 | } 140 | 141 | var escaper = strings.NewReplacer( 142 | "{", "\\{", 143 | "}", "\\}", 144 | "\"", "\\\"", 145 | ) 146 | 147 | func escapeString(s string) string { 148 | return escaper.Replace(s) 149 | } 150 | -------------------------------------------------------------------------------- /memviz_test.go: -------------------------------------------------------------------------------- 1 | package memviz_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/bradleyjkemp/cupaloy/v2" 11 | "github.com/bradleyjkemp/memviz" 12 | ) 13 | 14 | //nolint:structcheck,megacheck 15 | type basicNumerics struct { 16 | uint8 uint8 17 | uint32 uint32 18 | uint64 uint64 19 | int8 int8 20 | int16 int16 21 | int32 int32 22 | int64 int64 23 | float32 float32 24 | float64 float64 25 | complex64 complex64 26 | complex128 complex128 27 | byte byte 28 | rune rune 29 | uint uint 30 | int int 31 | uintptr uintptr 32 | 33 | Ptruint32 *uint32 34 | Ptruint64 *uint64 35 | Ptrint8 *int8 36 | Ptrint16 *int16 37 | Ptrint32 *int32 38 | Ptrint64 *int64 39 | Ptrfloat32 *float32 40 | Ptrfloat64 *float64 41 | Ptrcomplex64 *complex64 42 | Ptrcomplex128 *complex128 43 | Ptrbyte *byte 44 | Ptrrune *rune 45 | Ptruint *uint 46 | Ptrint *int 47 | Ptruintptr *uintptr 48 | } 49 | 50 | type basics struct { 51 | numerics *basicNumerics 52 | string string 53 | slice []string 54 | ptr *string 55 | iface interface{} 56 | } 57 | 58 | func TestBasicTypes(t *testing.T) { 59 | str := "Hello" 60 | b := &basics{ 61 | new(basicNumerics), 62 | "Hi", 63 | []string{"Hello", "World"}, 64 | &str, 65 | "interfaceValue", 66 | } 67 | 68 | v := reflect.ValueOf(b.numerics).Elem() 69 | for i := 0; i < v.NumField(); i++ { 70 | if f := v.Field(i); f.Kind() == reflect.Ptr { 71 | fv := reflect.New(f.Type().Elem()) 72 | f.Set(fv) 73 | } 74 | } 75 | 76 | buf := &bytes.Buffer{} 77 | memviz.Map(buf, b) 78 | fmt.Println(buf.String()) 79 | cupaloy.SnapshotT(t, buf.Bytes()) 80 | } 81 | 82 | type tree struct { 83 | id int 84 | left *tree 85 | right *tree 86 | } 87 | 88 | func TestTree(t *testing.T) { 89 | root := &tree{ 90 | id: 0, 91 | left: &tree{ 92 | id: 1, 93 | }, 94 | right: &tree{ 95 | id: 2, 96 | }, 97 | } 98 | leaf := &tree{ 99 | id: 3, 100 | } 101 | 102 | root.left.right = leaf 103 | root.right.left = leaf 104 | 105 | b := &bytes.Buffer{} 106 | memviz.Map(b, root) 107 | fmt.Println(b.String()) 108 | cupaloy.SnapshotT(t, b.Bytes()) 109 | } 110 | 111 | func TestVariadicArguments(t *testing.T) { 112 | leaf := &tree{ 113 | 0, 114 | nil, 115 | nil, 116 | } 117 | inner1 := &tree{ 118 | 1, 119 | nil, 120 | leaf, 121 | } 122 | inner2 := &tree{ 123 | 2, 124 | leaf, 125 | nil, 126 | } 127 | root1 := &tree{ 128 | 3, 129 | inner1, 130 | inner2, 131 | } 132 | root2 := &tree{ 133 | 4, 134 | inner2, 135 | nil, 136 | } 137 | 138 | b := &bytes.Buffer{} 139 | memviz.Map(b, root1, root2) 140 | fmt.Println(b.String()) 141 | cupaloy.SnapshotT(t, b.Bytes()) 142 | } 143 | 144 | func TestSliceTree(t *testing.T) { 145 | root := &tree{ 146 | id: 0, 147 | left: &tree{ 148 | id: 1, 149 | }, 150 | right: &tree{ 151 | id: 2, 152 | }, 153 | } 154 | leaf := &tree{ 155 | id: 3, 156 | } 157 | 158 | root.left.right = leaf 159 | root.right.left = leaf 160 | 161 | slice := []*tree{root, root.left, root.right, leaf} 162 | 163 | b := &bytes.Buffer{} 164 | memviz.Map(b, &slice) 165 | fmt.Println(b.String()) 166 | cupaloy.SnapshotT(t, b.Bytes()) 167 | } 168 | 169 | type fib struct { 170 | index int 171 | prev *fib 172 | prevprev *fib 173 | } 174 | 175 | func TestFib(t *testing.T) { 176 | f0 := &fib{ 177 | 0, 178 | nil, 179 | nil, 180 | } 181 | f1 := &fib{ 182 | 1, 183 | f0, 184 | nil, 185 | } 186 | f2 := &fib{ 187 | 2, 188 | f1, 189 | f0, 190 | } 191 | f3 := &fib{ 192 | 3, 193 | f2, 194 | f1, 195 | } 196 | f4 := &fib{ 197 | 4, 198 | f3, 199 | f2, 200 | } 201 | f5 := &fib{ 202 | 5, 203 | f4, 204 | f3, 205 | } 206 | 207 | b := &bytes.Buffer{} 208 | memviz.Map(b, f5) 209 | fmt.Println(b.String()) 210 | cupaloy.SnapshotT(t, b.Bytes()) 211 | } 212 | 213 | type structMap struct { 214 | id string 215 | links map[*structMap]bool 216 | } 217 | 218 | func TestMap(t *testing.T) { 219 | leaf := &structMap{ 220 | "leaf", 221 | nil, 222 | } 223 | 224 | leaf2 := &structMap{ 225 | "leaf2", 226 | nil, 227 | } 228 | 229 | parent := &structMap{ 230 | "parent", 231 | map[*structMap]bool{ 232 | leaf: true, 233 | leaf2: true, 234 | }, 235 | } 236 | 237 | leaf.links = map[*structMap]bool{parent: true} 238 | parent.links[parent] = true 239 | 240 | b := &bytes.Buffer{} 241 | memviz.Map(b, parent) 242 | fmt.Println(b.String()) 243 | 244 | // TODO: enable snapshot assertion once map keys are sorted (and so this has stable output) 245 | err := cupaloy.Snapshot(b.Bytes()) 246 | if err != nil { 247 | fmt.Fprintln(os.Stderr, err) 248 | } 249 | } 250 | 251 | func TestPointerChain(t *testing.T) { 252 | str := "Hello world" 253 | str2 := &str 254 | str3 := &str2 255 | str4 := &str3 256 | 257 | b := &bytes.Buffer{} 258 | memviz.Map(b, &str4) 259 | fmt.Println(b.String()) 260 | cupaloy.SnapshotT(t, b.Bytes()) 261 | } 262 | 263 | func TestPointerAliasing(t *testing.T) { 264 | leaf := "leaf" 265 | parent0 := &leaf 266 | parent1 := &parent0 267 | parent2 := &leaf 268 | root := struct { 269 | left **string 270 | right *string 271 | }{ 272 | parent1, 273 | parent2, 274 | } 275 | 276 | b := &bytes.Buffer{} 277 | memviz.Map(b, &root) 278 | fmt.Println(b.String()) 279 | cupaloy.SnapshotT(t, b.Bytes()) 280 | } 281 | --------------------------------------------------------------------------------