-
Notifications
You must be signed in to change notification settings - Fork 68
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Overlay tree migration explorations #343
Conversation
4ec1412
to
0190cce
Compare
// BatchNewLeafNode creates a new leaf node from the given data. It optimizes LeafNode creation | ||
// by batching expensive cryptography operations. It returns the LeafNodes sorted by stem. | ||
func BatchNewLeafNode(nodesValues []BatchNewLeafNodeData) []LeafNode { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm using here something I did for the full tree conversion, but added parallelization at this layer. This wasn't needed in the tree conversion since, in that case we parallelized "at the client level" by working in subtrees.
But the idea is the same/similar.
panic("stems are equal") | ||
} | ||
|
||
func (n *InternalNode) InsertMigratedLeaves(leaves []LeafNode, resolver NodeResolverFn) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a sketch/first-version of a method that receives prepared LeafNode
of base tree key/values, and attemps to "merge them" in a living VKT. Isn't final and can have rough edges.
case Empty: | ||
parent.cowChild(ln.stem[parent.depth]) | ||
parent.children[ln.stem[parent.depth]] = &ln | ||
ln.setDepth(parent.depth + 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Easy case, insert leaf and call it a day.
e92fc0c
to
eea2733
Compare
parent.cowChild(ln.stem[parent.depth]) | ||
parent.children[ln.stem[parent.depth]] = &ln | ||
ln.setDepth(parent.depth + 1) | ||
case *LeafNode: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, this case is more interesting. We have two subcases.
if bytes.Equal(node.stem, ln.stem) { | ||
// In `ln` we have migrated key/values which should be copied to the leaf | ||
// only if there isn't a value there. If there's a value, we skip it since | ||
// our migrated value is stale. | ||
nonPresentValues := make([][]byte, NodeWidth) | ||
for i := range ln.values { | ||
if node.values[i] == nil { | ||
nonPresentValues[i] = ln.values[i] | ||
} | ||
} | ||
|
||
node.updateMultipleLeaves(nonPresentValues) | ||
continue | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we already have the leaf for the stem, we have to do a sort of "merging" but not blindly.
Only copy values if the current value is nil
, which means our (migrated) value isn't stale.
So the idea in L121-L126 is to filter out values that are stale. Then we exploit the method updateMultipleLeaves
to update the existing Leaf.
Note that the original LeafNode
that we prepared with the migrated values wasted some effort in computing C1 and C2 which we aren't using here. That's fine... the probability of that work being wasted effort is the probability of having this case, which is very low probability. As in, the migrated value coincidentally matched a LeafNode that was touched in the block execution; so it's fine.
[Note for the future: we need some particular test to cover this case. Generating random key/values won't probably cover this. We'll have time for that. Early stages...]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Note for the future: we need some particular test to cover this case. Generating random key/values won't probably cover this. We'll have time for that. Early stages...]
you can simply pick a few random keys from leaves
and insert them in the tree with a different value, to simulate this case, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep.
conversion.go
Outdated
ln := leaves[i] | ||
parent := n | ||
|
||
// Look for the appropiate parent for the leaf node. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wal the tree looking for the parent for the LeafNode to be inserted. While we walk, we have to resolve potential HashedNodes
. Note that we carefully (L109) mark our walk as cow-ed since we know we'll insert the leaf "down the road".
nextParent, ok := parent.children[ln.stem[parent.depth]].(*InternalNode) | ||
if !ok { | ||
break | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we find a LeafNode or Empty, we're done; we have the parent.
default: | ||
return fmt.Errorf("unexpected node type %T", node) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just be sure nothing else can be found, which would be a bug.
// We do a sanity check to make sure that the fork point is not before the current depth. | ||
if byte(idx) <= parent.depth { | ||
return fmt.Errorf("unexpected fork point %d for nodes %x and %x", idx, node.stem, ln.stem) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's be paranoid and check if this invariant holds; if it doesn't hold, it's a bug.
// Create the missing internal nodes. | ||
for i := parent.depth + 1; i <= byte(idx); i++ { | ||
nextParent := newInternalNode(parent.depth + 1).(*InternalNode) | ||
parent.cowChild(ln.stem[parent.depth]) | ||
parent.children[ln.stem[parent.depth]] = nextParent | ||
parent = nextParent | ||
} | ||
// Add old and new leaf node to the latest created parent. | ||
parent.cowChild(node.stem[parent.depth]) | ||
parent.children[node.stem[parent.depth]] = node | ||
node.setDepth(parent.depth + 1) | ||
parent.cowChild(ln.stem[parent.depth]) | ||
parent.children[ln.stem[parent.depth]] = &ln | ||
ln.setDepth(parent.depth + 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Create the needed internal points depending on the "fork section" of the steam, and connect the final parent with the existing LeafNode and the "to be inserted" LeafNode.
Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com>
if bytes.Equal(node.stem, ln.stem) { | ||
// In `ln` we have migrated key/values which should be copied to the leaf | ||
// only if there isn't a value there. If there's a value, we skip it since | ||
// our migrated value is stale. | ||
nonPresentValues := make([][]byte, NodeWidth) | ||
for i := range ln.values { | ||
if node.values[i] == nil { | ||
nonPresentValues[i] = ln.values[i] | ||
} | ||
} | ||
|
||
node.updateMultipleLeaves(nonPresentValues) | ||
continue | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Note for the future: we need some particular test to cover this case. Generating random key/values won't probably cover this. We'll have time for that. Early stages...]
you can simply pick a few random keys from leaves
and insert them in the tree with a different value, to simulate this case, no?
parent := n | ||
|
||
// Look for the appropiate parent for the leaf node. | ||
for { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer a recursive version of this in the end, but as long as it works fine there's no issue for now.
Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com>
Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com>
Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com>
Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com>
Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com>
Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com>
Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com>
Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com>
Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com>
Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com>
Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com>
Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com>
02bf367
to
8102b90
Compare
if i >= NodeWidth-1 { | ||
if i >= NodeWidth { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was a bug.
Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com>
8102b90
to
fe91282
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
TL;DR: This PR contains a test/benchmark that simulates a touched tree with X key/values, and a later stage of migration Y key/values from a base tree. It includes some optimized methods for the migrated key/values.
The main intention of this PR is to show the exploration of:
Insert(.., ..)
API for the migrated key/values, which is somewhat naive compared to an optimized strategy, comparing speedups.This PR isn't meant to be merged necessarily, so I'll keep it as a draft.
Not expecting a formal review, if you want to skim the code I'd recommend looking at:
tree_test.go
to seeTestBatchMigratedKeyValues
test; you'll get 90% of the gist of the story there.conversion.go
see the new methodInsertMigratedLeaves
.This code isn't final or similar, so expect to see rough edges. I added PR comments to guide a bit on the important parts.
Overall explanation and details.
I created a test/benchmark that compares some scenarios between using an “unbatched” insertion for the migrated key/vals (i.e:
Insert(key, value)
in a nutshell), and a “batched” version which uses some work I did for the full-tree migration version with some twists and extra stuff (nothing wild, just reasonable ideas).Sharing the output of this new test/benchmark. ’ll add some notes below on how to read this:
So, look at:
Then X, Y, A, B, and C mean:
To understand what these “milliseconds it takes to insert the migrated/key values in this scenario”, the high-level run of the scenario is:
[This is run both with naive (unbatched) API, and the optimized (batched) API; so we get A and B times, respectively]
The time between 2 and 6 is what I report as that time. It’s a bit “unfair” in that committing and serializing the tree also involves doing the work for the X key/values, which isn’t only work related to the Y key/values. But we can’t separate them since commit and serialize exploits batching in everything that exists (which is actually a good thing). So the actual speedup of the optimized implementation is probably better. In any case, it is good to have a number.
What we can conclude from this:
What we can’t conclude from this:
HashedNode
in the path to insertion, which implies some extra disk lookups to load those nodes. Nothing weird, and quite normal, but have it in mind. Probably for the first 2-3 layers we don’t need new disk accesses, so it shouldn’t be that bad.What we could do about the “can’t conclude” points is actually “embedding” this same scenario test in our replay benchmark with real data, which would be very interesting since:
HashedNodes
for real, involving the mentioned disk lookups to load parts of the tree that aren’t in memory.