Skip to content

Commit 1109639

Browse files
chore: move cache from pkg, add tests and benchmrks and remove unneeded mutexs (#2648)
<!-- Please read and fill out this form before submitting your PR. Please make sure you have reviewed our contributors guide before submitting your first PR. NOTE: PR titles should follow semantic commits: https://www.conventionalcommits.org/en/v1.0.0/ --> ## Overview <!-- Please provide an explanation of the PR, including the appropriate context, background, goal, and rationale. If there is an issue with this information, please provide a tl;dr and link the issue. Ex: Closes #<issue number> --> --------- Co-authored-by: julienrbrt <julien@rbrt.fr>
1 parent ec2f933 commit 1109639

File tree

11 files changed

+595
-406
lines changed

11 files changed

+595
-406
lines changed

.github/workflows/claude.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,4 @@ jobs:
4747
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
4848
# or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line for available options
4949
# claude_args: '--model claude-opus-4-1-20250805 --allowed-tools Bash(gh pr:*)'
50+
use_sticky_comment: true

block/internal/cache/bench_test.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package cache
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/rs/zerolog"
8+
9+
"github.com/evstack/ev-node/pkg/config"
10+
"github.com/evstack/ev-node/pkg/store"
11+
"github.com/evstack/ev-node/types"
12+
)
13+
14+
/*
15+
goos: darwin
16+
goarch: arm64
17+
pkg: github.com/evstack/ev-node/block/internal/cache
18+
cpu: Apple M1 Pro
19+
BenchmarkManager_GetPendingHeaders/N=1000-10 278 3922717 ns/op 5064666 B/op 70818 allocs/op
20+
BenchmarkManager_GetPendingHeaders/N=10000-10 28 40704543 ns/op 50639803 B/op 709864 allocs/op
21+
BenchmarkManager_GetPendingData/N=1000-10 279 4258291 ns/op 5869716 B/op 73824 allocs/op
22+
BenchmarkManager_GetPendingData/N=10000-10 26 45428974 ns/op 58719067 B/op 739926 allocs/op
23+
BenchmarkManager_PendingEventsSnapshot-10 336 3251530 ns/op 2365497 B/op 285 allocs/op
24+
PASS
25+
ok github.com/evstack/ev-node/block/internal/cache 25.834s
26+
*/
27+
28+
func benchSetupStore(b *testing.B, n int, txsPer int, chainID string) store.Store {
29+
ds, err := store.NewDefaultInMemoryKVStore()
30+
if err != nil {
31+
b.Fatal(err)
32+
}
33+
st := store.New(ds)
34+
ctx := context.Background()
35+
for i := 1; i <= n; i++ {
36+
h, d := types.GetRandomBlock(uint64(i), txsPer, chainID)
37+
if err := st.SaveBlockData(ctx, h, d, &types.Signature{}); err != nil {
38+
b.Fatal(err)
39+
}
40+
}
41+
if err := st.SetHeight(ctx, uint64(n)); err != nil {
42+
b.Fatal(err)
43+
}
44+
return st
45+
}
46+
47+
func benchNewManager(b *testing.B, st store.Store) Manager {
48+
cfg := config.DefaultConfig
49+
cfg.RootDir = b.TempDir()
50+
m, err := NewManager(cfg, st, zerolog.Nop())
51+
if err != nil {
52+
b.Fatal(err)
53+
}
54+
return m
55+
}
56+
57+
func BenchmarkManager_GetPendingHeaders(b *testing.B) {
58+
for _, n := range []int{1_000, 10_000} {
59+
b.Run(benchName(n), func(b *testing.B) {
60+
st := benchSetupStore(b, n, 1, "bench-headers")
61+
m := benchNewManager(b, st)
62+
ctx := context.Background()
63+
b.ReportAllocs()
64+
b.ResetTimer()
65+
for i := 0; i < b.N; i++ {
66+
hs, err := m.GetPendingHeaders(ctx)
67+
if err != nil {
68+
b.Fatal(err)
69+
}
70+
if len(hs) == 0 {
71+
b.Fatal("unexpected empty headers")
72+
}
73+
}
74+
})
75+
}
76+
}
77+
78+
func BenchmarkManager_GetPendingData(b *testing.B) {
79+
for _, n := range []int{1_000, 10_000} {
80+
b.Run(benchName(n), func(b *testing.B) {
81+
st := benchSetupStore(b, n, 2, "bench-data") // ensure data not filtered
82+
m := benchNewManager(b, st)
83+
ctx := context.Background()
84+
b.ReportAllocs()
85+
b.ResetTimer()
86+
for i := 0; i < b.N; i++ {
87+
ds, err := m.GetPendingData(ctx)
88+
if err != nil {
89+
b.Fatal(err)
90+
}
91+
if len(ds) == 0 {
92+
b.Fatal("unexpected empty data")
93+
}
94+
}
95+
})
96+
}
97+
}
98+
99+
func BenchmarkManager_PendingEventsSnapshot(b *testing.B) {
100+
st := benchSetupStore(b, 1_000, 1, "bench-events")
101+
m := benchNewManager(b, st)
102+
for i := 1; i <= 50_000; i++ {
103+
h, d := types.GetRandomBlock(uint64(i), 1, "bench-events")
104+
m.SetPendingEvent(uint64(i), &DAHeightEvent{Header: h, Data: d, DaHeight: uint64(i)})
105+
}
106+
b.ReportAllocs()
107+
b.ResetTimer()
108+
for i := 0; i < b.N; i++ {
109+
ev := m.GetPendingEvents()
110+
if len(ev) == 0 {
111+
b.Fatal("unexpected empty events")
112+
}
113+
}
114+
}
115+
116+
func benchName(n int) string {
117+
// simple itoa without fmt to avoid allocations
118+
if n == 0 {
119+
return "N=0"
120+
}
121+
var buf [20]byte
122+
i := len(buf)
123+
for n > 0 {
124+
i--
125+
buf[i] = byte('0' + n%10)
126+
n /= 10
127+
}
128+
return "N=" + string(buf[i:])
129+
}
Lines changed: 27 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -10,54 +10,60 @@ import (
1010

1111
// Cache is a generic cache that maintains items that are seen and hard confirmed
1212
type Cache[T any] struct {
13-
items *sync.Map
14-
hashes *sync.Map
13+
// itemsByHeight stores items keyed by uint64 height
14+
itemsByHeight *sync.Map
15+
// hashes tracks whether a given hash has been seen
16+
hashes *sync.Map
17+
// daIncluded tracks the DA inclusion height for a given hash
1518
daIncluded *sync.Map
1619
}
1720

1821
// NewCache returns a new Cache struct
1922
func NewCache[T any]() *Cache[T] {
2023
return &Cache[T]{
21-
items: new(sync.Map),
22-
hashes: new(sync.Map),
23-
daIncluded: new(sync.Map),
24+
itemsByHeight: new(sync.Map),
25+
hashes: new(sync.Map),
26+
daIncluded: new(sync.Map),
2427
}
2528
}
2629

27-
// GetItem returns an item from the cache by height
30+
// GetItem returns an item from the cache by height.
31+
// Returns nil if not found or type mismatch.
2832
func (c *Cache[T]) GetItem(height uint64) *T {
29-
item, ok := c.items.Load(height)
33+
item, ok := c.itemsByHeight.Load(height)
34+
if !ok {
35+
return nil
36+
}
37+
val, ok := item.(*T)
3038
if !ok {
3139
return nil
3240
}
33-
val := item.(*T)
3441
return val
3542
}
3643

3744
// SetItem sets an item in the cache by height
3845
func (c *Cache[T]) SetItem(height uint64, item *T) {
39-
c.items.Store(height, item)
46+
c.itemsByHeight.Store(height, item)
4047
}
4148

4249
// DeleteItem deletes an item from the cache by height
4350
func (c *Cache[T]) DeleteItem(height uint64) {
44-
c.items.Delete(height)
51+
c.itemsByHeight.Delete(height)
4552
}
4653

47-
// RangeByHeight iterates over all items keyed by uint64 height and calls fn for each.
54+
// RangeByHeight iterates over items keyed by height in an unspecified order and calls fn for each.
4855
// If fn returns false, iteration stops early.
49-
// Non-uint64 keys (e.g. string hash entries) are ignored.
5056
func (c *Cache[T]) RangeByHeight(fn func(height uint64, item *T) bool) {
51-
c.items.Range(func(k, v any) bool {
57+
c.itemsByHeight.Range(func(k, v any) bool {
5258
height, ok := k.(uint64)
5359
if !ok {
5460
return true
5561
}
56-
item, ok := v.(*T)
62+
it, ok := v.(*T)
5763
if !ok {
5864
return true
5965
}
60-
return fn(height, item)
66+
return fn(height, it)
6167
})
6268
}
6369

@@ -81,23 +87,13 @@ func (c *Cache[T]) IsDAIncluded(hash string) bool {
8187
return ok
8288
}
8389

84-
// GetDAIncludedHeight returns the DA height at which the hash was DA included
85-
func (c *Cache[T]) GetDAIncludedHeight(hash string) (uint64, bool) {
86-
daIncluded, ok := c.daIncluded.Load(hash)
87-
if !ok {
88-
return 0, false
89-
}
90-
return daIncluded.(uint64), true
91-
}
92-
9390
// SetDAIncluded sets the hash as DA-included with the given DA height
9491
func (c *Cache[T]) SetDAIncluded(hash string, daHeight uint64) {
9592
c.daIncluded.Store(hash, daHeight)
9693
}
9794

9895
const (
9996
itemsByHeightFilename = "items_by_height.gob"
100-
itemsByHashFilename = "items_by_hash.gob"
10197
hashesFilename = "hashes.gob"
10298
daIncludedFilename = "da_included.gob"
10399
)
@@ -147,34 +143,19 @@ func (c *Cache[T]) SaveToDisk(folderPath string) error {
147143

148144
// prepare items maps
149145
itemsByHeightMap := make(map[uint64]*T)
150-
itemsByHashMap := make(map[string]*T)
151146

152-
var invalidItemsErr error
153-
c.items.Range(func(k, v any) bool {
154-
itemVal, ok := v.(*T)
155-
if !ok {
156-
invalidItemsErr = fmt.Errorf("invalid item type: %T", v)
157-
return false // early exit if the value is not of type *T
158-
}
159-
switch key := k.(type) {
160-
case uint64:
161-
itemsByHeightMap[key] = itemVal
162-
case string:
163-
itemsByHashMap[key] = itemVal
147+
c.itemsByHeight.Range(func(k, v any) bool {
148+
if hk, ok := k.(uint64); ok {
149+
if it, ok := v.(*T); ok {
150+
itemsByHeightMap[hk] = it
151+
}
164152
}
165153
return true
166154
})
167155

168-
if invalidItemsErr != nil {
169-
return invalidItemsErr
170-
}
171-
172156
if err := saveMapGob(filepath.Join(folderPath, itemsByHeightFilename), itemsByHeightMap); err != nil {
173157
return err
174158
}
175-
if err := saveMapGob(filepath.Join(folderPath, itemsByHashFilename), itemsByHashMap); err != nil {
176-
return err
177-
}
178159

179160
// prepare hashes map
180161
hashesToSave := make(map[string]bool)
@@ -214,16 +195,7 @@ func (c *Cache[T]) LoadFromDisk(folderPath string) error {
214195
return fmt.Errorf("failed to load items by height: %w", err)
215196
}
216197
for k, v := range itemsByHeightMap {
217-
c.items.Store(k, v)
218-
}
219-
220-
// load items by hash
221-
itemsByHashMap, err := loadMapGob[string, *T](filepath.Join(folderPath, itemsByHashFilename))
222-
if err != nil {
223-
return fmt.Errorf("failed to load items by hash: %w", err)
224-
}
225-
for k, v := range itemsByHashMap {
226-
c.items.Store(k, v)
198+
c.itemsByHeight.Store(k, v)
227199
}
228200

229201
// load hashes

0 commit comments

Comments
 (0)