go-ethereum/core/blockstm/executor.go
Jerry c36ad88aec Block-stm optimization
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.
2022-09-28 16:12:20 -07:00

539 lines
13 KiB
Go

package blockstm
import (
"container/heap"
"fmt"
"sort"
"sync"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
)
type ExecResult struct {
err error
ver Version
txIn TxnInput
txOut TxnOutput
txAllOut TxnOutput
}
type ExecTask interface {
Execute(mvh *MVHashMap, incarnation int) error
MVReadList() []ReadDescriptor
MVWriteList() []WriteDescriptor
MVFullWriteList() []WriteDescriptor
Sender() common.Address
Settle()
}
type ExecVersionView struct {
ver Version
et ExecTask
mvh *MVHashMap
sender common.Address
}
func (ev *ExecVersionView) Execute() (er ExecResult) {
er.ver = ev.ver
if er.err = ev.et.Execute(ev.mvh, ev.ver.Incarnation); er.err != nil {
return
}
er.txIn = ev.et.MVReadList()
er.txOut = ev.et.MVWriteList()
er.txAllOut = ev.et.MVFullWriteList()
return
}
type ErrExecAbortError struct {
Dependency int
}
func (e ErrExecAbortError) Error() string {
if e.Dependency >= 0 {
return fmt.Sprintf("Execution aborted due to dependency %d", e.Dependency)
} else {
return "Execution aborted"
}
}
type IntHeap []int
func (h IntHeap) Len() int { return len(h) }
func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *IntHeap) Push(x any) {
// Push and Pop use pointer receivers because they modify the slice's length,
// not just its contents.
*h = append(*h, x.(int))
}
func (h *IntHeap) Pop() any {
old := *h
n := len(old)
x := old[n-1]
*h = old[0 : n-1]
return x
}
// A thread safe priority queue
type SafePriorityQueue struct {
m sync.Mutex
queue *IntHeap
data map[int]interface{}
}
func NewSafePriorityQueue(capacity int) *SafePriorityQueue {
q := make(IntHeap, 0, capacity)
return &SafePriorityQueue{
m: sync.Mutex{},
queue: &q,
data: make(map[int]interface{}, capacity),
}
}
func (pq *SafePriorityQueue) Push(v int, d interface{}) {
pq.m.Lock()
heap.Push(pq.queue, v)
pq.data[v] = d
pq.m.Unlock()
}
func (pq *SafePriorityQueue) Pop() interface{} {
pq.m.Lock()
defer pq.m.Unlock()
v := heap.Pop(pq.queue).(int)
return pq.data[v]
}
func (pq *SafePriorityQueue) Len() int {
return pq.queue.Len()
}
type ParallelExecutionResult struct {
TxIO *TxnInputOutput
Stats *[][]uint64
Deps *DAG
}
const numGoProcs = 2
const numSpeculativeProcs = 16
// Max number of pre-validation to run per loop
const preValidateLimit = 5
// Max number of times a transaction (t) can be executed before its dependency is resolved to its previous tx (t-1)
const maxIncarnation = 2
// nolint: gocognit
// A stateless executor that executes transactions in parallel
func ExecuteParallel(tasks []ExecTask, profile bool) (ParallelExecutionResult, error) {
if len(tasks) == 0 {
return ParallelExecutionResult{MakeTxnInputOutput(len(tasks)), nil, nil}, nil
}
// Stores the execution statistics for each task
stats := make([][]uint64, 0, len(tasks))
statsMutex := sync.Mutex{}
// Channel for tasks that should be prioritized
chTasks := make(chan ExecVersionView, len(tasks))
// Channel for speculative tasks
chSpeculativeTasks := make(chan struct{}, len(tasks))
// A priority queue that stores speculative tasks
specTaskQueue := NewSafePriorityQueue(len(tasks))
// Channel to signal that the result of a transaction could be written to storage
chSettle := make(chan int, len(tasks))
// Channel to signal that a transaction has finished executing
chResults := make(chan struct{}, len(tasks))
// A priority queue that stores the transaction index of results, so we can validate the results in order
resultQueue := NewSafePriorityQueue(len(tasks))
// A wait group to wait for all settling tasks to finish
var settleWg sync.WaitGroup
// An integer that tracks the index of last settled transaction
lastSettled := -1
// For a task that runs only after all of its preceding tasks have finished and passed validation,
// its result will be absolutely valid and therefore its validation could be skipped.
// This map stores the boolean value indicating whether a task satisfy this condition ( absolutely valid).
skipCheck := make(map[int]bool)
for i := 0; i < len(tasks); i++ {
skipCheck[i] = false
}
// Execution tasks stores the state of each execution task
execTasks := makeStatusManager(len(tasks))
// Validate tasks stores the state of each validation task
validateTasks := makeStatusManager(0)
// Stats for debugging purposes
var cntExec, cntSuccess, cntAbort, cntTotalValidations, cntValidationFail int
diagExecSuccess := make([]int, len(tasks))
diagExecAbort := make([]int, len(tasks))
// Initialize MVHashMap
mvh := MakeMVHashMap()
// Stores the inputs and outputs of the last incardanotion of all transactions
lastTxIO := MakeTxnInputOutput(len(tasks))
// Tracks the incarnation number of each transaction
txIncarnations := make([]int, len(tasks))
// A map that stores the estimated dependency of a transaction if it is aborted without any known dependency
estimateDeps := make(map[int][]int, len(tasks))
for i := 0; i < len(tasks); i++ {
estimateDeps[i] = make([]int, 0)
}
// A map that records whether a transaction result has been speculatively validated
preValidated := make(map[int]bool, len(tasks))
begin := time.Now()
workerWg := sync.WaitGroup{}
workerWg.Add(numSpeculativeProcs + numGoProcs)
// Launch workers that execute transactions
for i := 0; i < numSpeculativeProcs+numGoProcs; i++ {
go func(procNum int) {
defer workerWg.Done()
doWork := func(task ExecVersionView) {
start := time.Duration(0)
if profile {
start = time.Since(begin)
}
res := task.Execute()
if res.err == nil {
mvh.FlushMVWriteSet(res.txAllOut)
}
resultQueue.Push(res.ver.TxnIndex, res)
chResults <- struct{}{}
if profile {
end := time.Since(begin)
stat := []uint64{uint64(res.ver.TxnIndex), uint64(res.ver.Incarnation), uint64(start), uint64(end), uint64(procNum)}
statsMutex.Lock()
stats = append(stats, stat)
statsMutex.Unlock()
}
}
if procNum < numSpeculativeProcs {
for range chSpeculativeTasks {
doWork(specTaskQueue.Pop().(ExecVersionView))
}
} else {
for task := range chTasks {
doWork(task)
}
}
}(i)
}
// Launch a worker that settles valid transactions
settleWg.Add(len(tasks))
go func() {
for t := range chSettle {
tasks[t].Settle()
settleWg.Done()
}
}()
// bootstrap first execution
tx := execTasks.takeNextPending()
if tx != -1 {
cntExec++
chTasks <- ExecVersionView{ver: Version{tx, 0}, et: tasks[tx], mvh: mvh, sender: tasks[tx].Sender()}
}
// Before starting execution, going through each task to check their explicit dependencies (whether they are coming from the same account)
prevSenderTx := make(map[common.Address]int)
for i, t := range tasks {
if tx, ok := prevSenderTx[t.Sender()]; ok {
execTasks.addDependencies(tx, i)
execTasks.clearPending(i)
}
prevSenderTx[t.Sender()] = i
}
var res ExecResult
var err error
// Start main validation loop
// nolint:nestif
for range chResults {
res = resultQueue.Pop().(ExecResult)
tx := res.ver.TxnIndex
if res.err == nil {
lastTxIO.recordRead(tx, res.txIn)
if res.ver.Incarnation == 0 {
lastTxIO.recordWrite(tx, res.txOut)
lastTxIO.recordAllWrite(tx, res.txAllOut)
} else {
if res.txAllOut.hasNewWrite(lastTxIO.AllWriteSet(tx)) {
validateTasks.pushPendingSet(execTasks.getRevalidationRange(tx + 1))
}
prevWrite := lastTxIO.AllWriteSet(tx)
// Remove entries that were previously written but are no longer written
cmpMap := make(map[Key]bool)
for _, w := range res.txAllOut {
cmpMap[w.Path] = true
}
for _, v := range prevWrite {
if _, ok := cmpMap[v.Path]; !ok {
mvh.Delete(v.Path, tx)
}
}
lastTxIO.recordWrite(tx, res.txOut)
lastTxIO.recordAllWrite(tx, res.txAllOut)
}
validateTasks.pushPending(tx)
execTasks.markComplete(tx)
diagExecSuccess[tx]++
cntSuccess++
execTasks.removeDependency(tx)
} else if execErr, ok := res.err.(ErrExecAbortError); ok {
addedDependencies := false
if execErr.Dependency >= 0 {
l := len(estimateDeps[tx])
for l > 0 && estimateDeps[tx][l-1] > execErr.Dependency {
execTasks.removeDependency(estimateDeps[tx][l-1])
estimateDeps[tx] = estimateDeps[tx][:l-1]
l--
}
if txIncarnations[tx] < maxIncarnation {
addedDependencies = execTasks.addDependencies(execErr.Dependency, tx)
} else {
addedDependencies = execTasks.addDependencies(tx-1, tx)
}
} else {
estimate := 0
if len(estimateDeps[tx]) > 0 {
estimate = estimateDeps[tx][len(estimateDeps[tx])-1]
}
addedDependencies = execTasks.addDependencies(estimate, tx)
newEstimate := estimate + (estimate+tx)/2
if newEstimate >= tx {
newEstimate = tx - 1
}
estimateDeps[tx] = append(estimateDeps[tx], newEstimate)
}
execTasks.clearInProgress(tx)
if !addedDependencies {
execTasks.pushPending(tx)
}
txIncarnations[tx]++
diagExecAbort[tx]++
cntAbort++
} else {
err = res.err
break
}
// do validations ...
maxComplete := execTasks.maxAllComplete()
var toValidate []int
for validateTasks.minPending() <= maxComplete && validateTasks.minPending() >= 0 {
toValidate = append(toValidate, validateTasks.takeNextPending())
}
for i := 0; i < len(toValidate); i++ {
cntTotalValidations++
tx := toValidate[i]
if skipCheck[tx] || ValidateVersion(tx, lastTxIO, mvh) {
validateTasks.markComplete(tx)
} else {
cntValidationFail++
diagExecAbort[tx]++
for _, v := range lastTxIO.AllWriteSet(tx) {
mvh.MarkEstimate(v.Path, tx)
}
// 'create validation tasks for all transactions > tx ...'
validateTasks.pushPendingSet(execTasks.getRevalidationRange(tx + 1))
validateTasks.clearInProgress(tx) // clear in progress - pending will be added again once new incarnation executes
addedDependencies := false
if txIncarnations[tx] >= maxIncarnation {
addedDependencies = execTasks.addDependencies(tx-1, tx)
}
execTasks.clearComplete(tx)
if !addedDependencies {
execTasks.pushPending(tx)
}
preValidated[tx] = false
txIncarnations[tx]++
}
}
preValidateCount := 0
invalidated := []int{}
i := sort.SearchInts(validateTasks.pending, maxComplete+1)
for i < len(validateTasks.pending) && preValidateCount < preValidateLimit {
tx := validateTasks.pending[i]
if !preValidated[tx] {
cntTotalValidations++
if !ValidateVersion(tx, lastTxIO, mvh) {
cntValidationFail++
diagExecAbort[tx]++
invalidated = append(invalidated, tx)
if execTasks.checkComplete(tx) {
execTasks.clearComplete(tx)
}
if !execTasks.checkInProgress(tx) {
for _, v := range lastTxIO.AllWriteSet(tx) {
mvh.MarkEstimate(v.Path, tx)
}
validateTasks.pushPendingSet(execTasks.getRevalidationRange(tx + 1))
addedDependencies := false
if txIncarnations[tx] >= maxIncarnation {
addedDependencies = execTasks.addDependencies(tx-1, tx)
}
if !addedDependencies {
execTasks.pushPending(tx)
}
}
txIncarnations[tx]++
preValidated[tx] = false
} else {
preValidated[tx] = true
}
preValidateCount++
}
i++
}
for _, tx := range invalidated {
validateTasks.clearPending(tx)
}
// Settle transactions that have been validated to be correct and that won't be re-executed again
maxValidated := validateTasks.maxAllComplete()
for lastSettled < maxValidated {
lastSettled++
if execTasks.checkInProgress(lastSettled) || execTasks.checkPending(lastSettled) || execTasks.blockCount[lastSettled] >= 0 {
lastSettled--
break
}
chSettle <- lastSettled
}
if validateTasks.countComplete() == len(tasks) && execTasks.countComplete() == len(tasks) {
log.Debug("blockstm exec summary", "execs", cntExec, "success", cntSuccess, "aborts", cntAbort, "validations", cntTotalValidations, "failures", cntValidationFail, "#tasks/#execs", fmt.Sprintf("%.2f%%", float64(len(tasks))/float64(cntExec)*100))
break
}
// Send the next immediate pending transaction to be executed
if execTasks.minPending() != -1 && execTasks.minPending() == maxValidated+1 {
nextTx := execTasks.takeNextPending()
if nextTx != -1 {
cntExec++
skipCheck[nextTx] = true
chTasks <- ExecVersionView{ver: Version{nextTx, txIncarnations[nextTx]}, et: tasks[nextTx], mvh: mvh, sender: tasks[nextTx].Sender()}
}
}
// Send speculative tasks
for execTasks.peekPendingGE(maxValidated+3) != -1 || len(execTasks.inProgress) == 0 {
// We skip the next transaction to avoid the case where they all have conflicts and could not be
// scheduled for re-execution immediately even when it's their time to run, because they are already in
// speculative queue.
nextTx := execTasks.takePendingGE(maxValidated + 3)
if nextTx == -1 {
nextTx = execTasks.takeNextPending()
}
if nextTx != -1 {
cntExec++
task := ExecVersionView{ver: Version{nextTx, txIncarnations[nextTx]}, et: tasks[nextTx], mvh: mvh, sender: tasks[nextTx].Sender()}
specTaskQueue.Push(nextTx, task)
chSpeculativeTasks <- struct{}{}
}
}
}
close(chTasks)
close(chSpeculativeTasks)
workerWg.Wait()
close(chResults)
settleWg.Wait()
close(chSettle)
var dag DAG
if profile {
dag = BuildDAG(*lastTxIO)
}
return ParallelExecutionResult{lastTxIO, &stats, &dag}, err
}