Skip to content

Commit c8d0622

Browse files
committed
make perf changes to memoize and remove gc pressure
1 parent 8d68f9d commit c8d0622

10 files changed

Lines changed: 111 additions & 3 deletions

File tree

block/internal/cache/generic_cache.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ func (c *Cache) isSeen(hash string) bool {
6161
func (c *Cache) setSeen(hash string, height uint64) {
6262
c.mu.Lock()
6363
defer c.mu.Unlock()
64+
if existing, ok := c.hashByHeight[height]; ok && existing == hash {
65+
c.hashes[existing] = true
66+
return
67+
}
6468
c.hashes[hash] = true
6569
c.hashByHeight[height] = hash
6670
}

block/internal/syncing/syncer.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1166,8 +1166,9 @@ func (s *Syncer) IsSyncedWithRaft(raftState *raft.RaftBlockState) (int, error) {
11661166
s.logger.Error().Err(err).Uint64("height", raftState.Height).Msg("failed to get header for sync check")
11671167
return 0, fmt.Errorf("get header for sync check at height %d: %w", raftState.Height, err)
11681168
}
1169-
if !bytes.Equal(header.Hash(), raftState.Hash) {
1170-
return 0, fmt.Errorf("header hash mismatch: %x vs %x", header.Hash(), raftState.Hash)
1169+
headerHash := header.Hash()
1170+
if !bytes.Equal(headerHash, raftState.Hash) {
1171+
return 0, fmt.Errorf("header hash mismatch: %x vs %x", headerHash, raftState.Hash)
11711172
}
11721173

11731174
return 0, nil

pkg/store/cached_store.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ func (cs *CachedStore) GetHeader(ctx context.Context, height uint64) (*types.Sig
9797
return nil, err
9898
}
9999

100+
header.MemoizeHash()
101+
100102
// Add to cache
101103
cs.headerCache.Add(height, header)
102104

@@ -116,6 +118,8 @@ func (cs *CachedStore) GetBlockData(ctx context.Context, height uint64) (*types.
116118
return nil, nil, err
117119
}
118120

121+
header.MemoizeHash()
122+
119123
// Add to cache
120124
cs.blockDataCache.Add(height, &blockDataEntry{header: header, data: data})
121125

types/hashing.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,38 @@ func (h *Header) HashLegacy() (Hash, error) {
4141
return hash[:], nil
4242
}
4343

44-
// Hash returns hash of the header
44+
// Hash returns the header hash. It reuses a memoized value if one has already
45+
// been prepared via MemoizeHash, but it does not write to the header itself.
4546
func (h *Header) Hash() Hash {
4647
if h == nil {
4748
return nil
4849
}
50+
if h.cachedHash != nil {
51+
return h.cachedHash
52+
}
53+
54+
return h.computeHash()
55+
}
56+
57+
// MemoizeHash computes the header hash and stores it on the header for future
58+
// Hash() calls. Call this before publishing the header to shared goroutines or
59+
// caches.
60+
func (h *Header) MemoizeHash() Hash {
61+
if h == nil {
62+
return nil
63+
}
64+
if h.cachedHash != nil {
65+
return h.cachedHash
66+
}
67+
68+
hash := h.computeHash()
69+
if hash != nil {
70+
h.cachedHash = hash
71+
}
72+
return hash
73+
}
4974

75+
func (h *Header) computeHash() Hash {
5076
slimHash, err := h.HashSlim()
5177
if err != nil {
5278
return nil
@@ -62,6 +88,14 @@ func (h *Header) Hash() Hash {
6288
return slimHash
6389
}
6490

91+
// InvalidateHash clears the memoized hash, forcing recomputation on the next
92+
// Hash() call. Must be called after any mutation of Header fields.
93+
func (h *Header) InvalidateHash() {
94+
if h != nil {
95+
h.cachedHash = nil
96+
}
97+
}
98+
6599
// Hash returns hash of the Data
66100
func (d *Data) Hash() Hash {
67101
// Ignoring the marshal error for now to satisfy the go-header interface

types/hashing_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ func TestHeaderHash(t *testing.T) {
2121
}
2222

2323
hash1 := header.Hash()
24+
assert.Nil(t, header.cachedHash, "Hash() should not memoize")
25+
26+
memoizedHash := header.MemoizeHash()
27+
assert.Equal(t, hash1, memoizedHash)
28+
assert.NotNil(t, header.cachedHash, "MemoizeHash() should store the computed value")
2429

2530
headerBytes, err := header.MarshalBinary()
2631
require.NoError(t, err)
@@ -31,6 +36,8 @@ func TestHeaderHash(t *testing.T) {
3136
assert.Equal(t, Hash(expectedHash[:]), hash1, "Header hash should match manual calculation")
3237

3338
header.BaseHeader.Height = 2
39+
header.InvalidateHash()
40+
assert.Nil(t, header.cachedHash)
3441
hash2 := header.Hash()
3542
assert.NotEqual(t, hash1, hash2, "Different headers should have different hashes")
3643
}
@@ -143,3 +150,17 @@ func TestHeaderHashWithBytes(t *testing.T) {
143150
require.NoError(t, targetHeader.UnmarshalBinary(headerBytes))
144151
assert.Equal(t, hash1, targetHeader.Hash(), "HeaderHash should produce same result as Header.Hash()")
145152
}
153+
154+
func TestHeaderCloneDropsCachedHash(t *testing.T) {
155+
header := &Header{
156+
BaseHeader: BaseHeader{Height: 1, Time: 1234567890},
157+
DataHash: []byte("datahash"),
158+
}
159+
160+
header.MemoizeHash()
161+
require.NotNil(t, header.cachedHash)
162+
163+
clone := header.Clone()
164+
assert.Nil(t, clone.cachedHash, "Clone should not copy memoized hash state")
165+
assert.Equal(t, header.Hash(), clone.Hash())
166+
}

types/header.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ type Header struct {
8282
// representation but may still be required for backwards compatible binary
8383
// serialization (e.g. legacy signing payloads).
8484
Legacy *LegacyHeaderFields
85+
86+
// cachedHash holds the memoized result of MemoizeHash(). nil means cold.
87+
// Any caller that mutates header fields must call InvalidateHash() to clear it.
88+
cachedHash Hash
8589
}
8690

8791
// New creates a new Header.
@@ -250,6 +254,7 @@ func (h *Header) ApplyLegacyDefaults() {
250254
h.Legacy = &LegacyHeaderFields{}
251255
}
252256
h.Legacy.EnsureDefaults()
257+
h.InvalidateHash()
253258
}
254259

255260
// Clone creates a deep copy of the header, ensuring all mutable slices are
@@ -262,6 +267,7 @@ func (h Header) Clone() Header {
262267
clone.ValidatorHash = cloneBytes(h.ValidatorHash)
263268
clone.ProposerAddress = cloneBytes(h.ProposerAddress)
264269
clone.Legacy = h.Legacy.Clone()
270+
clone.cachedHash = nil
265271

266272
return clone
267273
}

types/serialization.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ func (h *Header) FromProto(other *pb.Header) error {
257257
if other == nil {
258258
return errors.New("header is nil")
259259
}
260+
h.InvalidateHash()
260261
if other.Version != nil {
261262
h.Version.Block = other.Version.Block
262263
h.Version.App = other.Version.App

types/serialization_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,27 @@ func TestHeader_HashFields_NilAndEmpty(t *testing.T) {
356356
assert.Nil(t, h2.ValidatorHash)
357357
}
358358

359+
func TestHeaderFromProtoClearsCachedHash(t *testing.T) {
360+
t.Parallel()
361+
362+
header := &Header{
363+
BaseHeader: BaseHeader{Height: 1, Time: 1234567890},
364+
DataHash: []byte("datahash"),
365+
}
366+
header.MemoizeHash()
367+
require.NotNil(t, header.cachedHash)
368+
369+
protoMsg := (&Header{
370+
BaseHeader: BaseHeader{Height: 2, Time: 1234567891},
371+
DataHash: []byte("otherhash"),
372+
}).ToProto()
373+
374+
require.NoError(t, header.FromProto(protoMsg))
375+
assert.Nil(t, header.cachedHash)
376+
assert.Equal(t, uint64(2), header.Height())
377+
assert.Equal(t, Hash([]byte("otherhash")), header.DataHash)
378+
}
379+
359380
func TestHeaderMarshalBinary_PreservesLegacyFields(t *testing.T) {
360381
t.Parallel()
361382

types/utils.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ func GetRandomNextHeader(header Header, chainID string) Header {
126126
nextHeader.LastHeaderHash = header.Hash()
127127
nextHeader.ProposerAddress = header.ProposerAddress
128128
nextHeader.ValidatorHash = header.ValidatorHash
129+
nextHeader.InvalidateHash()
129130
return nextHeader
130131
}
131132

types/utils_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package types_test
22

33
import (
44
// Import bytes package
5+
"crypto/sha256"
56
"testing"
67
"time" // Used for time.Time comparison
78

@@ -56,6 +57,20 @@ func TestGetRandomHeader(t *testing.T) {
5657
}
5758
}
5859

60+
func TestGetRandomNextHeader_InvalidatesCachedHash(t *testing.T) {
61+
header := types.GetRandomHeader("TestGetRandomNextHeader", types.GetRandomBytes(32))
62+
header.MemoizeHash()
63+
64+
nextHeader := types.GetRandomNextHeader(header, "TestGetRandomNextHeader")
65+
gotHash := nextHeader.Hash()
66+
67+
headerBytes, err := nextHeader.MarshalBinary()
68+
assert.NoError(t, err)
69+
expected := sha256.Sum256(headerBytes)
70+
71+
assert.Equal(t, types.Hash(expected[:]), gotHash)
72+
}
73+
5974
func TestGetFirstSignedHeader(t *testing.T) {
6075
testCases := []struct {
6176
name string

0 commit comments

Comments
 (0)