mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-05 20:51:16 +00:00
Parallelize the PageWalker trie update across multiple goroutines by partitioning sorted operations by the root page's 64 child subtrees (first 6 bits of each key path). Each worker runs an independent PageWalker constrained to child pages below the root (using parentPage mechanism), producing ChildPageRoots. After all workers complete, a root walker places the child roots using AdvanceAndPlaceNode and concludes with the final trie root. Workers operate on disjoint page subtrees so no synchronization is needed during computation — only sync.WaitGroup for goroutine join. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
258 lines
7.1 KiB
Go
258 lines
7.1 KiB
Go
package merkle
|
|
|
|
import (
|
|
"math/rand"
|
|
"sort"
|
|
"testing"
|
|
|
|
"github.com/ethereum/go-ethereum/nomt/core"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// --- Unit tests for helpers ---
|
|
|
|
func TestPartitionByChildIndex(t *testing.T) {
|
|
// Key 0x00... → child 0, key 0xFC... → child 63.
|
|
kvs := []core.KeyValue{
|
|
{Key: makeKVKey(0x00), Value: makeKVVal(1)},
|
|
{Key: makeKVKey(0x04), Value: makeKVVal(2)}, // 0x04 >> 2 = 1
|
|
{Key: makeKVKey(0xFC), Value: makeKVVal(3)}, // 0xFC >> 2 = 63
|
|
}
|
|
buckets := partitionByChildIndex(kvs)
|
|
|
|
assert.Len(t, buckets[0], 1)
|
|
assert.Len(t, buckets[1], 1)
|
|
assert.Len(t, buckets[63], 1)
|
|
|
|
// All other buckets should be empty.
|
|
nonEmpty := 0
|
|
for _, b := range buckets {
|
|
if len(b) > 0 {
|
|
nonEmpty++
|
|
}
|
|
}
|
|
assert.Equal(t, 3, nonEmpty)
|
|
}
|
|
|
|
func TestChildPosition(t *testing.T) {
|
|
// Child 0, left: 7 bits all false → depth 7.
|
|
pos := childPosition(0, false)
|
|
assert.Equal(t, uint16(7), pos.Depth())
|
|
for i := range 7 {
|
|
assert.False(t, pos.Bit(i), "bit %d should be 0", i)
|
|
}
|
|
|
|
// Child 0, right: 6 false + 1 true → depth 7.
|
|
pos = childPosition(0, true)
|
|
assert.Equal(t, uint16(7), pos.Depth())
|
|
for i := range 6 {
|
|
assert.False(t, pos.Bit(i), "bit %d should be 0", i)
|
|
}
|
|
assert.True(t, pos.Bit(6))
|
|
|
|
// Child 63 (0b111111), left: 6 true + 1 false → depth 7.
|
|
pos = childPosition(63, false)
|
|
assert.Equal(t, uint16(7), pos.Depth())
|
|
for i := range 6 {
|
|
assert.True(t, pos.Bit(i), "bit %d should be 1", i)
|
|
}
|
|
assert.False(t, pos.Bit(6))
|
|
|
|
// Child 63 (0b111111), right: 7 true → depth 7.
|
|
pos = childPosition(63, true)
|
|
assert.Equal(t, uint16(7), pos.Depth())
|
|
for i := range 7 {
|
|
assert.True(t, pos.Bit(i), "bit %d should be 1", i)
|
|
}
|
|
}
|
|
|
|
func TestAssignToWorkers(t *testing.T) {
|
|
// 3 non-empty buckets, 2 workers.
|
|
var buckets [64][]core.KeyValue
|
|
buckets[0] = []core.KeyValue{{Key: makeKVKey(0x00), Value: makeKVVal(1)}}
|
|
buckets[10] = []core.KeyValue{{Key: makeKVKey(0x28), Value: makeKVVal(2)}} // 0x28>>2=10
|
|
buckets[63] = []core.KeyValue{{Key: makeKVKey(0xFC), Value: makeKVVal(3)}}
|
|
|
|
tasks := assignToWorkers(buckets, 2)
|
|
require.Len(t, tasks, 2)
|
|
// 3 items / 2 workers: first gets 2, second gets 1.
|
|
assert.Len(t, tasks[0].children, 2)
|
|
assert.Len(t, tasks[1].children, 1)
|
|
assert.Equal(t, uint8(0), tasks[0].children[0].childIndex)
|
|
assert.Equal(t, uint8(10), tasks[0].children[1].childIndex)
|
|
assert.Equal(t, uint8(63), tasks[1].children[0].childIndex)
|
|
}
|
|
|
|
func TestAssignToWorkersMoreWorkersThanChildren(t *testing.T) {
|
|
var buckets [64][]core.KeyValue
|
|
buckets[5] = []core.KeyValue{{Key: makeKVKey(0x14), Value: makeKVVal(1)}} // 0x14>>2=5
|
|
buckets[6] = []core.KeyValue{{Key: makeKVKey(0x18), Value: makeKVVal(2)}} // 0x18>>2=6
|
|
|
|
tasks := assignToWorkers(buckets, 8)
|
|
// Only 2 non-empty, so cap to 2 workers.
|
|
require.Len(t, tasks, 2)
|
|
assert.Len(t, tasks[0].children, 1)
|
|
assert.Len(t, tasks[1].children, 1)
|
|
}
|
|
|
|
// --- Integration tests ---
|
|
|
|
// permissivePageSet wraps MemoryPageSet to return fresh pages for missing
|
|
// entries (matching bitboxPageSet behavior). This is needed because the
|
|
// parallel workers descend into child pages that may not exist yet.
|
|
type permissivePageSet struct {
|
|
*MemoryPageSet
|
|
}
|
|
|
|
func (ps *permissivePageSet) Get(pageID core.PageID) (*core.RawPage, PageOrigin, bool) {
|
|
page, origin, ok := ps.MemoryPageSet.Get(pageID)
|
|
if !ok {
|
|
fresh := new(core.RawPage)
|
|
return fresh, PageOrigin{Kind: PageOriginFresh}, true
|
|
}
|
|
return page, origin, true
|
|
}
|
|
|
|
func memoryPageSetFactory() PageSet {
|
|
return &permissivePageSet{NewMemoryPageSet(true)}
|
|
}
|
|
|
|
func TestParallelUpdateEmpty(t *testing.T) {
|
|
out := ParallelUpdate(core.Terminator, nil, 4, memoryPageSetFactory)
|
|
assert.Equal(t, core.Terminator, out.Root)
|
|
}
|
|
|
|
func TestParallelUpdateSingleKey(t *testing.T) {
|
|
kv := core.KeyValue{Key: makeKVKey(0x50), Value: makeKVVal(1)}
|
|
kvs := []core.KeyValue{kv}
|
|
|
|
out := ParallelUpdate(core.Terminator, kvs, 4, memoryPageSetFactory)
|
|
expected := expectedRoot(kvs)
|
|
assert.Equal(t, expected, out.Root)
|
|
}
|
|
|
|
func TestParallelUpdateTwoKeysDifferentChildren(t *testing.T) {
|
|
// 0x00 → child 0, 0x80 → child 32.
|
|
kvs := []core.KeyValue{
|
|
{Key: makeKVKey(0x00), Value: makeKVVal(1)},
|
|
{Key: makeKVKey(0x80), Value: makeKVVal(2)},
|
|
}
|
|
|
|
out := ParallelUpdate(core.Terminator, kvs, 4, memoryPageSetFactory)
|
|
expected := expectedRoot(kvs)
|
|
assert.Equal(t, expected, out.Root)
|
|
}
|
|
|
|
func TestParallelUpdateSparseChildren(t *testing.T) {
|
|
// Only children 0 and 63 have ops.
|
|
kvs := []core.KeyValue{
|
|
{Key: makeKVKey(0x00), Value: makeKVVal(1)},
|
|
{Key: makeKVKey(0xFC), Value: makeKVVal(2)},
|
|
}
|
|
|
|
out := ParallelUpdate(core.Terminator, kvs, 4, memoryPageSetFactory)
|
|
expected := expectedRoot(kvs)
|
|
assert.Equal(t, expected, out.Root)
|
|
}
|
|
|
|
func TestParallelUpdateSingleChild(t *testing.T) {
|
|
// All keys land in child 0 (first 6 bits = 000000).
|
|
kvs := []core.KeyValue{
|
|
{Key: makeKVKey(0x00), Value: makeKVVal(1)},
|
|
{Key: makeKVKey(0x01), Value: makeKVVal(2)},
|
|
{Key: makeKVKey(0x02), Value: makeKVVal(3)},
|
|
{Key: makeKVKey(0x03), Value: makeKVVal(4)},
|
|
}
|
|
sort.Slice(kvs, func(i, j int) bool { return kvLess(&kvs[i], &kvs[j]) })
|
|
|
|
out := ParallelUpdate(core.Terminator, kvs, 4, memoryPageSetFactory)
|
|
expected := expectedRoot(kvs)
|
|
assert.Equal(t, expected, out.Root)
|
|
}
|
|
|
|
func TestParallelUpdateFallbackSmallBatch(t *testing.T) {
|
|
// Less than 64 ops → single-threaded fallback.
|
|
kvs := randomKVs(10, 42)
|
|
out := ParallelUpdate(core.Terminator, kvs, 8, memoryPageSetFactory)
|
|
expected := expectedRoot(kvs)
|
|
assert.Equal(t, expected, out.Root)
|
|
}
|
|
|
|
func TestParallelUpdateDeterministic(t *testing.T) {
|
|
kvs := randomKVs(200, 99)
|
|
|
|
r1 := ParallelUpdate(core.Terminator, kvs, 4, memoryPageSetFactory).Root
|
|
r2 := ParallelUpdate(core.Terminator, kvs, 4, memoryPageSetFactory).Root
|
|
assert.Equal(t, r1, r2, "same inputs should produce same root")
|
|
}
|
|
|
|
func TestParallelUpdateMatchesSingleThreaded(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
numKVs int
|
|
workers int
|
|
}{
|
|
{"1kv_2w", 1, 2},
|
|
{"10kv_2w", 10, 2},
|
|
{"100kv_2w", 100, 2},
|
|
{"100kv_4w", 100, 4},
|
|
{"100kv_8w", 100, 8},
|
|
{"500kv_4w", 500, 4},
|
|
{"1000kv_8w", 1000, 8},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
kvs := randomKVs(tc.numKVs, 12345)
|
|
|
|
single := singleThreadedUpdate(
|
|
core.Terminator, kvs, NewMemoryPageSet(true),
|
|
)
|
|
parallel := ParallelUpdate(
|
|
core.Terminator, kvs, tc.workers, memoryPageSetFactory,
|
|
)
|
|
|
|
assert.Equal(t, single.Root, parallel.Root,
|
|
"parallel root should match single-threaded root")
|
|
})
|
|
}
|
|
}
|
|
|
|
// --- helpers ---
|
|
|
|
func randomKVs(n int, seed int64) []core.KeyValue {
|
|
rng := rand.New(rand.NewSource(seed))
|
|
kvs := make([]core.KeyValue, n)
|
|
seen := make(map[core.KeyPath]bool, n)
|
|
|
|
for i := range n {
|
|
for {
|
|
var kp core.KeyPath
|
|
rng.Read(kp[:])
|
|
if seen[kp] {
|
|
continue
|
|
}
|
|
seen[kp] = true
|
|
var vh core.ValueHash
|
|
rng.Read(vh[:])
|
|
kvs[i] = core.KeyValue{Key: kp, Value: vh}
|
|
break
|
|
}
|
|
}
|
|
|
|
sort.Slice(kvs, func(i, j int) bool { return kvLess(&kvs[i], &kvs[j]) })
|
|
return kvs
|
|
}
|
|
|
|
func kvLess(a, b *core.KeyValue) bool {
|
|
for i := range a.Key {
|
|
if a.Key[i] < b.Key[i] {
|
|
return true
|
|
}
|
|
if a.Key[i] > b.Key[i] {
|
|
return false
|
|
}
|
|
}
|
|
return false
|
|
}
|