diff --git a/go.mod b/go.mod index 8d335a727..5af80e013 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.18 require ( github.com/confio/ics23/go v0.9.0 github.com/cosmos/cosmos-db v0.0.0-20220822060143-23a8145386c0 + github.com/emicklei/dot v1.2.0 github.com/golang/mock v1.6.0 github.com/golangci/golangci-lint v1.50.1 github.com/stretchr/testify v1.8.1 diff --git a/go.sum b/go.sum index eba09f504..bf922f37c 100644 --- a/go.sum +++ b/go.sum @@ -153,6 +153,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= +github.com/emicklei/dot v1.2.0 h1:WjL422LPltH/ThM9AJQ8HJXEMw9SOxLrglppg/1pFYU= +github.com/emicklei/dot v1.2.0/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= diff --git a/nodedb_test.go b/nodedb_test.go index ad96854a5..c65078188 100644 --- a/nodedb_test.go +++ b/nodedb_test.go @@ -253,6 +253,96 @@ func TestIsFastStorageEnabled_False(t *testing.T) { require.NoError(t, err) } +func assertOrphansAndBranches(t *testing.T, ndb *nodeDB, version int64, branches int, orphanKeys [][]byte) { + var branchCount, orphanIndex int + err := ndb.traverseOrphans(version, func(node *Node) error { + if node.isLeaf() { + require.Equal(t, orphanKeys[orphanIndex], node.key) + orphanIndex++ + } else { + branchCount++ + } + return nil + }) + + require.NoError(t, err) + require.Equal(t, branches, branchCount) +} + +func TestNodeDB_traverseOrphans(t *testing.T) { + tree, _ := getTestTree(0) + var up bool + var err error + + // version 1 + for i := 0; i < 20; i++ { + up, err = tree.Set([]byte{byte(i)}, []byte{byte(i)}) + require.False(t, up) + require.NoError(t, err) + } + _, _, err = tree.SaveVersion() + require.NoError(t, err) + // note: assertions were constructed by hand after inspecting the output of the graphviz below. + // WriteDOTGraphToFile("/tmp/tree_one.dot", tree.ImmutableTree) + + // version 2 + up, err = tree.Set([]byte{byte(19)}, []byte{byte(0)}) + require.True(t, up) + require.NoError(t, err) + _, _, err = tree.SaveVersion() + require.NoError(t, err) + // WriteDOTGraphToFile("/tmp/tree_two.dot", tree.ImmutableTree) + + assertOrphansAndBranches(t, tree.ndb, 1, 5, [][]byte{{byte(19)}}) + + // version 3 + k, up, err := tree.Remove([]byte{byte(0)}) + require.Equal(t, []byte{byte(0)}, k) + require.True(t, up) + require.NoError(t, err) + + _, _, err = tree.SaveVersion() + require.NoError(t, err) + // WriteDOTGraphToFile("/tmp/tree_three.dot", tree.ImmutableTree) + + assertOrphansAndBranches(t, tree.ndb, 2, 4, [][]byte{{byte(0)}}) + + // version 4 + k, up, err = tree.Remove([]byte{byte(1)}) + require.Equal(t, []byte{byte(1)}, k) + require.True(t, up) + require.NoError(t, err) + k, up, err = tree.Remove([]byte{byte(19)}) + require.Equal(t, []byte{byte(0)}, k) + require.True(t, up) + require.NoError(t, err) + + _, _, err = tree.SaveVersion() + require.NoError(t, err) + // WriteDOTGraphToFile("/tmp/tree_four.dot", tree.ImmutableTree) + + assertOrphansAndBranches(t, tree.ndb, 3, 7, [][]byte{{byte(1)}, {byte(19)}}) + + // version 5 + k, up, err = tree.Remove([]byte{byte(10)}) + require.Equal(t, []byte{byte(10)}, k) + require.True(t, up) + require.NoError(t, err) + k, up, err = tree.Remove([]byte{byte(9)}) + require.Equal(t, []byte{byte(9)}, k) + require.True(t, up) + require.NoError(t, err) + up, err = tree.Set([]byte{byte(12)}, []byte{byte(0)}) + require.True(t, up) + require.NoError(t, err) + + _, _, err = tree.SaveVersion() + require.NoError(t, err) + // WriteDOTGraphToFile("/tmp/tree_five.dot", tree.ImmutableTree) + + assertOrphansAndBranches(t, tree.ndb, 4, 8, [][]byte{{byte(9)}, {byte(10)}, {byte(12)}}) +} + func makeHashes(b *testing.B, seed int64) [][]byte { b.StopTimer() rnd := rand.NewSource(seed) diff --git a/tree_dotgraph.go b/tree_dotgraph.go index ef99157a6..43eaeb940 100644 --- a/tree_dotgraph.go +++ b/tree_dotgraph.go @@ -1,12 +1,15 @@ package iavl import ( + "bufio" "bytes" "fmt" "io" + "os" "text/template" ibytes "github.com/cosmos/iavl/internal/bytes" + "github.com/emicklei/dot" ) type graphEdge struct { @@ -106,3 +109,70 @@ func WriteDOTGraph(w io.Writer, tree *ImmutableTree, paths []PathToLeaf) { func mkLabel(label string, pt int, face string) string { return fmt.Sprintf("%s
", face, pt, label) } + +// WriteDOTGraphToFile writes the DOT graph to the given filename. Read like: +// $ dot /tmp/tree_one.dot -Tpng | display +func WriteDOTGraphToFile(filename string, tree *ImmutableTree) { + f1, _ := os.Create(filename) + defer f1.Close() + writer := bufio.NewWriter(f1) + WriteDotGraphv2(writer, tree) + err := writer.Flush() + if err != nil { + panic(err) + } +} + +// WriteDotGraphv2 writes a DOT graph to the given writer. WriteDOTGraph failed to produce valid DOT +// graphs for large trees. This function is a rewrite of WriteDOTGraph that produces valid DOT graphs +func WriteDotGraphv2(w io.Writer, tree *ImmutableTree) { + graph := dot.NewGraph(dot.Directed) + + var traverse func(node *Node, parent *dot.Node, direction string) + traverse = func(node *Node, parent *dot.Node, direction string) { + var label string + if node.isLeaf() { + label = fmt.Sprintf("%v:%v\nv%v", node.key, node.value, node.version) + } else { + label = fmt.Sprintf("%v:%v\nv%v", node.subtreeHeight, node.key, node.version) + } + + n := graph.Node(label) + if parent != nil { + parent.Edge(n, direction) + } + + var leftNode, rightNode *Node + + if node.leftNode != nil { + leftNode = node.leftNode + } else if node.leftHash != nil { + in, err := node.getLeftNode(tree) + if err == nil { + leftNode = in + } + } + + if node.rightNode != nil { + rightNode = node.rightNode + } else if node.rightHash != nil { + in, err := node.getRightNode(tree) + if err == nil { + rightNode = in + } + } + + if leftNode != nil { + traverse(leftNode, &n, "l") + } + if rightNode != nil { + traverse(rightNode, &n, "r") + } + } + + traverse(tree.root, nil, "") + _, err := w.Write([]byte(graph.String())) + if err != nil { + panic(err) + } +}