Skip to content

Commit c930483

Browse files
Convert P-chain Tx cache to be byte based (ava-labs#1517)
Co-authored-by: Stephen Buttolph <stephen@avalabs.org>
1 parent ec147ab commit c930483

File tree

12 files changed

+383
-142
lines changed

12 files changed

+383
-142
lines changed

cache/cache.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ type Cacher[K comparable, V any] interface {
1818

1919
// Flush removes all entries from the cache
2020
Flush()
21+
22+
// Returns fraction of cache currently filled (0 --> 1)
23+
PortionFilled() float64
2124
}
2225

2326
// Evictable allows the object to be notified when it is evicted

cache/lru_cache.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ func (c *LRU[_, _]) Flush() {
5050
c.flush()
5151
}
5252

53+
func (c *LRU[_, _]) PortionFilled() float64 {
54+
c.lock.Lock()
55+
defer c.lock.Unlock()
56+
57+
return c.portionFilled()
58+
}
59+
5360
func (c *LRU[K, V]) put(key K, value V) {
5461
c.resize()
5562

@@ -81,6 +88,10 @@ func (c *LRU[K, V]) flush() {
8188
c.elements = linkedhashmap.New[K, V]()
8289
}
8390

91+
func (c *LRU[_, _]) portionFilled() float64 {
92+
return float64(c.elements.Len()) / float64(c.Size)
93+
}
94+
8495
// Initializes [c.elements] if it's nil.
8596
// Sets [c.size] to 1 if it's <= 0.
8697
// Removes oldest elements to make number of elements

cache/lru_cache_test.go

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,59 +6,60 @@ package cache
66
import (
77
"testing"
88

9+
"github.com/stretchr/testify/require"
10+
911
"github.com/ava-labs/avalanchego/ids"
1012
)
1113

1214
func TestLRU(t *testing.T) {
13-
cache := &LRU[ids.ID, int]{Size: 1}
15+
cache := &LRU[ids.ID, TestSizedInt]{Size: 1}
1416

1517
TestBasic(t, cache)
1618
}
1719

1820
func TestLRUEviction(t *testing.T) {
19-
cache := &LRU[ids.ID, int]{Size: 2}
21+
cache := &LRU[ids.ID, TestSizedInt]{Size: 2}
2022

2123
TestEviction(t, cache)
2224
}
2325

2426
func TestLRUResize(t *testing.T) {
25-
cache := LRU[ids.ID, int]{Size: 2}
27+
require := require.New(t)
28+
cache := LRU[ids.ID, TestSizedInt]{Size: 2}
2629

2730
id1 := ids.ID{1}
2831
id2 := ids.ID{2}
2932

30-
cache.Put(id1, 1)
31-
cache.Put(id2, 2)
33+
expectedVal1 := TestSizedInt{i: 1}
34+
expectedVal2 := TestSizedInt{i: 2}
35+
cache.Put(id1, expectedVal1)
36+
cache.Put(id2, expectedVal2)
37+
38+
val, found := cache.Get(id1)
39+
require.True(found)
40+
require.Equal(expectedVal1, val)
3241

33-
if val, found := cache.Get(id1); !found {
34-
t.Fatalf("Failed to retrieve value when one exists")
35-
} else if val != 1 {
36-
t.Fatalf("Retrieved wrong value")
37-
} else if val, found := cache.Get(id2); !found {
38-
t.Fatalf("Failed to retrieve value when one exists")
39-
} else if val != 2 {
40-
t.Fatalf("Retrieved wrong value")
41-
}
42+
val, found = cache.Get(id2)
43+
require.True(found)
44+
require.Equal(expectedVal2, val)
4245

4346
cache.Size = 1
4447
// id1 evicted
4548

46-
if _, found := cache.Get(id1); found {
47-
t.Fatalf("Retrieve value when none exists")
48-
} else if val, found := cache.Get(id2); !found {
49-
t.Fatalf("Failed to retrieve value when one exists")
50-
} else if val != 2 {
51-
t.Fatalf("Retrieved wrong value")
52-
}
49+
_, found = cache.Get(id1)
50+
require.False(found)
51+
52+
val, found = cache.Get(id2)
53+
require.True(found)
54+
require.Equal(expectedVal2, val)
5355

5456
cache.Size = 0
5557
// We reset the size to 1 in resize
5658

57-
if _, found := cache.Get(id1); found {
58-
t.Fatalf("Retrieve value when none exists")
59-
} else if val, found := cache.Get(id2); !found {
60-
t.Fatalf("Failed to retrieve value when one exists")
61-
} else if val != 2 {
62-
t.Fatalf("Retrieved wrong value")
63-
}
59+
_, found = cache.Get(id1)
60+
require.False(found)
61+
62+
val, found = cache.Get(id2)
63+
require.True(found)
64+
require.Equal(expectedVal2, val)
6465
}

cache/lru_sized_cache.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved.
2+
// See the file LICENSE for licensing terms.
3+
4+
package cache
5+
6+
import (
7+
"sync"
8+
9+
"github.com/ava-labs/avalanchego/utils"
10+
"github.com/ava-labs/avalanchego/utils/linkedhashmap"
11+
)
12+
13+
var _ Cacher[struct{}, SizedElement] = (*sizedLRU[struct{}, SizedElement])(nil)
14+
15+
type SizedElement interface {
16+
Size() int
17+
}
18+
19+
// sizedLRU is a key value store with bounded size. If the size is attempted to
20+
// be exceeded, then elements are removed from the cache until the bound is
21+
// honored, based on evicting the least recently used value.
22+
type sizedLRU[K comparable, V SizedElement] struct {
23+
lock sync.Mutex
24+
elements linkedhashmap.LinkedHashmap[K, V]
25+
maxSize int
26+
currentSize int
27+
}
28+
29+
func NewSizedLRU[K comparable, V SizedElement](maxSize int) Cacher[K, V] {
30+
return &sizedLRU[K, V]{
31+
elements: linkedhashmap.New[K, V](),
32+
maxSize: maxSize,
33+
}
34+
}
35+
36+
func (c *sizedLRU[K, V]) Put(key K, value V) {
37+
c.lock.Lock()
38+
defer c.lock.Unlock()
39+
40+
c.put(key, value)
41+
}
42+
43+
func (c *sizedLRU[K, V]) Get(key K) (V, bool) {
44+
c.lock.Lock()
45+
defer c.lock.Unlock()
46+
47+
return c.get(key)
48+
}
49+
50+
func (c *sizedLRU[K, V]) Evict(key K) {
51+
c.lock.Lock()
52+
defer c.lock.Unlock()
53+
54+
c.evict(key)
55+
}
56+
57+
func (c *sizedLRU[K, V]) Flush() {
58+
c.lock.Lock()
59+
defer c.lock.Unlock()
60+
61+
c.flush()
62+
}
63+
64+
func (c *sizedLRU[_, _]) PortionFilled() float64 {
65+
c.lock.Lock()
66+
defer c.lock.Unlock()
67+
68+
return c.portionFilled()
69+
}
70+
71+
func (c *sizedLRU[K, V]) put(key K, value V) {
72+
valueSize := value.Size()
73+
if valueSize > c.maxSize {
74+
c.flush()
75+
return
76+
}
77+
78+
if oldValue, ok := c.elements.Get(key); ok {
79+
c.currentSize -= oldValue.Size()
80+
}
81+
82+
// Remove elements until the size of elements in the cache <= [c.maxSize].
83+
for c.currentSize > c.maxSize-valueSize {
84+
oldestKey, value, _ := c.elements.Oldest()
85+
c.elements.Delete(oldestKey)
86+
c.currentSize -= value.Size()
87+
}
88+
89+
c.elements.Put(key, value)
90+
c.currentSize += valueSize
91+
}
92+
93+
func (c *sizedLRU[K, V]) get(key K) (V, bool) {
94+
value, ok := c.elements.Get(key)
95+
if !ok {
96+
return utils.Zero[V](), false
97+
}
98+
99+
c.elements.Put(key, value) // Mark [k] as MRU.
100+
return value, true
101+
}
102+
103+
func (c *sizedLRU[K, _]) evict(key K) {
104+
if value, ok := c.elements.Get(key); ok {
105+
c.elements.Delete(key)
106+
c.currentSize -= value.Size()
107+
}
108+
}
109+
110+
func (c *sizedLRU[K, V]) flush() {
111+
c.elements = linkedhashmap.New[K, V]()
112+
c.currentSize = 0
113+
}
114+
115+
func (c *sizedLRU[_, _]) portionFilled() float64 {
116+
return float64(c.currentSize) / float64(c.maxSize)
117+
}

cache/lru_sized_cache_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved.
2+
// See the file LICENSE for licensing terms.
3+
4+
package cache
5+
6+
import (
7+
"testing"
8+
9+
"github.com/ava-labs/avalanchego/ids"
10+
)
11+
12+
func TestSizedLRU(t *testing.T) {
13+
cache := NewSizedLRU[ids.ID, TestSizedInt](TestSizedIntSize)
14+
15+
TestBasic(t, cache)
16+
}
17+
18+
func TestSizedLRUEviction(t *testing.T) {
19+
cache := NewSizedLRU[ids.ID, TestSizedInt](2 * TestSizedIntSize)
20+
21+
TestEviction(t, cache)
22+
}

cache/metercacher/cache.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ func (c *Cache[K, V]) Put(key K, value V) {
3333
c.Cacher.Put(key, value)
3434
end := c.clock.Time()
3535
c.put.Observe(float64(end.Sub(start)))
36+
c.portionFilled.Set(c.Cacher.PortionFilled())
3637
}
3738

3839
func (c *Cache[K, V]) Get(key K) (V, bool) {
@@ -48,3 +49,13 @@ func (c *Cache[K, V]) Get(key K) (V, bool) {
4849

4950
return value, has
5051
}
52+
53+
func (c *Cache[K, _]) Evict(key K) {
54+
c.Cacher.Evict(key)
55+
c.portionFilled.Set(c.Cacher.PortionFilled())
56+
}
57+
58+
func (c *Cache[_, _]) Flush() {
59+
c.Cacher.Flush()
60+
c.portionFilled.Set(c.Cacher.PortionFilled())
61+
}

cache/metercacher/cache_test.go

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,39 @@ import (
88

99
"github.com/prometheus/client_golang/prometheus"
1010

11+
"github.com/stretchr/testify/require"
12+
1113
"github.com/ava-labs/avalanchego/cache"
1214
"github.com/ava-labs/avalanchego/ids"
1315
)
1416

1517
func TestInterface(t *testing.T) {
16-
for _, test := range cache.CacherTests {
17-
cache := &cache.LRU[ids.ID, int]{Size: test.Size}
18-
c, err := New[ids.ID, int]("", prometheus.NewRegistry(), cache)
19-
if err != nil {
20-
t.Fatal(err)
21-
}
18+
type scenario struct {
19+
description string
20+
setup func(size int) cache.Cacher[ids.ID, cache.TestSizedInt]
21+
}
2222

23-
test.Func(t, c)
23+
scenarios := []scenario{
24+
{
25+
description: "cache LRU",
26+
setup: func(size int) cache.Cacher[ids.ID, cache.TestSizedInt] {
27+
return &cache.LRU[ids.ID, cache.TestSizedInt]{Size: size}
28+
},
29+
},
30+
{
31+
description: "sized cache LRU",
32+
setup: func(size int) cache.Cacher[ids.ID, cache.TestSizedInt] {
33+
return cache.NewSizedLRU[ids.ID, cache.TestSizedInt](size * cache.TestSizedIntSize)
34+
},
35+
},
36+
}
37+
38+
for _, scenario := range scenarios {
39+
for _, test := range cache.CacherTests {
40+
baseCache := scenario.setup(test.Size)
41+
c, err := New("", prometheus.NewRegistry(), baseCache)
42+
require.NoError(t, err)
43+
test.Func(t, c)
44+
}
2445
}
2546
}

cache/metercacher/metrics.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func newCounterMetric(namespace, name string, reg prometheus.Registerer, errs *w
3535
type metrics struct {
3636
get,
3737
put metric.Averager
38-
38+
portionFilled prometheus.Gauge
3939
hit,
4040
miss prometheus.Counter
4141
}
@@ -47,6 +47,14 @@ func (m *metrics) Initialize(
4747
errs := wrappers.Errs{}
4848
m.get = newAveragerMetric(namespace, "get", reg, &errs)
4949
m.put = newAveragerMetric(namespace, "put", reg, &errs)
50+
m.portionFilled = prometheus.NewGauge(
51+
prometheus.GaugeOpts{
52+
Namespace: namespace,
53+
Name: "portion_filled",
54+
Help: "fraction of cache filled",
55+
},
56+
)
57+
errs.Add(reg.Register(m.portionFilled))
5058
m.hit = newCounterMetric(namespace, "hit", reg, &errs)
5159
m.miss = newCounterMetric(namespace, "miss", reg, &errs)
5260
return errs.Err

0 commit comments

Comments
 (0)