go-ethereum/nomt/merkle/worker_test.go
weiihann cb3e13d93d nomt/merkle: add Phase 7 parallel workers for trie updates
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>
2026-02-12 18:37:42 +08:00

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
}