btcd/blockchain/sizehelper_test.go
Calvin Kim e318551538 blockchain: Add sizehelper
This change is part of the effort to add utxocache support to btcd.

sizehelper introduces code for 2 main things:
    1: Calculating how many entries to allocate for a map given a size
       in bytes.
    2: Calculating how much a map takes up in memory given the entries
       were allocated for the map.

These functionality are useful for allocating maps so that they'll be
allocating below a certain number of bytes.  Since go maps will always
allocate in powers of B (where B is the bucket size for the given map),
it may allocate too much memory.  For example, for a map that can store
8GB of entries, the map will grow to be 16GB once the map is full and
the caller puts an extra entry onto the map.

If we want to give a memory guarantee to the user, we can either:
    1: Limit the cache size to fixed sizes (4GB, 8GB, ...).
    2: Allocate a slice of maps.

The sizehelper code helps with (2).
2023-12-13 23:48:54 +09:00

71 lines
2.6 KiB
Go

// Copyright (c) 2023 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package blockchain
import (
"math"
"testing"
)
// calculateEntries returns a number of entries that will make the map allocate
// the given total bytes. The returned number is always the maximum number of
// entries that will allocate inside the given parameters.
func calculateEntries(totalBytes int, bucketSize int) int {
// 48 is the number of bytes needed for the map header in a
// 64 bit system. Refer to hmap in runtime/map.go in the go
// standard library.
totalBytes -= 48
numBuckets := totalBytes / bucketSize
B := uint8(math.Log2(float64(numBuckets)))
if B == 0 {
// For 0 buckets, the max is the bucket count.
return bucketCnt
}
return int(loadFactorNum * (bucketShift(B) / loadFactorDen))
}
func TestCalculateEntries(t *testing.T) {
for i := 0; i < 10_000_000; i++ {
// It's not possible to calculate the exact amount of entries since
// the map will only allocate for 2^N where N is the amount of buckets.
//
// So to see if the calculate entries function is working correctly,
// we get the rough map size for i entries, then calculate the entries
// for that map size. If the size is the same, the function is correct.
roughMapSize := calculateRoughMapSize(i, bucketSize)
entries := calculateEntries(roughMapSize, bucketSize)
gotRoughMapSize := calculateRoughMapSize(entries, bucketSize)
if roughMapSize != gotRoughMapSize {
t.Errorf("For hint of %d, expected %v, got %v\n",
i, roughMapSize, gotRoughMapSize)
}
// Test that the entries returned are the maximum for the given map size.
// If we increment the entries by one, we should get a bigger map.
gotRoughMapSizeWrong := calculateRoughMapSize(entries+1, bucketSize)
if roughMapSize == gotRoughMapSizeWrong {
t.Errorf("For hint %d incremented by 1, expected %v, got %v\n",
i, gotRoughMapSizeWrong*2, gotRoughMapSizeWrong)
}
minEntries := calculateMinEntries(roughMapSize, bucketSize)
gotMinRoughMapSize := calculateRoughMapSize(minEntries, bucketSize)
if roughMapSize != gotMinRoughMapSize {
t.Errorf("For hint of %d, expected %v, got %v\n",
i, roughMapSize, gotMinRoughMapSize)
}
// Can only test if they'll be half the size if the entries aren't 0.
if minEntries > 0 {
gotMinRoughMapSizeWrong := calculateRoughMapSize(minEntries-1, bucketSize)
if gotMinRoughMapSize == gotMinRoughMapSizeWrong {
t.Errorf("For hint %d decremented by 1, expected %v, got %v\n",
i, gotRoughMapSizeWrong/2, gotRoughMapSizeWrong)
}
}
}
}