Skip to content

Commit 3eaada9

Browse files
authored
Merge pull request #168 from kcalvinalvin/2024-04-22-move-nodesmap-to-utreexobackends-1
blockchain, utreexobackends: move nodesmapslice and cachedleavesmapslice to utreexobackends
2 parents df15689 + 8144ed9 commit 3eaada9

File tree

6 files changed

+621
-546
lines changed

6 files changed

+621
-546
lines changed
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
package utreexobackends
2+
3+
import (
4+
"sync"
5+
6+
"github.com/utreexo/utreexo"
7+
"github.com/utreexo/utreexod/blockchain/internal/sizehelper"
8+
"github.com/utreexo/utreexod/chaincfg/chainhash"
9+
)
10+
11+
const (
12+
// Bucket size for the cached leaves map.
13+
cachedLeavesMapBucketSize = 16 + sizehelper.Uint64Size*chainhash.HashSize + sizehelper.Uint64Size*sizehelper.Uint64Size
14+
)
15+
16+
// CachedLeavesMapSlice is a slice of maps for utxo entries. The slice of maps are needed to
17+
// guarantee that the map will only take up N amount of bytes. As of v1.20, the
18+
// go runtime will allocate 2^N + few extra buckets, meaning that for large N, we'll
19+
// allocate a lot of extra memory if the amount of entries goes over the previously
20+
// allocated buckets. A slice of maps allows us to have a better control of how much
21+
// total memory gets allocated by all the maps.
22+
type CachedLeavesMapSlice struct {
23+
// mtx protects against concurrent access for the map slice.
24+
mtx *sync.Mutex
25+
26+
// maps are the underlying maps in the slice of maps.
27+
maps []map[utreexo.Hash]uint64
28+
29+
// maxEntries is the maximum amount of elemnts that the map is allocated for.
30+
maxEntries []int
31+
32+
// maxTotalMemoryUsage is the maximum memory usage in bytes that the state
33+
// should contain in normal circumstances.
34+
maxTotalMemoryUsage uint64
35+
}
36+
37+
// Length returns the length of all the maps in the map slice added together.
38+
//
39+
// This function is safe for concurrent access.
40+
func (ms *CachedLeavesMapSlice) Length() int {
41+
ms.mtx.Lock()
42+
defer ms.mtx.Unlock()
43+
44+
var l int
45+
for _, m := range ms.maps {
46+
l += len(m)
47+
}
48+
49+
return l
50+
}
51+
52+
// Get looks for the outpoint in all the maps in the map slice and returns
53+
// the entry. nil and false is returned if the outpoint is not found.
54+
//
55+
// This function is safe for concurrent access.
56+
func (ms *CachedLeavesMapSlice) Get(k utreexo.Hash) (uint64, bool) {
57+
ms.mtx.Lock()
58+
defer ms.mtx.Unlock()
59+
60+
var v uint64
61+
var found bool
62+
63+
for _, m := range ms.maps {
64+
v, found = m[k]
65+
if found {
66+
return v, found
67+
}
68+
}
69+
70+
return 0, false
71+
}
72+
73+
// Put puts the keys and the values into one of the maps in the map slice. If the
74+
// existing maps are all full and it fails to put the entry in the cache, it will
75+
// return false.
76+
//
77+
// This function is safe for concurrent access.
78+
func (ms *CachedLeavesMapSlice) Put(k utreexo.Hash, v uint64) bool {
79+
ms.mtx.Lock()
80+
defer ms.mtx.Unlock()
81+
82+
for i := range ms.maxEntries {
83+
m := ms.maps[i]
84+
_, found := m[k]
85+
if found {
86+
m[k] = v
87+
return true
88+
}
89+
}
90+
91+
for i, maxNum := range ms.maxEntries {
92+
m := ms.maps[i]
93+
if len(m) >= maxNum {
94+
// Don't try to insert if the map already at max since
95+
// that'll force the map to allocate double the memory it's
96+
// currently taking up.
97+
continue
98+
}
99+
100+
m[k] = v
101+
return true // Return as we were successful in adding the entry.
102+
}
103+
104+
// We only reach this code if we've failed to insert into the map above as
105+
// all the current maps were full.
106+
return false
107+
}
108+
109+
// Delete attempts to delete the given outpoint in all of the maps. No-op if the
110+
// outpoint doesn't exist.
111+
//
112+
// This function is safe for concurrent access.
113+
func (ms *CachedLeavesMapSlice) Delete(k utreexo.Hash) {
114+
ms.mtx.Lock()
115+
defer ms.mtx.Unlock()
116+
117+
for i := 0; i < len(ms.maps); i++ {
118+
delete(ms.maps[i], k)
119+
}
120+
}
121+
122+
// DeleteMaps deletes all maps and allocate new ones with the maxEntries defined in
123+
// ms.maxEntries.
124+
//
125+
// This function is safe for concurrent access.
126+
func (ms *CachedLeavesMapSlice) DeleteMaps() {
127+
ms.mtx.Lock()
128+
defer ms.mtx.Unlock()
129+
130+
ms.maps = make([]map[utreexo.Hash]uint64, len(ms.maxEntries))
131+
for i := range ms.maxEntries {
132+
ms.maps[i] = make(map[utreexo.Hash]uint64, ms.maxEntries[i])
133+
}
134+
}
135+
136+
// ForEach loops through all the elements in the cachedleaves map slice and calls fn with the key-value pairs.
137+
//
138+
// This function is safe for concurrent access.
139+
func (ms *CachedLeavesMapSlice) ForEach(fn func(utreexo.Hash, uint64)) {
140+
ms.mtx.Lock()
141+
defer ms.mtx.Unlock()
142+
143+
for _, m := range ms.maps {
144+
for k, v := range m {
145+
fn(k, v)
146+
}
147+
}
148+
}
149+
150+
// createMaps creates a slice of maps and returns the total count that the maps
151+
// can handle. maxEntries are also set along with the newly created maps.
152+
func (ms *CachedLeavesMapSlice) createMaps(maxMemoryUsage int64) int64 {
153+
ms.mtx.Lock()
154+
defer ms.mtx.Unlock()
155+
156+
if maxMemoryUsage <= 0 {
157+
return 0
158+
}
159+
160+
// Get the entry count for the maps we'll allocate.
161+
var totalElemCount int
162+
ms.maxEntries, totalElemCount = sizehelper.CalcNumEntries(cachedLeavesMapBucketSize, maxMemoryUsage)
163+
164+
// maxMemoryUsage that's smaller than the minimum map size will return a totalElemCount
165+
// that's equal to 0.
166+
if totalElemCount <= 0 {
167+
return 0
168+
}
169+
170+
// Create the maps.
171+
ms.maps = make([]map[utreexo.Hash]uint64, len(ms.maxEntries))
172+
for i := range ms.maxEntries {
173+
ms.maps[i] = make(map[utreexo.Hash]uint64, ms.maxEntries[i])
174+
}
175+
176+
return int64(totalElemCount)
177+
}
178+
179+
// NewCachedLeavesMapSlice returns a new CachedLeavesMapSlice and the total amount of elements
180+
// that the map slice can accomodate.
181+
func NewCachedLeavesMapSlice(maxTotalMemoryUsage int64) (CachedLeavesMapSlice, int64) {
182+
ms := CachedLeavesMapSlice{
183+
mtx: new(sync.Mutex),
184+
maxTotalMemoryUsage: uint64(maxTotalMemoryUsage),
185+
}
186+
187+
totalCacheElem := ms.createMaps(maxTotalMemoryUsage)
188+
return ms, totalCacheElem
189+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package utreexobackends
2+
3+
import (
4+
"crypto/sha256"
5+
"encoding/binary"
6+
"testing"
7+
8+
"github.com/utreexo/utreexo"
9+
)
10+
11+
func TestCachedLeavesMapSliceMaxCacheElems(t *testing.T) {
12+
_, maxCacheElems := NewCachedLeavesMapSlice(0)
13+
if maxCacheElems != 0 {
14+
t.Fatalf("expected %v got %v", 0, maxCacheElems)
15+
}
16+
17+
_, maxCacheElems = NewCachedLeavesMapSlice(-1)
18+
if maxCacheElems != 0 {
19+
t.Fatalf("expected %v got %v", 0, maxCacheElems)
20+
}
21+
22+
_, maxCacheElems = NewCachedLeavesMapSlice(8000)
23+
if maxCacheElems <= 0 {
24+
t.Fatalf("expected something bigger than 0 but got %v", maxCacheElems)
25+
}
26+
}
27+
28+
func uint64ToHash(v uint64) utreexo.Hash {
29+
var buf [8]byte
30+
binary.LittleEndian.PutUint64(buf[:], v)
31+
return sha256.Sum256(buf[:])
32+
}
33+
34+
func TestCachedLeaveMapSliceDuplicates(t *testing.T) {
35+
m, maxElems := NewCachedLeavesMapSlice(8000)
36+
for i := 0; i < 10; i++ {
37+
for j := int64(0); j < maxElems; j++ {
38+
if !m.Put(uint64ToHash(uint64(j)), 0) {
39+
t.Fatalf("unexpected error on m.put")
40+
}
41+
}
42+
}
43+
44+
if m.Length() != int(maxElems) {
45+
t.Fatalf("expected length of %v but got %v",
46+
maxElems, m.Length())
47+
}
48+
49+
// Try inserting x which should be unique. Should fail as the map is full.
50+
x := uint64(0)
51+
x -= 1
52+
if m.Put(uint64ToHash(x), 0) {
53+
t.Fatalf("expected error but successfully called put")
54+
}
55+
56+
// Remove the first element in the first map and then try inserting
57+
// a duplicate element.
58+
m.Delete(uint64ToHash(0))
59+
x = uint64(maxElems) - 1
60+
if !m.Put(uint64ToHash(x), 0) {
61+
t.Fatalf("unexpected failure on put")
62+
}
63+
64+
// Make sure the length of the map is 1 less than the max elems.
65+
if m.Length() != int(maxElems)-1 {
66+
t.Fatalf("expected length of %v but got %v",
67+
maxElems-1, m.Length())
68+
}
69+
70+
// Put 0 back in and then compare the map.
71+
if !m.Put(uint64ToHash(0), 0) {
72+
t.Fatalf("didn't expect error but unsuccessfully called put")
73+
}
74+
if m.Length() != int(maxElems) {
75+
t.Fatalf("expected length of %v but got %v",
76+
maxElems, m.Length())
77+
}
78+
}

0 commit comments

Comments
 (0)