Skip to content

Instantly share code, notes, and snippets.

@thanethomson
Created March 19, 2019 21:23
Show Gist options
  • Save thanethomson/56e1640d057a26186e38ad678a1d114c to your computer and use it in GitHub Desktop.
Save thanethomson/56e1640d057a26186e38ad678a1d114c to your computer and use it in GitHub Desktop.
Tendermint object type graph visualization
package main
import (
"flag"
"fmt"
"os"
"reflect"
"sort"
"strings"
"github.com/tendermint/tendermint/blockchain"
"github.com/tendermint/tendermint/mempool"
"github.com/tendermint/tendermint/state"
"github.com/tendermint/tendermint/consensus"
cfg "github.com/tendermint/tendermint/config"
"github.com/tendermint/tendermint/node"
)
// depNode is a node in a dependency graph.
type depNode struct {
rt reflect.Type
name string
pkg string
kind reflect.Kind
level int
dependsOn map[string]*depNode
}
var supportedStructs = map[string]reflect.Type{
"blockchain.BlockchainReactor": reflect.TypeOf((*blockchain.BlockchainReactor)(nil)).Elem(),
"consensus.ConsensusReactor": reflect.TypeOf((*consensus.ConsensusReactor)(nil)).Elem(),
"config.Config": reflect.TypeOf((*cfg.Config)(nil)).Elem(),
"mempool.Mempool": reflect.TypeOf((*mempool.Mempool)(nil)).Elem(),
"mempool.MempoolReactor": reflect.TypeOf((*mempool.MempoolReactor)(nil)).Elem(),
"node.Node": reflect.TypeOf((*node.Node)(nil)).Elem(),
"state.BlockExecutor": reflect.TypeOf((*state.BlockExecutor)(nil)).Elem(),
}
var rootCmd *flag.FlagSet
var (
flagList bool
flagStruct string
flagDepth int
flagExclude string
)
func init() {
rootCmd = flag.NewFlagSet("root", flag.ExitOnError)
rootCmd.Usage = func() {
fmt.Println(`Object graph visualization for Tendermint data structures.
Usage:
tm-obj-viz [flags] <struct> <outputFile>
Flags:`)
rootCmd.PrintDefaults()
fmt.Println("")
}
rootCmd.IntVar(&flagDepth, "depth", 3, "how many levels deep to recursively analyze the object graph")
rootCmd.BoolVar(&flagList, "list", false, "specify this flag to list the available structs for visualization")
rootCmd.StringVar(&flagStruct, "struct", "node.Node", "the struct whose object graph is to be printed out")
rootCmd.StringVar(&flagExclude, "exclude", "", "a comma-separated list of types to exclude when rendering the graph")
}
func showSupportedStructs() {
structs := make([]string, 0)
for structName := range supportedStructs {
structs = append(structs, structName)
}
sort.SliceStable(structs[:], func(i, j int) bool {
return strings.Compare(structs[i], structs[j]) < 0
})
fmt.Println("Supported structs:")
for _, structName := range structs {
fmt.Printf(" %s\n", structName)
}
}
func longNodeID(t reflect.Type) string {
return fmt.Sprintf("%s.%s", t.PkgPath(), t.Name())
}
func (n *depNode) longPath() string {
return longNodeID(n.rt)
}
func shortNodeID(t reflect.Type) string {
return strings.Replace(t.PkgPath(), "github.com/tendermint/tendermint/", "", 1) + "." + t.Name()
}
func (n *depNode) shortPath() string {
return shortNodeID(n.rt)
}
func (n *depNode) isTendermint() bool {
tmparts := strings.Split(n.pkg, "github.com/tendermint/tendermint")
return (len(tmparts) > 1) && (len(strings.Split(tmparts[1], "vendor/")) < 2)
}
func (n *depNode) attr() string {
attrParts := make([]string, 0)
if n.isTendermint() {
switch n.kind {
case reflect.Struct:
attrParts = append(attrParts, "style=filled", "fillcolor=lightblue")
case reflect.Interface:
attrParts = append(attrParts, "style=filled", "fillcolor=\"#eeeeee\"")
default:
attrParts = append(attrParts, "style=filled", "fillcolor=\"#ccffcc\"")
}
} else {
attrParts = append(attrParts, "style=dashed")
}
return strings.Join(attrParts, ",")
}
func buildDepGraph(n *depNode, maxLevels, curLevel int, seenDeps map[string]*depNode, exclude map[string]interface{}) int {
if maxLevels == curLevel {
return curLevel
}
highestLevel := curLevel
if n.kind == reflect.Struct {
for i := 0; i < n.rt.NumField(); i++ {
subFieldType := n.rt.Field(i).Type
for subFieldType.Kind() == reflect.Ptr {
subFieldType = subFieldType.Elem()
}
if len(subFieldType.Name()) > 0 && len(subFieldType.PkgPath()) > 0 {
longID := longNodeID(subFieldType)
shortID := shortNodeID(subFieldType)
// if we haven't explicitly excluded this particular type
if _, ok := exclude[shortID]; !ok {
subNode, ok := seenDeps[longID]
// if we haven't seen this type before
if !ok {
subNode = &depNode{
rt: subFieldType,
name: subFieldType.Name(),
pkg: subFieldType.PkgPath(),
kind: subFieldType.Kind(),
level: n.level + 1,
dependsOn: make(map[string]*depNode),
}
seenDeps[longID] = subNode
if subNode.kind == reflect.Struct && subNode.isTendermint() {
highestLevel = buildDepGraph(subNode, maxLevels, curLevel+1, seenDeps, exclude)
}
}
n.dependsOn[longID] = subNode
}
}
}
}
return highestLevel
}
// printDepNode recursively prints the given dependency node's sub-dependencies.
func printDepNode(n *depNode, curLevel int, seenEdges map[string]interface{}, rootIndent string) {
indent := rootIndent + strings.Repeat(" ", curLevel+1)
for _, subNode := range n.dependsOn {
edge := fmt.Sprintf("\"%s\"", n.shortPath()) + " -> " + fmt.Sprintf("\"%s\"", subNode.shortPath())
if _, ok := seenEdges[edge]; !ok {
seenEdges[edge] = nil
fmt.Println(indent + edge + ";")
printDepNode(subNode, curLevel+1, seenEdges, rootIndent)
}
}
}
func printGraphFormatting(n *depNode, seenNodes map[string]*depNode) {
attrs := n.attr()
if len(attrs) > 0 {
fmt.Println(fmt.Sprintf(" \"%s\" [%s];", n.shortPath(), n.attr()))
}
for id, subNode := range n.dependsOn {
if _, ok := seenNodes[id]; !ok {
seenNodes[id] = subNode
printGraphFormatting(subNode, seenNodes)
}
}
}
func printLegend() {
fmt.Println(" subgraph cluster_legend {")
fmt.Println(" fontname = \"helvetica\";")
fmt.Println(" fontsize = 18;")
fmt.Println(" label = \"Legend\";")
fmt.Println(" color = lightgray;")
fmt.Println(" \"Tendermint struct\" [style=filled,fillcolor=lightblue];")
fmt.Println(" \"Tendermint interface\" [style=filled,fillcolor=\"#eeeeee\"];")
fmt.Println(" \"Tendermint type\" [style=filled,fillcolor=\"#ccffcc\"];")
fmt.Println(" \"Non-Tendermint type\" [style=dashed];")
fmt.Println(" }")
}
func printExcluded(exclude map[string]interface{}) {
if len(exclude) > 0 {
fmt.Println(" subgraph cluster_exclude {")
fmt.Println(" fontname = \"helvetica\";")
fmt.Println(" fontsize = 18;")
fmt.Println(" label = \"Excluded\";")
fmt.Println(" color = lightgray;")
for e := range exclude {
fmt.Println(" \"" + e + "\" [style=filled,fillcolor=\"#ff8888\"];")
}
fmt.Println(" }")
}
}
func printDepGraph(s reflect.Type, maxLevels int, exclude map[string]interface{}) {
for s.Kind() == reflect.Ptr {
s = s.Elem()
}
rootNode := &depNode{
rt: s,
name: s.Name(),
pkg: s.PkgPath(),
kind: s.Kind(),
level: 0,
dependsOn: make(map[string]*depNode),
}
seenDeps := make(map[string]*depNode)
buildDepGraph(rootNode, maxLevels, 0, seenDeps, exclude)
fmt.Println("digraph G {")
fmt.Println(" rankdir=\"LR\";")
fmt.Println(" ranksep=\"2 equally\";")
fmt.Println(" splines=spline;")
fmt.Println(" ratio=fill;")
fmt.Println(" node [ fontname=\"helvetica\" ];")
// then print all the edges
seenEdges := make(map[string]interface{})
fmt.Println(" subgraph cluster_graph {")
fmt.Println(" style=invis;")
printDepNode(rootNode, 0, seenEdges, " ")
fmt.Println(" }")
// format the different kinds of nodes
seenDeps = make(map[string]*depNode)
printGraphFormatting(rootNode, seenDeps)
// print out which classes have been excluded from the graph
printExcluded(exclude)
// print out the legend
printLegend()
fmt.Println("}")
}
func main() {
if err := rootCmd.Parse(os.Args[1:]); err != nil {
fmt.Println("Failed to parse command line arguments:", err)
os.Exit(1)
}
if flagList {
showSupportedStructs()
os.Exit(0)
}
t, ok := supportedStructs[flagStruct]
if !ok {
fmt.Println("Unrecognized struct:", flagStruct)
os.Exit(1)
}
exclude := make(map[string]interface{})
for _, e := range strings.Split(flagExclude, ",") {
if len(e) > 0 {
exclude[e] = nil
}
}
printDepGraph(t, flagDepth, exclude)
}
@thanethomson
Copy link
Author

tm-obj-viz

tm-obj-viz is a small visualization utility that dumps Tendermint type graphs
in a format usable by Graphviz (specifically dot). This is an effort to aid in
understanding object relationships and help in simplifying them.

Requirements

In order to make use of tm-obj-viz, you will need:

Usage

tm-obj-viz will output the dot format for your chosen Tendermint type. So
the following command will output the relevant text format:

go run ./tm-obj-viz.go -struct node.Node

The following will help in converting your object graph output into an image,
and then displaying it in your browser:

go run ./tm-obj-viz.go -struct node.Node > /tmp/graph && \
    dot -Tsvg /tmp/graph -o /tmp/graph.svg && \
    /path/to/your/browser /tmp/graph.svg

To exclude specific classes from being rendered, simply just exclude their
short type IDs (package.TypeName):

go run ./tm-obj-viz.go -struct node.Node -exclude config.Config

@thanethomson
Copy link
Author

graph

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment