mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-28 07:36:44 +00:00
Added tests for executor and some improvements: 1. Add a dependency map during execution. This will prevent aborted tasks from being sent for execution immedaitely after failure. 2. Change the key of MVHashMap from string to a byte array. This will reduce time to convert byte slices to strings. 3. Use sync.Map to reduce the time spent in global mutex. 4. Skip applying intermediate states. 5. Estimate dependency when an execution fails without dependency information. 6. Divide execution task queue into two separate queues. One for relatively certain transactions, and the other for speculative future transactions. 7. Setting dependencies of Txs coming from the same sender before starting parallel execution. 8. Process results in their semantic order (transaction index) instead of the order when they arrive. Replace result channel with a priority queue.
470 lines
13 KiB
Go
470 lines
13 KiB
Go
package blockstm
|
|
|
|
import (
|
|
"fmt"
|
|
"math/big"
|
|
"math/rand"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/log"
|
|
)
|
|
|
|
type OpType int
|
|
|
|
const readType = 0
|
|
const writeType = 1
|
|
const otherType = 2
|
|
|
|
type Op struct {
|
|
key Key
|
|
duration time.Duration
|
|
opType OpType
|
|
val int
|
|
}
|
|
|
|
type testExecTask struct {
|
|
txIdx int
|
|
ops []Op
|
|
readMap map[Key]ReadDescriptor
|
|
writeMap map[Key]WriteDescriptor
|
|
sender common.Address
|
|
nonce int
|
|
}
|
|
|
|
type PathGenerator func(addr common.Address, j int, total int) Key
|
|
|
|
type TaskRunner func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration)
|
|
|
|
type Timer func(txIdx int, opIdx int) time.Duration
|
|
|
|
type Sender func(int) common.Address
|
|
|
|
func NewTestExecTask(txIdx int, ops []Op, sender common.Address, nonce int) *testExecTask {
|
|
return &testExecTask{
|
|
txIdx: txIdx,
|
|
ops: ops,
|
|
readMap: make(map[Key]ReadDescriptor),
|
|
writeMap: make(map[Key]WriteDescriptor),
|
|
sender: sender,
|
|
nonce: nonce,
|
|
}
|
|
}
|
|
|
|
func sleep(i time.Duration) {
|
|
start := time.Now()
|
|
for time.Since(start) < i {
|
|
}
|
|
}
|
|
|
|
func (t *testExecTask) Execute(mvh *MVHashMap, incarnation int) error {
|
|
// Sleep for 50 microsecond to simulate setup time
|
|
sleep(time.Microsecond * 50)
|
|
|
|
version := Version{TxnIndex: t.txIdx, Incarnation: incarnation}
|
|
|
|
t.readMap = make(map[Key]ReadDescriptor)
|
|
t.writeMap = make(map[Key]WriteDescriptor)
|
|
|
|
deps := -1
|
|
|
|
for i, op := range t.ops {
|
|
k := op.key
|
|
|
|
switch op.opType {
|
|
case readType:
|
|
if _, ok := t.writeMap[k]; ok {
|
|
sleep(op.duration)
|
|
continue
|
|
}
|
|
|
|
result := mvh.Read(k, t.txIdx)
|
|
|
|
val := result.Value()
|
|
|
|
if i == 0 && val != nil && (val.(int) != t.nonce) {
|
|
return ErrExecAbortError{}
|
|
}
|
|
|
|
if result.Status() == MVReadResultDependency {
|
|
if result.depIdx > deps {
|
|
deps = result.depIdx
|
|
}
|
|
}
|
|
|
|
var readKind int
|
|
|
|
if result.Status() == MVReadResultDone {
|
|
readKind = ReadKindMap
|
|
} else if result.Status() == MVReadResultNone {
|
|
readKind = ReadKindStorage
|
|
}
|
|
|
|
sleep(op.duration)
|
|
|
|
t.readMap[k] = ReadDescriptor{k, readKind, Version{TxnIndex: result.depIdx, Incarnation: result.incarnation}}
|
|
case writeType:
|
|
t.writeMap[k] = WriteDescriptor{k, version, op.val}
|
|
case otherType:
|
|
sleep(op.duration)
|
|
default:
|
|
panic(fmt.Sprintf("Unknown op type: %d", op.opType))
|
|
}
|
|
}
|
|
|
|
if deps != -1 {
|
|
return ErrExecAbortError{deps}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (t *testExecTask) MVWriteList() []WriteDescriptor {
|
|
return t.MVFullWriteList()
|
|
}
|
|
|
|
func (t *testExecTask) MVFullWriteList() []WriteDescriptor {
|
|
writes := make([]WriteDescriptor, 0, len(t.writeMap))
|
|
|
|
for _, v := range t.writeMap {
|
|
writes = append(writes, v)
|
|
}
|
|
|
|
return writes
|
|
}
|
|
|
|
func (t *testExecTask) MVReadList() []ReadDescriptor {
|
|
reads := make([]ReadDescriptor, 0, len(t.readMap))
|
|
|
|
for _, v := range t.readMap {
|
|
reads = append(reads, v)
|
|
}
|
|
|
|
return reads
|
|
}
|
|
|
|
func (t *testExecTask) Settle() {}
|
|
|
|
func (t *testExecTask) Sender() common.Address {
|
|
return t.sender
|
|
}
|
|
|
|
func randTimeGenerator(min time.Duration, max time.Duration) func(txIdx int, opIdx int) time.Duration {
|
|
return func(txIdx int, opIdx int) time.Duration {
|
|
return time.Duration(rand.Int63n(int64(max-min))) + min
|
|
}
|
|
}
|
|
|
|
func longTailTimeGenerator(min time.Duration, max time.Duration, i int, j int) func(txIdx int, opIdx int) time.Duration {
|
|
return func(txIdx int, opIdx int) time.Duration {
|
|
if txIdx%i == 0 && opIdx == j {
|
|
return max * 100
|
|
} else {
|
|
return time.Duration(rand.Int63n(int64(max-min))) + min
|
|
}
|
|
}
|
|
}
|
|
|
|
var randomPathGenerator = func(sender common.Address, j int, total int) Key {
|
|
return NewStateKey(sender, common.BigToHash((big.NewInt(int64(total)))))
|
|
}
|
|
|
|
var dexPathGenerator = func(sender common.Address, j int, total int) Key {
|
|
if j == total-1 || j == 2 {
|
|
return NewSubpathKey(common.BigToAddress(big.NewInt(int64(0))), 1)
|
|
} else {
|
|
return NewSubpathKey(common.BigToAddress(big.NewInt(int64(j))), 1)
|
|
}
|
|
}
|
|
|
|
var readTime = randTimeGenerator(4*time.Microsecond, 12*time.Microsecond)
|
|
var writeTime = randTimeGenerator(2*time.Microsecond, 6*time.Microsecond)
|
|
var nonIOTime = randTimeGenerator(1*time.Microsecond, 2*time.Microsecond)
|
|
|
|
func taskFactory(numTask int, sender Sender, readsPerT int, writesPerT int, nonIOPerT int, pathGenerator PathGenerator, readTime Timer, writeTime Timer, nonIOTime Timer) ([]ExecTask, time.Duration) {
|
|
exec := make([]ExecTask, 0, numTask)
|
|
|
|
var serialDuration time.Duration
|
|
|
|
senderNonces := make(map[common.Address]int)
|
|
|
|
for i := 0; i < numTask; i++ {
|
|
s := sender(i)
|
|
|
|
// Set first two ops to always read and write nonce
|
|
ops := make([]Op, 0, readsPerT+writesPerT+nonIOPerT)
|
|
|
|
ops = append(ops, Op{opType: readType, key: NewSubpathKey(s, 2), duration: readTime(i, 0), val: senderNonces[s]})
|
|
|
|
senderNonces[s]++
|
|
|
|
ops = append(ops, Op{opType: writeType, key: NewSubpathKey(s, 2), duration: writeTime(i, 1), val: senderNonces[s]})
|
|
|
|
for j := 0; j < readsPerT-1; j++ {
|
|
ops = append(ops, Op{opType: readType})
|
|
}
|
|
|
|
for j := 0; j < nonIOPerT; j++ {
|
|
ops = append(ops, Op{opType: otherType})
|
|
}
|
|
|
|
for j := 0; j < writesPerT-1; j++ {
|
|
ops = append(ops, Op{opType: writeType})
|
|
}
|
|
|
|
// shuffle ops except for the first three (read nonce, write nonce, another read) ops and last write op.
|
|
// This enables random path generator to generate deterministic paths for these "special" ops.
|
|
for j := 3; j < len(ops)-1; j++ {
|
|
k := rand.Intn(len(ops)-j-1) + j
|
|
ops[j], ops[k] = ops[k], ops[j]
|
|
}
|
|
|
|
// Generate time and key path for each op except first two that are always read and write nonce
|
|
for j := 2; j < len(ops); j++ {
|
|
if ops[j].opType == readType {
|
|
ops[j].key = pathGenerator(s, j, len(ops))
|
|
ops[j].duration = readTime(i, j)
|
|
} else if ops[j].opType == writeType {
|
|
ops[j].key = pathGenerator(s, j, len(ops))
|
|
ops[j].duration = writeTime(i, j)
|
|
} else {
|
|
ops[j].duration = nonIOTime(i, j)
|
|
}
|
|
|
|
serialDuration += ops[j].duration
|
|
}
|
|
|
|
if ops[len(ops)-1].opType != writeType {
|
|
panic("Last op must be a write")
|
|
}
|
|
|
|
t := NewTestExecTask(i, ops, s, senderNonces[s]-1)
|
|
exec = append(exec, t)
|
|
}
|
|
|
|
return exec, serialDuration
|
|
}
|
|
|
|
func testExecutorComb(t *testing.T, totalTxs []int, numReads []int, numWrites []int, numNonIO []int, taskRunner TaskRunner) {
|
|
t.Helper()
|
|
log.Root().SetHandler(log.LvlFilterHandler(log.LvlDebug, log.StreamHandler(os.Stderr, log.TerminalFormat(false))))
|
|
|
|
improved := 0
|
|
total := 0
|
|
|
|
totalExecDuration := time.Duration(0)
|
|
totalSerialDuration := time.Duration(0)
|
|
|
|
for _, numTx := range totalTxs {
|
|
for _, numRead := range numReads {
|
|
for _, numWrite := range numWrites {
|
|
for _, numNonIO := range numNonIO {
|
|
log.Info("Executing block", "numTx", numTx, "numRead", numRead, "numWrite", numWrite, "numNonIO", numNonIO)
|
|
execDuration, expectedSerialDuration := taskRunner(numTx, numRead, numWrite, numNonIO)
|
|
|
|
if execDuration < expectedSerialDuration {
|
|
improved++
|
|
}
|
|
total++
|
|
|
|
performance := "✅"
|
|
|
|
if execDuration >= expectedSerialDuration {
|
|
performance = "❌"
|
|
}
|
|
|
|
fmt.Printf("exec duration %v, serial duration %v, time reduced %v %.2f%%, %v \n", execDuration, expectedSerialDuration, expectedSerialDuration-execDuration, float64(expectedSerialDuration-execDuration)/float64(expectedSerialDuration)*100, performance)
|
|
|
|
totalExecDuration += execDuration
|
|
totalSerialDuration += expectedSerialDuration
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fmt.Println("Improved: ", improved, "Total: ", total, "success rate: ", float64(improved)/float64(total)*100)
|
|
fmt.Printf("Total exec duration: %v, total serial duration: %v, time reduced: %v, time reduced percent: %.2f%%\n", totalExecDuration, totalSerialDuration, totalSerialDuration-totalExecDuration, float64(totalSerialDuration-totalExecDuration)/float64(totalSerialDuration)*100)
|
|
}
|
|
|
|
func runParallel(t *testing.T, tasks []ExecTask, validation func(TxnInputOutput) bool) time.Duration {
|
|
t.Helper()
|
|
|
|
start := time.Now()
|
|
results, _ := ExecuteParallel(tasks, false)
|
|
|
|
txio := results.TxIO
|
|
|
|
// Need to apply the final write set to storage
|
|
|
|
finalWriteSet := make(map[Key]time.Duration)
|
|
|
|
for _, task := range tasks {
|
|
task := task.(*testExecTask)
|
|
for _, op := range task.ops {
|
|
if op.opType == writeType {
|
|
finalWriteSet[op.key] = op.duration
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, v := range finalWriteSet {
|
|
sleep(v)
|
|
}
|
|
|
|
duration := time.Since(start)
|
|
|
|
if validation != nil {
|
|
assert.True(t, validation(*txio))
|
|
}
|
|
|
|
return duration
|
|
}
|
|
|
|
func TestLessConflicts(t *testing.T) {
|
|
t.Parallel()
|
|
rand.Seed(0)
|
|
|
|
totalTxs := []int{10, 50, 100, 200, 300}
|
|
numReads := []int{20, 100, 200}
|
|
numWrites := []int{20, 100, 200}
|
|
numNonIO := []int{100, 500}
|
|
|
|
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) {
|
|
sender := func(i int) common.Address {
|
|
randomness := rand.Intn(10) + 10
|
|
return common.BigToAddress(big.NewInt(int64(i % randomness)))
|
|
}
|
|
tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime)
|
|
|
|
return runParallel(t, tasks, nil), serialDuration
|
|
}
|
|
|
|
testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
|
|
}
|
|
|
|
func TestAlternatingTx(t *testing.T) {
|
|
t.Parallel()
|
|
rand.Seed(0)
|
|
|
|
totalTxs := []int{200}
|
|
numReads := []int{20}
|
|
numWrites := []int{20}
|
|
numNonIO := []int{100}
|
|
|
|
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) {
|
|
sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(i % 2))) }
|
|
tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime)
|
|
|
|
return runParallel(t, tasks, nil), serialDuration
|
|
}
|
|
|
|
testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
|
|
}
|
|
|
|
func TestMoreConflicts(t *testing.T) {
|
|
t.Parallel()
|
|
rand.Seed(0)
|
|
|
|
totalTxs := []int{10, 50, 100, 200, 300}
|
|
numReads := []int{20, 100, 200}
|
|
numWrites := []int{20, 100, 200}
|
|
numNonIO := []int{100, 500}
|
|
|
|
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) {
|
|
sender := func(i int) common.Address {
|
|
randomness := rand.Intn(10) + 10
|
|
return common.BigToAddress(big.NewInt(int64(i / randomness)))
|
|
}
|
|
tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime)
|
|
|
|
return runParallel(t, tasks, nil), serialDuration
|
|
}
|
|
|
|
testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
|
|
}
|
|
|
|
func TestRandomTx(t *testing.T) {
|
|
t.Parallel()
|
|
rand.Seed(0)
|
|
|
|
totalTxs := []int{10, 50, 100, 200, 300}
|
|
numReads := []int{20, 100, 200}
|
|
numWrites := []int{20, 100, 200}
|
|
numNonIO := []int{100, 500}
|
|
|
|
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) {
|
|
// Randomly assign this tx to one of 10 senders
|
|
sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(rand.Intn(10)))) }
|
|
tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime)
|
|
|
|
return runParallel(t, tasks, nil), serialDuration
|
|
}
|
|
|
|
testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
|
|
}
|
|
|
|
func TestTxWithLongTailRead(t *testing.T) {
|
|
t.Parallel()
|
|
rand.Seed(0)
|
|
|
|
totalTxs := []int{10, 50, 100, 200, 300}
|
|
numReads := []int{20, 100, 200}
|
|
numWrites := []int{20, 100, 200}
|
|
numNonIO := []int{100, 500}
|
|
|
|
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) {
|
|
sender := func(i int) common.Address {
|
|
randomness := rand.Intn(10) + 10
|
|
return common.BigToAddress(big.NewInt(int64(i / randomness)))
|
|
}
|
|
|
|
longTailReadTimer := longTailTimeGenerator(4*time.Microsecond, 12*time.Microsecond, 7, 10)
|
|
|
|
tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, longTailReadTimer, writeTime, nonIOTime)
|
|
|
|
return runParallel(t, tasks, nil), serialDuration
|
|
}
|
|
|
|
testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
|
|
}
|
|
|
|
func TestDexScenario(t *testing.T) {
|
|
t.Parallel()
|
|
rand.Seed(0)
|
|
|
|
totalTxs := []int{10, 50, 100, 200, 300}
|
|
numReads := []int{20, 100, 200}
|
|
numWrites := []int{20, 100, 200}
|
|
numNonIO := []int{100, 500}
|
|
|
|
validation := func(txio TxnInputOutput) bool {
|
|
for i, inputs := range txio.inputs {
|
|
foundDep := false
|
|
|
|
for _, input := range inputs {
|
|
if input.V.TxnIndex == i-1 {
|
|
foundDep = true
|
|
}
|
|
}
|
|
|
|
if !foundDep {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) {
|
|
sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(i))) }
|
|
tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, dexPathGenerator, readTime, writeTime, nonIOTime)
|
|
|
|
return runParallel(t, tasks, validation), serialDuration
|
|
}
|
|
|
|
testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
|
|
}
|