From 8967633ad07057be1d48bb428d35336dff4c22b1 Mon Sep 17 00:00:00 2001 From: cool-developer <51834436+cool-develope@users.noreply.github.com> Date: Wed, 27 Mar 2024 16:56:43 -0400 Subject: [PATCH] feat: optimize the fast key format and encoding (#923) --- internal/encoding/encoding.go | 22 ++++++- keyformat/prefix_formatter.go | 42 ++++++++++++++ mutable_tree.go | 106 ++++++++++++++++++---------------- node.go | 26 +++++---- node_test.go | 8 +-- nodedb.go | 35 ++++++----- 6 files changed, 152 insertions(+), 87 deletions(-) create mode 100644 keyformat/prefix_formatter.go diff --git a/internal/encoding/encoding.go b/internal/encoding/encoding.go index d649711a5..6e390c36c 100644 --- a/internal/encoding/encoding.go +++ b/internal/encoding/encoding.go @@ -30,6 +30,7 @@ var uvarintPool = &sync.Pool{ // decodeBytes decodes a varint length-prefixed byte slice, returning it along with the number // of input bytes read. +// Assumes bz will not be mutated. func DecodeBytes(bz []byte) ([]byte, int, error) { s, n, err := DecodeUvarint(bz) if err != nil { @@ -51,9 +52,7 @@ func DecodeBytes(bz []byte) ([]byte, int, error) { if len(bz) < end { return nil, n, fmt.Errorf("insufficient bytes decoding []byte of length %v", size) } - bz2 := make([]byte, size) - copy(bz2, bz[n:end]) - return bz2, end, nil + return bz[n:end], end, nil } // decodeUvarint decodes a varint-encoded unsigned integer from a byte slice, returning it and the @@ -97,6 +96,23 @@ func EncodeBytes(w io.Writer, bz []byte) error { return err } +var hashLenBz []byte + +func init() { + hashLenBz = make([]byte, 1) + binary.PutUvarint(hashLenBz, 32) +} + +// Encode 32 byte long hash +func Encode32BytesHash(w io.Writer, bz []byte) error { + _, err := w.Write(hashLenBz) + if err != nil { + return err + } + _, err = w.Write(bz) + return err +} + // encodeBytesSlice length-prefixes the byte slice and returns it. func EncodeBytesSlice(bz []byte) ([]byte, error) { buf := bufPool.Get().(*bytes.Buffer) diff --git a/keyformat/prefix_formatter.go b/keyformat/prefix_formatter.go new file mode 100644 index 000000000..873c3a7e1 --- /dev/null +++ b/keyformat/prefix_formatter.go @@ -0,0 +1,42 @@ +package keyformat + +import "encoding/binary" + +// This file builds some dedicated key formatters for what appears in benchmarks. + +// Prefixes a single byte before a 32 byte hash. +type FastPrefixFormatter struct { + prefix byte + length int + prefixSlice []byte +} + +func NewFastPrefixFormatter(prefix byte, length int) *FastPrefixFormatter { + return &FastPrefixFormatter{prefix: prefix, length: length, prefixSlice: []byte{prefix}} +} + +func (f *FastPrefixFormatter) Key(bz []byte) []byte { + key := make([]byte, 1+f.length) + key[0] = f.prefix + copy(key[1:], bz) + return key +} + +func (f *FastPrefixFormatter) Scan(key []byte, a interface{}) { + scan(a, key[1:]) +} + +func (f *FastPrefixFormatter) KeyInt64(bz int64) []byte { + key := make([]byte, 1+f.length) + key[0] = f.prefix + binary.BigEndian.PutUint64(key[1:], uint64(bz)) + return key +} + +func (f *FastPrefixFormatter) Prefix() []byte { + return f.prefixSlice +} + +func (f *FastPrefixFormatter) Length() int { + return 1 + f.length +} diff --git a/mutable_tree.go b/mutable_tree.go index fc915d6a0..6308c35ab 100644 --- a/mutable_tree.go +++ b/mutable_tree.go @@ -263,64 +263,68 @@ func (tree *MutableTree) set(key []byte, value []byte) (updated bool, err error) func (tree *MutableTree) recursiveSet(node *Node, key []byte, value []byte) ( newSelf *Node, updated bool, err error, ) { - version := tree.version + 1 - if node.isLeaf() { - if !tree.skipFastStorageUpgrade { - tree.addUnsavedAddition(key, fastnode.NewNode(key, value, version)) - } - switch bytes.Compare(key, node.key) { - case -1: // setKey < leafKey - return &Node{ - key: node.key, - subtreeHeight: 1, - size: 2, - nodeKey: nil, - leftNode: NewNode(key, value), - rightNode: node, - }, false, nil - case 1: // setKey > leafKey - return &Node{ - key: key, - subtreeHeight: 1, - size: 2, - nodeKey: nil, - leftNode: node, - rightNode: NewNode(key, value), - }, false, nil - default: - return NewNode(key, value), true, nil + return tree.recursiveSetLeaf(node, key, value) + } + node, err = node.clone(tree) + if err != nil { + return nil, false, err + } + + if bytes.Compare(key, node.key) < 0 { + node.leftNode, updated, err = tree.recursiveSet(node.leftNode, key, value) + if err != nil { + return nil, updated, err } } else { - node, err = node.clone(tree) + node.rightNode, updated, err = tree.recursiveSet(node.rightNode, key, value) if err != nil { - return nil, false, err + return nil, updated, err } + } - if bytes.Compare(key, node.key) < 0 { - node.leftNode, updated, err = tree.recursiveSet(node.leftNode, key, value) - if err != nil { - return nil, updated, err - } - } else { - node.rightNode, updated, err = tree.recursiveSet(node.rightNode, key, value) - if err != nil { - return nil, updated, err - } - } + if updated { + return node, updated, nil + } + err = node.calcHeightAndSize(tree.ImmutableTree) + if err != nil { + return nil, false, err + } + newNode, err := tree.balance(node) + if err != nil { + return nil, false, err + } + return newNode, updated, err +} - if updated { - return node, updated, nil - } - err = node.calcHeightAndSize(tree.ImmutableTree) - if err != nil { - return nil, false, err - } - newNode, err := tree.balance(node) - if err != nil { - return nil, false, err - } - return newNode, updated, err +func (tree *MutableTree) recursiveSetLeaf(node *Node, key []byte, value []byte) ( + newSelf *Node, updated bool, err error, +) { + version := tree.version + 1 + if !tree.skipFastStorageUpgrade { + tree.addUnsavedAddition(key, fastnode.NewNode(key, value, version)) + } + switch bytes.Compare(key, node.key) { + case -1: // setKey < leafKey + return &Node{ + key: node.key, + subtreeHeight: 1, + size: 2, + nodeKey: nil, + leftNode: NewNode(key, value), + rightNode: node, + }, false, nil + case 1: // setKey > leafKey + return &Node{ + key: key, + subtreeHeight: 1, + size: 2, + nodeKey: nil, + leftNode: node, + rightNode: NewNode(key, value), + }, false, nil + default: + return NewNode(key, value), true, nil } } diff --git a/node.go b/node.go index 79511d9b2..e71e415ba 100644 --- a/node.go +++ b/node.go @@ -57,11 +57,15 @@ func GetRootKey(version int64) []byte { // Node represents a node in a Tree. type Node struct { - key []byte - value []byte - hash []byte - nodeKey *NodeKey - leftNodeKey []byte + key []byte + value []byte + hash []byte + nodeKey *NodeKey + // Legacy: LeftNodeHash + // v1: Left node ptr via Version/key + leftNodeKey []byte + // Legacy: RightNodeHash + // v1: Right node ptr via Version/key rightNodeKey []byte size int64 leftNode *Node @@ -522,7 +526,7 @@ func (node *Node) writeHashBytes(w io.Writer, version int64) error { // (e.g. ProofLeafNode.ValueHash) valueHash := sha256.Sum256(node.value) - err = encoding.EncodeBytes(w, valueHash[:]) + err = encoding.Encode32BytesHash(w, valueHash[:]) if err != nil { return fmt.Errorf("writing value, %w", err) } @@ -530,11 +534,11 @@ func (node *Node) writeHashBytes(w io.Writer, version int64) error { if node.leftNode == nil || node.rightNode == nil { return ErrEmptyChild } - err = encoding.EncodeBytes(w, node.leftNode.hash) + err = encoding.Encode32BytesHash(w, node.leftNode.hash) if err != nil { return fmt.Errorf("writing left hash, %w", err) } - err = encoding.EncodeBytes(w, node.rightNode.hash) + err = encoding.Encode32BytesHash(w, node.rightNode.hash) if err != nil { return fmt.Errorf("writing right hash, %w", err) } @@ -600,7 +604,7 @@ func (node *Node) writeBytes(w io.Writer) error { return fmt.Errorf("writing value, %w", err) } } else { - err = encoding.EncodeBytes(w, node.hash) + err = encoding.Encode32BytesHash(w, node.hash) if err != nil { return fmt.Errorf("writing hash, %w", err) } @@ -620,7 +624,7 @@ func (node *Node) writeBytes(w io.Writer) error { return fmt.Errorf("writing mode, %w", err) } if mode&ModeLegacyLeftNode != 0 { // legacy leftNodeKey - err = encoding.EncodeBytes(w, node.leftNodeKey) + err = encoding.Encode32BytesHash(w, node.leftNodeKey) if err != nil { return fmt.Errorf("writing the legacy left node key, %w", err) } @@ -639,7 +643,7 @@ func (node *Node) writeBytes(w io.Writer) error { return ErrRightNodeKeyEmpty } if mode&ModeLegacyRightNode != 0 { // legacy rightNodeKey - err = encoding.EncodeBytes(w, node.rightNodeKey) + err = encoding.Encode32BytesHash(w, node.rightNodeKey) if err != nil { return fmt.Errorf("writing the legacy right node key, %w", err) } diff --git a/node_test.go b/node_test.go index 61cb6e638..12dfcb051 100644 --- a/node_test.go +++ b/node_test.go @@ -61,8 +61,8 @@ func TestNode_encode_decode(t *testing.T) { }, leftNodeKey: childNodeKey.GetKey(), rightNodeKey: childNodeKey.GetKey(), - hash: []byte{0x70, 0x80, 0x90, 0xa0}, - }, "060e036b657904708090a00002020202", false}, + hash: []byte{0x7f, 0x68, 0x90, 0xca, 0x16, 0xde, 0xa6, 0xe8, 0x89, 0x3d, 0x96, 0xf0, 0xa3, 0xd, 0xa, 0x14, 0xe5, 0x55, 0x59, 0xfc, 0x9b, 0x83, 0x4, 0x91, 0xe3, 0xd2, 0x45, 0x1c, 0x81, 0xf6, 0xd1, 0xe}, + }, "060e036b6579207f6890ca16dea6e8893d96f0a30d0a14e55559fc9b830491e3d2451c81f6d10e0002020202", false}, "inner hybrid": {&Node{ subtreeHeight: 3, size: 7, @@ -73,8 +73,8 @@ func TestNode_encode_decode(t *testing.T) { }, leftNodeKey: childNodeKey.GetKey(), rightNodeKey: childNodeHash, - hash: []byte{0x70, 0x80, 0x90, 0xa0}, - }, "060e036b657904708090a0040202207f6890ca16dea6e8893d96f0a30d0a14e55559fc9b830491e3d2451c81f6d10e", false}, + hash: []byte{0x7f, 0x68, 0x90, 0xca, 0x16, 0xde, 0xa6, 0xe8, 0x89, 0x3d, 0x96, 0xf0, 0xa3, 0xd, 0xa, 0x14, 0xe5, 0x55, 0x59, 0xfc, 0x9b, 0x83, 0x4, 0x91, 0xe3, 0xd2, 0x45, 0x1c, 0x81, 0xf6, 0xd1, 0xe}, + }, "060e036b6579207f6890ca16dea6e8893d96f0a30d0a14e55559fc9b830491e3d2451c81f6d10e040202207f6890ca16dea6e8893d96f0a30d0a14e55559fc9b830491e3d2451c81f6d10e", false}, "leaf": {&Node{ subtreeHeight: 0, size: 1, diff --git a/nodedb.go b/nodedb.go index 65f29d214..dc0bc9f4b 100644 --- a/nodedb.go +++ b/nodedb.go @@ -46,10 +46,10 @@ const ( var ( // All new node keys are prefixed with the byte 's'. This ensures no collision is // possible with the legacy nodes, and makes them easier to traverse. They are indexed by the version and the local nonce. - nodeKeyFormat = keyformat.NewKeyFormat('s', int64Size+int32Size) // s + nodeKeyFormat = keyformat.NewFastPrefixFormatter('s', int64Size+int32Size) // s // This is only used for the iteration purpose. - nodeKeyPrefixFormat = keyformat.NewKeyFormat('s', int64Size) // s + nodeKeyPrefixFormat = keyformat.NewFastPrefixFormatter('s', int64Size) // s // Key Format for making reads and iterates go through a data-locality preserving db. // The value at an entry will list what version it was written to. @@ -64,7 +64,7 @@ var ( metadataKeyFormat = keyformat.NewKeyFormat('m', 0) // m // All legacy node keys are prefixed with the byte 'n'. - legacyNodeKeyFormat = keyformat.NewKeyFormat('n', hashSize) // n + legacyNodeKeyFormat = keyformat.NewFastPrefixFormatter('n', hashSize) // n // All legacy orphan keys are prefixed with the byte 'o'. legacyOrphanKeyFormat = keyformat.NewKeyFormat('o', int64Size, int64Size, hashSize) // o @@ -430,14 +430,6 @@ func (ndb *nodeDB) deleteLegacyVersions(legacyLatestVersion int64) error { } } - // Delete the last version for the legacyLastVersion - if err := ndb.traverseOrphans(legacyLatestVersion, legacyLatestVersion+1, func(orphan *Node) error { - checkDeletePause() - return ndb.batch.Delete(ndb.legacyNodeKey(orphan.hash)) - }); err != nil { - return err - } - // Delete orphans for all legacy versions if err := ndb.traversePrefix(legacyOrphanKeyFormat.Key(), func(key, value []byte) error { checkDeletePause() @@ -510,7 +502,7 @@ func (ndb *nodeDB) DeleteVersionsFrom(fromVersion int64) error { } // Delete the nodes for new format - err = ndb.traverseRange(nodeKeyPrefixFormat.Key(fromVersion), nodeKeyPrefixFormat.Key(latest+1), func(k, v []byte) error { + err = ndb.traverseRange(nodeKeyPrefixFormat.KeyInt64(fromVersion), nodeKeyPrefixFormat.KeyInt64(latest+1), func(k, v []byte) error { return ndb.batch.Delete(k) }) @@ -563,6 +555,12 @@ func (ndb *nodeDB) DeleteVersionsTo(toVersion int64) error { // Delete the legacy versions if legacyLatestVersion >= first { + // Delete the last version for the legacyLastVersion + if err := ndb.traverseOrphans(legacyLatestVersion, legacyLatestVersion+1, func(orphan *Node) error { + return ndb.batch.Delete(ndb.legacyNodeKey(orphan.hash)) + }); err != nil { + return err + } // reset the legacy latest version forcibly to avoid multiple calls ndb.resetLegacyLatestVersion(-1) go func() { @@ -710,8 +708,8 @@ func (ndb *nodeDB) getLatestVersion() (int64, error) { } itr, err := ndb.db.ReverseIterator( - nodeKeyPrefixFormat.Key(int64(1)), - nodeKeyPrefixFormat.Key(int64(math.MaxInt64)), + nodeKeyPrefixFormat.KeyInt64(int64(1)), + nodeKeyPrefixFormat.KeyInt64(int64(math.MaxInt64)), ) if err != nil { return 0, err @@ -789,7 +787,7 @@ func (ndb *nodeDB) GetRoot(version int64) ([]byte, error) { switch n { case nodeKeyFormat.Length(): // (prefix, version, 1) nk := GetNodeKey(val[1:]) - val, err = ndb.db.Get(nodeKeyFormat.Key(val[1:])) + val, err = ndb.db.Get(val) if err != nil { return nil, err } @@ -827,6 +825,7 @@ func (ndb *nodeDB) SaveEmptyRoot(version int64) error { func (ndb *nodeDB) SaveRoot(version int64, nk *NodeKey) error { ndb.mtx.Lock() defer ndb.mtx.Unlock() + ndb.logger.Debug("SaveRoot", "version", version, "nodeKey", nk) return ndb.batch.Set(nodeKeyFormat.Key(GetRootKey(version)), nodeKeyFormat.Key(nk.GetKey())) } @@ -1056,7 +1055,7 @@ func (ndb *nodeDB) nodes() ([]*Node, error) { func (ndb *nodeDB) legacyNodes() ([]*Node, error) { nodes := []*Node{} - err := ndb.traversePrefix(legacyNodeKeyFormat.Key(), func(key, value []byte) error { + err := ndb.traversePrefix(legacyNodeKeyFormat.Prefix(), func(key, value []byte) error { node, err := MakeLegacyNode(key[1:], value) if err != nil { return err @@ -1107,7 +1106,7 @@ func (ndb *nodeDB) size() int { func (ndb *nodeDB) traverseNodes(fn func(node *Node) error) error { nodes := []*Node{} - if err := ndb.traversePrefix(nodeKeyFormat.Key(), func(key, value []byte) error { + if err := ndb.traversePrefix(nodeKeyFormat.Prefix(), func(key, value []byte) error { if isRef, _ := isReferenceRoot(value); isRef { return nil } @@ -1190,7 +1189,7 @@ func (ndb *nodeDB) String() (string, error) { index := 0 - err := ndb.traversePrefix(nodeKeyFormat.Key(), func(key, value []byte) error { + err := ndb.traversePrefix(nodeKeyFormat.Prefix(), func(key, value []byte) error { fmt.Fprintf(buf, "%s: %x\n", key, value) return nil })