Faster pool block iteration backwards via PoolBlock iteration cache, added benchmark to SideChain

This commit is contained in:
DataHoarder 2023-07-01 14:17:24 +02:00
parent 4ef60296f1
commit c4053f3480
Signed by: DataHoarder
SSH key fingerprint: SHA256:OLTRf6Fl87G52SiR7sWLGNzlJt4WOX+tfI2yxo0z7xk
5 changed files with 245 additions and 88 deletions

View file

@ -6,12 +6,13 @@ import (
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/client"
p2pooltypes "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"sync/atomic"
"sync"
)
type FakeServer struct {
consensus *Consensus
lastHeader atomic.Pointer[mainblock.Header]
consensus *Consensus
headersLock sync.Mutex
headers map[uint64]*mainblock.Header
}
func (s *FakeServer) Context() context.Context {
@ -50,7 +51,9 @@ func (s *FakeServer) GetChainMainByHash(hash types.Hash) *ChainMain {
return nil
}
func (s *FakeServer) GetMinimalBlockHeaderByHeight(height uint64) *mainblock.Header {
if h := s.lastHeader.Load(); h != nil && h.Height == height {
s.headersLock.Lock()
defer s.headersLock.Unlock()
if h, ok := s.headers[height]; ok {
return h
}
if h, err := s.ClientRPC().GetBlockHeaderByHeight(height, context.Background()); err != nil {
@ -67,7 +70,7 @@ func (s *FakeServer) GetMinimalBlockHeaderByHeight(height uint64) *mainblock.Hea
Difficulty: types.DifficultyFrom64(h.BlockHeader.Difficulty),
Id: types.MustHashFromString(h.BlockHeader.Hash),
}
s.lastHeader.Store(header)
s.headers[height] = header
return header
}
}
@ -99,5 +102,6 @@ func (s *FakeServer) ClearCachedBlocks() {
func GetFakeTestServer(consensus *Consensus) *FakeServer {
return &FakeServer{
consensus: consensus,
headers: make(map[uint64]*mainblock.Header),
}
}

View file

@ -72,6 +72,13 @@ func (s UniquePoolBlockSlice) GetHeight(height uint64) (result UniquePoolBlockSl
return result
}
// IterationCache Used for fast scan backwards, and of uncles
// Only maybe available in verified blocks
type IterationCache struct {
Parent *PoolBlock
Uncles []*PoolBlock
}
type PoolBlock struct {
Main mainblock.Block `json:"main"`
@ -88,6 +95,33 @@ type PoolBlock struct {
LocalTimestamp uint64 `json:"-"`
CachedShareVersion ShareVersion `json:"share_version"`
iterationCache *IterationCache
}
func (b *PoolBlock) iteratorGetParent(getByTemplateId GetByTemplateIdFunc) *PoolBlock {
if b.iterationCache == nil {
return getByTemplateId(b.Side.Parent)
}
return b.iterationCache.Parent
}
func (b *PoolBlock) iteratorUncles(getByTemplateId GetByTemplateIdFunc, uncleFunc func(uncle *PoolBlock)) error {
if b.iterationCache == nil {
for _, uncleId := range b.Side.Uncles {
uncle := getByTemplateId(uncleId)
if uncle == nil {
return fmt.Errorf("could not find uncle %s", uncleId)
}
uncleFunc(uncle)
}
} else {
for _, uncle := range b.iterationCache.Uncles {
uncleFunc(uncle)
}
}
return nil
}
// NewShareFromExportedBytes

View file

@ -96,8 +96,8 @@ func NewSideChain(server P2PoolInterface) *SideChain {
blocksByTemplateId: swiss.NewMap[types.Hash, *PoolBlock](uint32(server.Consensus().ChainWindowSize*2 + 300)),
blocksByHeight: swiss.NewMap[uint64, []*PoolBlock](uint32(server.Consensus().ChainWindowSize*2 + 300)),
preAllocatedShares: PreAllocateShares(server.Consensus().ChainWindowSize * 2),
preAllocatedDifficultyData: make([]DifficultyData, server.Consensus().ChainWindowSize*2),
preAllocatedDifficultyDifferences: make([]uint32, server.Consensus().ChainWindowSize*2),
preAllocatedDifficultyData: make([]DifficultyData, 0, server.Consensus().ChainWindowSize*2),
preAllocatedDifficultyDifferences: make([]uint32, 0, server.Consensus().ChainWindowSize*2),
preAllocatedSharesPool: NewPreAllocatedSharesPool(server.Consensus().ChainWindowSize * 2),
preAllocatedBuffer: make([]byte, 0, PoolBlockMaxTemplateSize),
preAllocatedMinedBlocks: make([]types.Hash, 0, 6*UncleBlockDepth*2+1),
@ -488,14 +488,27 @@ func (c *SideChain) verifyLoop(blockToVerify *PoolBlock) (err error) {
// This block is now verified
// Fill cache here
block.iterationCache = &IterationCache{
Parent: c.getParent(block),
Uncles: nil,
}
if len(block.Side.Uncles) > 0 {
block.iterationCache.Uncles = make([]*PoolBlock, 0, len(block.Side.Uncles))
for _, uncleId := range block.Side.Uncles {
block.iterationCache.Uncles = append(block.iterationCache.Uncles, c.getPoolBlockByTemplateId(uncleId))
}
}
c.fillPoolBlockTransactionParentIndices(block)
if isLongerChain, _ := c.isLongerChain(highestBlock, block); isLongerChain {
highestBlock = block
} else if highestBlock != nil && highestBlock.Side.Height > block.Side.Height {
log.Printf("[SideChain] block at height = %d, id = %s, is not a longer chain than height = %d, id = %s", block.Side.Height, block.SideTemplateId(c.Consensus()), highestBlock.Side.Height, highestBlock.SideTemplateId(c.Consensus()))
}
c.fillPoolBlockTransactionParentIndices(block)
if block.WantBroadcast.Load() && !block.Broadcasted.Swap(true) {
if block.Depth.Load() < UncleBlockDepth {
c.server.Broadcast(block)
@ -879,13 +892,17 @@ func (c *SideChain) pruneOldBlocks() {
for i := len(v) - 1; i >= 0; i-- {
block := v[i]
if block.Depth.Load() >= pruneDistance || (curTime >= (block.LocalTimestamp + pruneDelay)) {
if c.blocksByTemplateId.Has(block.SideTemplateId(c.Consensus())) {
c.blocksByTemplateId.Delete(block.SideTemplateId(c.Consensus()))
templateId := block.SideTemplateId(c.Consensus())
if c.blocksByTemplateId.Has(templateId) {
c.blocksByTemplateId.Delete(templateId)
numBlocksPruned++
} else {
log.Printf("[SideChain] blocksByHeight and blocksByTemplateId are inconsistent at height = %d, id = %s", height, block.SideTemplateId(c.Consensus()))
}
v = slices.Delete(v, i, i+1)
// Empty cache here
block.iterationCache = nil
}
}

View file

@ -2,6 +2,7 @@ package sidechain
import (
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/client"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"io"
"log"
"os"
@ -131,3 +132,119 @@ func TestSideChainMiniPreFork(t *testing.T) {
testSideChain(s, t, f, 2424349, 2696040, block2420028, block2420027)
}
func benchmarkResetState(tip, parent *PoolBlock, templateId types.Hash, fullId FullId, difficulty types.Difficulty, s *SideChain) {
//Remove states in maps
s.blocksByHeight.Delete(tip.Side.Height)
s.blocksByTemplateId.Delete(templateId)
s.seenBlocks.Delete(fullId)
// Update tip and depths
tip.Depth.Store(0)
parent.Depth.Store(0)
s.chainTip.Store(parent)
s.syncTip.Store(parent)
s.currentDifficulty.Store(&difficulty)
s.updateDepths(parent)
// Update verification state
tip.Verified.Store(false)
tip.Invalid.Store(false)
tip.iterationCache = nil
}
func benchSideChain(b *testing.B, s *SideChain, tipHash types.Hash) {
b.StopTimer()
tip := s.GetChainTip()
for tip.SideTemplateId(s.Consensus()) != tipHash {
s.blocksByHeight.Delete(tip.Side.Height)
s.blocksByTemplateId.Delete(tip.SideTemplateId(s.Consensus()))
s.seenBlocks.Delete(tip.FullId())
tip = s.GetParent(tip)
if tip == nil {
b.Error("nil tip")
return
}
}
templateId := tip.SideTemplateId(s.Consensus())
fullId := tip.FullId()
parent := s.GetParent(tip)
difficulty, _, _ := s.GetDifficulty(parent)
benchmarkResetState(tip, parent, templateId, fullId, difficulty, s)
var err error
b.StartTimer()
for i := 0; i < b.N; i++ {
benchmarkResetState(tip, parent, templateId, fullId, difficulty, s)
_, err, _ = s.AddPoolBlockExternal(tip)
if err != nil {
b.Error(err)
return
}
}
}
var benchLoadedSideChain *SideChain
func TestMain(m *testing.M) {
var isBenchmark bool
for _, arg := range os.Args {
if arg == "-test.bench" {
isBenchmark = true
}
}
if isBenchmark {
benchLoadedSideChain = NewSideChain(GetFakeTestServer(ConsensusDefault))
f, err := os.Open("testdata/sidechain_dump.dat")
if err != nil {
panic(err)
}
defer f.Close()
testSideChain(benchLoadedSideChain, nil, f, 4957203, 2870010)
tip := benchLoadedSideChain.GetChainTip()
// Pre-calculate PoW
for i := 0; i < 5; i++ {
tip.PowHashWithError(benchLoadedSideChain.Consensus().GetHasher(), benchLoadedSideChain.getSeedByHeightFunc())
tip = benchLoadedSideChain.GetParent(tip)
}
}
os.Exit(m.Run())
}
func BenchmarkSideChainDefault_AddPoolBlockExternal(b *testing.B) {
b.ReportAllocs()
benchSideChain(b, benchLoadedSideChain, types.MustHashFromString("61ecfc1c7738eacd8b815d2e28f124b31962996ae3af4121621b5c5501f19c5d"))
}
func BenchmarkSideChainDefault_GetDifficulty(b *testing.B) {
b.ReportAllocs()
tip := benchLoadedSideChain.GetChainTip()
b.ResetTimer()
var verifyError, invalidError error
for i := 0; i < b.N; i++ {
_, verifyError, invalidError = benchLoadedSideChain.getDifficulty(tip)
if verifyError != nil {
b.Error(verifyError)
return
}
if invalidError != nil {
b.Error(invalidError)
return
}
}
}

View file

@ -4,7 +4,6 @@ import (
"encoding/binary"
"fmt"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/address"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/block"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/randomx"
@ -210,33 +209,31 @@ func BlocksInPPLNSWindow(tip *PoolBlock, consensus *Consensus, difficultyByHeigh
for {
curWeight := cur.Side.Difficulty
for _, uncleId := range cur.Side.Uncles {
if uncle := getByTemplateId(uncleId); uncle == nil {
//cannot find uncles
return 0, fmt.Errorf("could not find uncle %s", uncleId.String())
} else {
// Skip uncles which are already out of PPLNS window
if (tip.Side.Height - uncle.Side.Height) >= consensus.ChainWindowSize {
continue
}
// Take some % of uncle's weight into this share
unclePenalty := uncle.Side.Difficulty.Mul64(consensus.UnclePenalty).Div64(100)
uncleWeight := uncle.Side.Difficulty.Sub(unclePenalty)
newPplnsWeight := pplnsWeight.Add(uncleWeight)
// Skip uncles that push PPLNS weight above the limit
if newPplnsWeight.Cmp(maxPplnsWeight) > 0 {
continue
}
curWeight = curWeight.Add(unclePenalty)
if addWeightFunc != nil {
addWeightFunc(uncle, uncleWeight)
}
pplnsWeight = newPplnsWeight
if err := cur.iteratorUncles(getByTemplateId, func(uncle *PoolBlock) {
// Skip uncles which are already out of PPLNS window
if (tip.Side.Height - uncle.Side.Height) >= consensus.ChainWindowSize {
return
}
// Take some % of uncle's weight into this share
unclePenalty := uncle.Side.Difficulty.Mul64(consensus.UnclePenalty).Div64(100)
uncleWeight := uncle.Side.Difficulty.Sub(unclePenalty)
newPplnsWeight := pplnsWeight.Add(uncleWeight)
// Skip uncles that push PPLNS weight above the limit
if newPplnsWeight.Cmp(maxPplnsWeight) > 0 {
return
}
curWeight = curWeight.Add(unclePenalty)
if addWeightFunc != nil {
addWeightFunc(uncle, uncleWeight)
}
pplnsWeight = newPplnsWeight
}); err != nil {
return 0, err
}
// Always add non-uncle shares even if PPLNS weight goes above the limit
@ -265,7 +262,7 @@ func BlocksInPPLNSWindow(tip *PoolBlock, consensus *Consensus, difficultyByHeigh
}
parentId := cur.Side.Parent
cur = getByTemplateId(parentId)
cur = cur.iteratorGetParent(getByTemplateId)
if cur == nil {
return 0, fmt.Errorf("could not find parent %s", parentId.String())
@ -278,21 +275,17 @@ func GetSharesOrdered(tip *PoolBlock, consensus *Consensus, difficultyByHeight b
index := 0
l := len(preAllocatedShares)
insertShare := func(weight types.Difficulty, a address.PackedAddress) {
if bottomHeight, err := BlocksInPPLNSWindow(tip, consensus, difficultyByHeight, getByTemplateId, func(b *PoolBlock, weight types.Difficulty) {
if index < l {
preAllocatedShares[index].Address = a
preAllocatedShares[index].Address = b.GetAddress()
preAllocatedShares[index].Weight = weight
} else {
preAllocatedShares = append(preAllocatedShares, &Share{
Address: a,
Address: b.GetAddress(),
Weight: weight,
})
}
index++
}
if bottomHeight, err := BlocksInPPLNSWindow(tip, consensus, difficultyByHeight, getByTemplateId, func(b *PoolBlock, weight types.Difficulty) {
insertShare(weight, b.GetAddress())
}); err != nil {
return nil, 0
} else {
@ -344,43 +337,35 @@ func ShuffleSequence(shareVersion ShareVersion, privateKeySeed types.Hash, items
}
}
// GetDifficulty Gets the difficulty at tip
// preAllocatedDifficultyData should contain enough capacity to fit all entries to iterate through.
// preAllocatedTimestampDifferences should contain enough capacity to fit all differences.
func GetDifficulty(tip *PoolBlock, consensus *Consensus, getByTemplateId GetByTemplateIdFunc, preAllocatedDifficultyData []DifficultyData, preAllocatedTimestampDifferences []uint32) (difficulty types.Difficulty, verifyError, invalidError error) {
index := 0
l := len(preAllocatedDifficultyData)
insertDifficultyData := func(cumDiff types.Difficulty, timestamp uint64) {
if index < l {
preAllocatedDifficultyData[index].CumulativeDifficulty = cumDiff
preAllocatedDifficultyData[index].Timestamp = timestamp
} else {
preAllocatedDifficultyData = append(preAllocatedDifficultyData, DifficultyData{
CumulativeDifficulty: cumDiff,
Timestamp: timestamp,
})
}
index++
}
difficultyData := preAllocatedDifficultyData[:0]
cur := tip
var blockDepth uint64
var oldestTimestamp uint64 = math.MaxUint64
for {
oldestTimestamp = min(oldestTimestamp, cur.Main.Timestamp)
insertDifficultyData(cur.Side.CumulativeDifficulty, cur.Main.Timestamp)
difficultyData = append(difficultyData, DifficultyData{
CumulativeDifficulty: cur.Side.CumulativeDifficulty,
Timestamp: cur.Main.Timestamp,
})
for _, uncleId := range cur.Side.Uncles {
if uncle := getByTemplateId(uncleId); uncle == nil {
//cannot find uncles
return types.ZeroDifficulty, fmt.Errorf("could not find uncle %s", uncleId), nil
} else {
// Skip uncles which are already out of PPLNS window
if (tip.Side.Height - uncle.Side.Height) >= consensus.ChainWindowSize {
continue
}
oldestTimestamp = min(oldestTimestamp, uncle.Main.Timestamp)
insertDifficultyData(uncle.Side.CumulativeDifficulty, uncle.Main.Timestamp)
if err := cur.iteratorUncles(getByTemplateId, func(uncle *PoolBlock) {
// Skip uncles which are already out of PPLNS window
if (tip.Side.Height - uncle.Side.Height) >= consensus.ChainWindowSize {
return
}
difficultyData = append(difficultyData, DifficultyData{
CumulativeDifficulty: uncle.Side.CumulativeDifficulty,
Timestamp: uncle.Main.Timestamp,
})
}); err != nil {
return types.ZeroDifficulty, err, nil
}
blockDepth++
@ -395,24 +380,25 @@ func GetDifficulty(tip *PoolBlock, consensus *Consensus, getByTemplateId GetByTe
}
parentId := cur.Side.Parent
cur = getByTemplateId(parentId)
cur = cur.iteratorGetParent(getByTemplateId)
if cur == nil {
return types.ZeroDifficulty, fmt.Errorf("could not find parent %s", parentId), nil
return types.ZeroDifficulty, fmt.Errorf("could not find parent %s", parentId.String()), nil
}
}
difficultyData := preAllocatedDifficultyData[:index]
if len(difficultyData) > len(preAllocatedTimestampDifferences) {
preAllocatedTimestampDifferences = make([]uint32, len(difficultyData))
for i := range difficultyData {
if difficultyData[i].Timestamp < oldestTimestamp {
oldestTimestamp = difficultyData[i].Timestamp
}
}
tmpTimestamps := preAllocatedTimestampDifferences[:0]
// Discard 10% oldest and 10% newest (by timestamp) blocks
for i := range difficultyData {
preAllocatedTimestampDifferences[i] = uint32(difficultyData[i].Timestamp - oldestTimestamp)
tmpTimestamps = append(tmpTimestamps, uint32(difficultyData[i].Timestamp-oldestTimestamp))
}
tmpTimestamps := preAllocatedTimestampDifferences[:len(difficultyData)]
cutSize := (len(difficultyData) + 9) / 10
index1 := cutSize - 1
@ -440,13 +426,12 @@ func GetDifficulty(tip *PoolBlock, consensus *Consensus, getByTemplateId GetByTe
var diff2 types.Difficulty
for i := range difficultyData {
d := &difficultyData[i]
if timestamp1 <= d.Timestamp && d.Timestamp <= timestamp2 {
if d.CumulativeDifficulty.Cmp(diff1) < 0 {
diff1 = d.CumulativeDifficulty
if timestamp1 <= difficultyData[i].Timestamp && difficultyData[i].Timestamp <= timestamp2 {
if difficultyData[i].CumulativeDifficulty.Cmp(diff1) < 0 {
diff1 = difficultyData[i].CumulativeDifficulty
}
if diff2.Cmp(d.CumulativeDifficulty) < 0 {
diff2 = d.CumulativeDifficulty
if diff2.Cmp(difficultyData[i].CumulativeDifficulty) < 0 {
diff2 = difficultyData[i].CumulativeDifficulty
}
}
}