Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions core/filtermaps/bitset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// Copyright 2024 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

package filtermaps

import "math/bits"

// indexBitset represents a set of indices using a bitmap.
type indexBitset struct {
minIndex uint32
maxIndex uint32
bits []uint64
}

// newIndexBitset creates a bitset from a list of indices.
// Returns an empty bitset if indices is empty.
func newIndexBitset(indices []uint32) *indexBitset {
if len(indices) == 0 {
return &indexBitset{}
}
// Find index range
minIdx, maxIdx := indices[0], indices[0]
for _, idx := range indices[1:] {
if idx < minIdx {
minIdx = idx
}
if idx > maxIdx {
maxIdx = idx
}
}
// Calculate number of uint64 needed
rangeSize := maxIdx - minIdx + 1
bitsCount := (rangeSize + 63) / 64
bitset := &indexBitset{
minIndex: minIdx,
maxIndex: maxIdx,
bits: make([]uint64, bitsCount),
}
// Set all specified indices
for _, idx := range indices {
bitset.Set(idx)
}
return bitset
}

// Has checks if an index exists in the set.
func (b *indexBitset) Has(idx uint32) bool {
if b.bits == nil || idx < b.minIndex || idx > b.maxIndex {
return false
}
pos := idx - b.minIndex
wordIdx := pos / 64
bitIdx := pos % 64
return (b.bits[wordIdx] & (1 << bitIdx)) != 0
}

// Set adds an index to the set.
func (b *indexBitset) Set(idx uint32) {
if b.bits == nil || idx < b.minIndex || idx > b.maxIndex {
return
}
pos := idx - b.minIndex
wordIdx := pos / 64
bitIdx := pos % 64
b.bits[wordIdx] |= 1 << bitIdx
}

// Clear removes an index from the set.
func (b *indexBitset) Clear(idx uint32) {
if b.bits == nil || idx < b.minIndex || idx > b.maxIndex {
return
}
pos := idx - b.minIndex
wordIdx := pos / 64
bitIdx := pos % 64
b.bits[wordIdx] &^= 1 << bitIdx
}

// Count returns the number of indices in the set.
func (b *indexBitset) Count() int {
if b.bits == nil {
return 0
}
count := 0
for _, word := range b.bits {
count += bits.OnesCount64(word)
}
return count
}

// IsEmpty checks if the set is empty.
func (b *indexBitset) IsEmpty() bool {
if b.bits == nil {
return true
}
for _, word := range b.bits {
if word != 0 {
return false
}
}
return true
}

// Iterate traverses all indices in the set.
// The callback function fn is called with each index in the set.
// Iteration order is from smallest to largest.
func (b *indexBitset) Iterate(fn func(uint32)) {
if b.bits == nil {
return
}
for i, word := range b.bits {
if word == 0 {
continue
}
baseIdx := b.minIndex + uint32(i*64)
for bitIdx := 0; bitIdx < 64; bitIdx++ {
if (word & (1 << bitIdx)) != 0 {
fn(baseIdx + uint32(bitIdx))
}
}
}
}
170 changes: 170 additions & 0 deletions core/filtermaps/bitset_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// Copyright 2024 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

package filtermaps

import (
"testing"
)

func TestBitsetBasic(t *testing.T) {
indices := []uint32{100, 101, 105, 110, 115}
bs := newIndexBitset(indices)
// Test Has - existing indices
for _, idx := range indices {
if !bs.Has(idx) {
t.Errorf("Expected Has(%d) = true", idx)
}
}
// Test Has - non-existing indices
notInSet := []uint32{99, 102, 103, 104, 106, 107, 108, 109, 111, 116}
for _, idx := range notInSet {
if bs.Has(idx) {
t.Errorf("Expected Has(%d) = false", idx)
}
}
// Test Count
if count := bs.Count(); count != len(indices) {
t.Errorf("Expected Count = %d, got %d", len(indices), count)
}
}

func TestBitsetClear(t *testing.T) {
indices := []uint32{100, 101, 105, 110, 115}
bs := newIndexBitset(indices)
// Clear an index
bs.Clear(105)
if bs.Has(105) {
t.Error("Expected Has(105) = false after Clear")
}
// Count should decrease
if count := bs.Count(); count != 4 {
t.Errorf("Expected Count = 4, got %d", count)
}
// Other indices should remain unaffected
if !bs.Has(100) || !bs.Has(101) || !bs.Has(110) || !bs.Has(115) {
t.Error("Other indices should remain")
}
}

func TestBitsetSet(t *testing.T) {
indices := []uint32{100, 105, 110}
bs := newIndexBitset(indices)
// Set a new index
bs.Set(102)
if !bs.Has(102) {
t.Error("Expected Has(102) = true after Set")
}
// Count should increase
if count := bs.Count(); count != 4 {
t.Errorf("Expected Count = 4, got %d", count)
}
}

func TestBitsetIterate(t *testing.T) {
indices := []uint32{100, 101, 105, 110, 115}
bs := newIndexBitset(indices)
// Collect iterated indices
var collected []uint32
bs.Iterate(func(idx uint32) {
collected = append(collected, idx)
})
// Verify count
if len(collected) != len(indices) {
t.Errorf("Expected %d indices, got %d", len(indices), len(collected))
}
// Verify all indices are present
for _, idx := range indices {
found := false
for _, c := range collected {
if c == idx {
found = true
break
}
}
if !found {
t.Errorf("Index %d not found in iteration", idx)
}
}
}

// Benchmark: Bitset vs Map
func BenchmarkBitsetVsMap(b *testing.B) {
// Generate test data: 1000 consecutive indices
indices := make([]uint32, 1000)
for i := range indices {
indices[i] = uint32(5000 + i)
}

b.Run("Bitset_Has", func(b *testing.B) {
bs := newIndexBitset(indices)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = bs.Has(5500)
}
})

b.Run("Map_Has", func(b *testing.B) {
m := make(map[uint32]struct{})
for _, idx := range indices {
m[idx] = struct{}{}
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = m[5500]
}
})

b.Run("Bitset_Set", func(b *testing.B) {
bs := newIndexBitset(indices)
b.ResetTimer()
for i := 0; i < b.N; i++ {
bs.Set(5500)
}
})

b.Run("Map_Set", func(b *testing.B) {
m := make(map[uint32]struct{})
for _, idx := range indices {
m[idx] = struct{}{}
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
m[5500] = struct{}{}
}
})

b.Run("Bitset_Clear", func(b *testing.B) {
bs := newIndexBitset(indices)
b.ResetTimer()
for i := 0; i < b.N; i++ {
bs.Clear(5500)
bs.Set(5500) // Reset for next iteration
}
})

b.Run("Map_Delete", func(b *testing.B) {
m := make(map[uint32]struct{})
for _, idx := range indices {
m[idx] = struct{}{}
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
delete(m, 5500)
m[5500] = struct{}{} // Re-add for next iteration
}
})
}
42 changes: 23 additions & 19 deletions core/filtermaps/map_renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,17 @@ var (
// mapRenderer represents a process that renders filter maps in a specified
// range according to the actual targetView.
type mapRenderer struct {
f *FilterMaps
renderBefore uint32
currentMap *renderedMap
finishedMaps map[uint32]*renderedMap
finished common.Range[uint32]
iterator *logIterator
f *FilterMaps
renderBefore uint32
currentMap *renderedMap
finishedMaps map[uint32]*renderedMap
finished common.Range[uint32]
iterator *logIterator
rowMappingCache *lru.Cache[common.Hash, lvPos]
}

type lvPos struct{ rowIndex, layerIndex uint32 }

// renderedMap represents a single filter map that is being rendered in memory.
type renderedMap struct {
filterMap filterMap
Expand Down Expand Up @@ -110,10 +113,11 @@ func (f *FilterMaps) renderMapsFromSnapshot(cp *renderedMap) (*mapRenderer, erro
lastBlock: cp.lastBlock,
blockLvPtrs: slices.Clone(cp.blockLvPtrs),
},
finishedMaps: make(map[uint32]*renderedMap),
finished: common.NewRange(cp.mapIndex, 0),
renderBefore: math.MaxUint32,
iterator: iter,
finishedMaps: make(map[uint32]*renderedMap),
finished: common.NewRange(cp.mapIndex, 0),
renderBefore: math.MaxUint32,
iterator: iter,
rowMappingCache: lru.NewCache[common.Hash, lvPos](cachedRowMappings),
}, nil
}

Expand All @@ -131,10 +135,11 @@ func (f *FilterMaps) renderMapsFromMapBoundary(firstMap, renderBefore uint32, st
mapIndex: firstMap,
lastBlock: iter.blockNumber,
},
finishedMaps: make(map[uint32]*renderedMap),
finished: common.NewRange(firstMap, 0),
renderBefore: renderBefore,
iterator: iter,
finishedMaps: make(map[uint32]*renderedMap),
finished: common.NewRange(firstMap, 0),
renderBefore: renderBefore,
iterator: iter,
rowMappingCache: lru.NewCache[common.Hash, lvPos](cachedRowMappings),
}, nil
}

Expand Down Expand Up @@ -319,9 +324,8 @@ func (r *mapRenderer) renderCurrentMap(stopCb func() bool) (bool, error) {
if r.iterator.lvIndex == 0 {
r.currentMap.blockLvPtrs = []uint64{0}
}
type lvPos struct{ rowIndex, layerIndex uint32 }
rowMappingCache := lru.NewCache[common.Hash, lvPos](cachedRowMappings)
defer rowMappingCache.Purge()
// Clear the cache for this map rendering
r.rowMappingCache.Purge()

for r.iterator.lvIndex < uint64(r.currentMap.mapIndex+1)<<r.f.logValuesPerMap && !r.iterator.finished {
waitCnt++
Expand All @@ -337,7 +341,7 @@ func (r *mapRenderer) renderCurrentMap(stopCb func() bool) (bool, error) {
waitCnt = 0
}
if logValue := r.iterator.getValueHash(); logValue != (common.Hash{}) {
lvp, cached := rowMappingCache.Get(logValue)
lvp, cached := r.rowMappingCache.Get(logValue)
if !cached {
lvp = lvPos{rowIndex: r.f.rowIndex(r.currentMap.mapIndex, 0, logValue)}
}
Expand All @@ -348,7 +352,7 @@ func (r *mapRenderer) renderCurrentMap(stopCb func() bool) (bool, error) {
}
r.currentMap.filterMap[lvp.rowIndex] = append(r.currentMap.filterMap[lvp.rowIndex], r.f.columnIndex(r.iterator.lvIndex, &logValue))
if !cached {
rowMappingCache.Add(logValue, lvp)
r.rowMappingCache.Add(logValue, lvp)
}
}
if err := r.iterator.next(); err != nil {
Expand Down
Loading
Loading