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)
+ }
+}