1
0
mirror of https://github.com/MetaCubeX/mihomo.git synced 2025-09-21 04:55:57 +08:00
Files
mihomo/common/xsync/map_test.go
2025-07-15 13:39:03 +08:00

1733 lines
41 KiB
Go

package xsync
import (
"math"
"math/rand"
"strconv"
"sync"
"sync/atomic"
"testing"
"time"
"unsafe"
"github.com/metacubex/randv2"
)
const (
// number of entries to use in benchmarks
benchmarkNumEntries = 1_000
// key prefix used in benchmarks
benchmarkKeyPrefix = "what_a_looooooooooooooooooooooong_key_prefix_"
)
type point struct {
x int32
y int32
}
var benchmarkCases = []struct {
name string
readPercentage int
}{
{"reads=100%", 100}, // 100% loads, 0% stores, 0% deletes
{"reads=99%", 99}, // 99% loads, 0.5% stores, 0.5% deletes
{"reads=90%", 90}, // 90% loads, 5% stores, 5% deletes
{"reads=75%", 75}, // 75% loads, 12.5% stores, 12.5% deletes
}
var benchmarkKeys []string
func init() {
benchmarkKeys = make([]string, benchmarkNumEntries)
for i := 0; i < benchmarkNumEntries; i++ {
benchmarkKeys[i] = benchmarkKeyPrefix + strconv.Itoa(i)
}
}
func runParallel(b *testing.B, benchFn func(pb *testing.PB)) {
b.ResetTimer()
start := time.Now()
b.RunParallel(benchFn)
opsPerSec := float64(b.N) / float64(time.Since(start).Seconds())
b.ReportMetric(opsPerSec, "ops/s")
}
func TestMap_BucketStructSize(t *testing.T) {
size := unsafe.Sizeof(bucketPadded[string, int64]{})
if size != 64 {
t.Fatalf("size of 64B (one cache line) is expected, got: %d", size)
}
size = unsafe.Sizeof(bucketPadded[struct{}, int32]{})
if size != 64 {
t.Fatalf("size of 64B (one cache line) is expected, got: %d", size)
}
}
func TestMap_MissingEntry(t *testing.T) {
m := NewMap[string, string]()
v, ok := m.Load("foo")
if ok {
t.Fatalf("value was not expected: %v", v)
}
if deleted, loaded := m.LoadAndDelete("foo"); loaded {
t.Fatalf("value was not expected %v", deleted)
}
if actual, loaded := m.LoadOrStore("foo", "bar"); loaded {
t.Fatalf("value was not expected %v", actual)
}
}
func TestMap_EmptyStringKey(t *testing.T) {
m := NewMap[string, string]()
m.Store("", "foobar")
v, ok := m.Load("")
if !ok {
t.Fatal("value was expected")
}
if v != "foobar" {
t.Fatalf("value does not match: %v", v)
}
}
func TestMapStore_NilValue(t *testing.T) {
m := NewMap[string, *struct{}]()
m.Store("foo", nil)
v, ok := m.Load("foo")
if !ok {
t.Fatal("nil value was expected")
}
if v != nil {
t.Fatalf("value was not nil: %v", v)
}
}
func TestMapLoadOrStore_NilValue(t *testing.T) {
m := NewMap[string, *struct{}]()
m.LoadOrStore("foo", nil)
v, loaded := m.LoadOrStore("foo", nil)
if !loaded {
t.Fatal("nil value was expected")
}
if v != nil {
t.Fatalf("value was not nil: %v", v)
}
}
func TestMapLoadOrStore_NonNilValue(t *testing.T) {
type foo struct{}
m := NewMap[string, *foo]()
newv := &foo{}
v, loaded := m.LoadOrStore("foo", newv)
if loaded {
t.Fatal("no value was expected")
}
if v != newv {
t.Fatalf("value does not match: %v", v)
}
newv2 := &foo{}
v, loaded = m.LoadOrStore("foo", newv2)
if !loaded {
t.Fatal("value was expected")
}
if v != newv {
t.Fatalf("value does not match: %v", v)
}
}
func TestMapLoadAndStore_NilValue(t *testing.T) {
m := NewMap[string, *struct{}]()
m.LoadAndStore("foo", nil)
v, loaded := m.LoadAndStore("foo", nil)
if !loaded {
t.Fatal("nil value was expected")
}
if v != nil {
t.Fatalf("value was not nil: %v", v)
}
v, loaded = m.Load("foo")
if !loaded {
t.Fatal("nil value was expected")
}
if v != nil {
t.Fatalf("value was not nil: %v", v)
}
}
func TestMapLoadAndStore_NonNilValue(t *testing.T) {
m := NewMap[string, int]()
v1 := 1
v, loaded := m.LoadAndStore("foo", v1)
if loaded {
t.Fatal("no value was expected")
}
if v != v1 {
t.Fatalf("value does not match: %v", v)
}
v2 := 2
v, loaded = m.LoadAndStore("foo", v2)
if !loaded {
t.Fatal("value was expected")
}
if v != v1 {
t.Fatalf("value does not match: %v", v)
}
v, loaded = m.Load("foo")
if !loaded {
t.Fatal("value was expected")
}
if v != v2 {
t.Fatalf("value does not match: %v", v)
}
}
func TestMapRange(t *testing.T) {
const numEntries = 1000
m := NewMap[string, int]()
for i := 0; i < numEntries; i++ {
m.Store(strconv.Itoa(i), i)
}
iters := 0
met := make(map[string]int)
m.Range(func(key string, value int) bool {
if key != strconv.Itoa(value) {
t.Fatalf("got unexpected key/value for iteration %d: %v/%v", iters, key, value)
return false
}
met[key] += 1
iters++
return true
})
if iters != numEntries {
t.Fatalf("got unexpected number of iterations: %d", iters)
}
for i := 0; i < numEntries; i++ {
if c := met[strconv.Itoa(i)]; c != 1 {
t.Fatalf("range did not iterate correctly over %d: %d", i, c)
}
}
}
func TestMapRange_FalseReturned(t *testing.T) {
m := NewMap[string, int]()
for i := 0; i < 100; i++ {
m.Store(strconv.Itoa(i), i)
}
iters := 0
m.Range(func(key string, value int) bool {
iters++
return iters != 13
})
if iters != 13 {
t.Fatalf("got unexpected number of iterations: %d", iters)
}
}
func TestMapRange_NestedDelete(t *testing.T) {
const numEntries = 256
m := NewMap[string, int]()
for i := 0; i < numEntries; i++ {
m.Store(strconv.Itoa(i), i)
}
m.Range(func(key string, value int) bool {
m.Delete(key)
return true
})
for i := 0; i < numEntries; i++ {
if _, ok := m.Load(strconv.Itoa(i)); ok {
t.Fatalf("value found for %d", i)
}
}
}
func TestMapStringStore(t *testing.T) {
const numEntries = 128
m := NewMap[string, int]()
for i := 0; i < numEntries; i++ {
m.Store(strconv.Itoa(i), i)
}
for i := 0; i < numEntries; i++ {
v, ok := m.Load(strconv.Itoa(i))
if !ok {
t.Fatalf("value not found for %d", i)
}
if v != i {
t.Fatalf("values do not match for %d: %v", i, v)
}
}
}
func TestMapIntStore(t *testing.T) {
const numEntries = 128
m := NewMap[int, int]()
for i := 0; i < numEntries; i++ {
m.Store(i, i)
}
for i := 0; i < numEntries; i++ {
v, ok := m.Load(i)
if !ok {
t.Fatalf("value not found for %d", i)
}
if v != i {
t.Fatalf("values do not match for %d: %v", i, v)
}
}
}
func TestMapStore_StructKeys_IntValues(t *testing.T) {
const numEntries = 128
m := NewMap[point, int]()
for i := 0; i < numEntries; i++ {
m.Store(point{int32(i), -int32(i)}, i)
}
for i := 0; i < numEntries; i++ {
v, ok := m.Load(point{int32(i), -int32(i)})
if !ok {
t.Fatalf("value not found for %d", i)
}
if v != i {
t.Fatalf("values do not match for %d: %v", i, v)
}
}
}
func TestMapStore_StructKeys_StructValues(t *testing.T) {
const numEntries = 128
m := NewMap[point, point]()
for i := 0; i < numEntries; i++ {
m.Store(point{int32(i), -int32(i)}, point{-int32(i), int32(i)})
}
for i := 0; i < numEntries; i++ {
v, ok := m.Load(point{int32(i), -int32(i)})
if !ok {
t.Fatalf("value not found for %d", i)
}
if v.x != -int32(i) {
t.Fatalf("x value does not match for %d: %v", i, v)
}
if v.y != int32(i) {
t.Fatalf("y value does not match for %d: %v", i, v)
}
}
}
func TestMapLoadOrStore(t *testing.T) {
const numEntries = 1000
m := NewMap[string, int]()
for i := 0; i < numEntries; i++ {
m.Store(strconv.Itoa(i), i)
}
for i := 0; i < numEntries; i++ {
if _, loaded := m.LoadOrStore(strconv.Itoa(i), i); !loaded {
t.Fatalf("value not found for %d", i)
}
}
}
func TestMapLoadOrCompute(t *testing.T) {
const numEntries = 1000
m := NewMap[string, int]()
for i := 0; i < numEntries; i++ {
v, loaded := m.LoadOrCompute(strconv.Itoa(i), func() (newValue int, cancel bool) {
return i, true
})
if loaded {
t.Fatalf("value not computed for %d", i)
}
if v != 0 {
t.Fatalf("values do not match for %d: %v", i, v)
}
}
if m.Size() != 0 {
t.Fatalf("zero map size expected: %d", m.Size())
}
for i := 0; i < numEntries; i++ {
v, loaded := m.LoadOrCompute(strconv.Itoa(i), func() (newValue int, cancel bool) {
return i, false
})
if loaded {
t.Fatalf("value not computed for %d", i)
}
if v != i {
t.Fatalf("values do not match for %d: %v", i, v)
}
}
for i := 0; i < numEntries; i++ {
v, loaded := m.LoadOrCompute(strconv.Itoa(i), func() (newValue int, cancel bool) {
t.Fatalf("value func invoked")
return newValue, false
})
if !loaded {
t.Fatalf("value not loaded for %d", i)
}
if v != i {
t.Fatalf("values do not match for %d: %v", i, v)
}
}
}
func TestMapLoadOrCompute_FunctionCalledOnce(t *testing.T) {
m := NewMap[int, int]()
for i := 0; i < 100; {
m.LoadOrCompute(i, func() (newValue int, cancel bool) {
newValue, i = i, i+1
return newValue, false
})
}
m.Range(func(k, v int) bool {
if k != v {
t.Fatalf("%dth key is not equal to value %d", k, v)
}
return true
})
}
func TestMapOfCompute(t *testing.T) {
m := NewMap[string, int]()
// Store a new value.
v, ok := m.Compute("foobar", func(oldValue int, loaded bool) (newValue int, op ComputeOp) {
if oldValue != 0 {
t.Fatalf("oldValue should be 0 when computing a new value: %d", oldValue)
}
if loaded {
t.Fatal("loaded should be false when computing a new value")
}
newValue = 42
op = UpdateOp
return
})
if v != 42 {
t.Fatalf("v should be 42 when computing a new value: %d", v)
}
if !ok {
t.Fatal("ok should be true when computing a new value")
}
// Update an existing value.
v, ok = m.Compute("foobar", func(oldValue int, loaded bool) (newValue int, op ComputeOp) {
if oldValue != 42 {
t.Fatalf("oldValue should be 42 when updating the value: %d", oldValue)
}
if !loaded {
t.Fatal("loaded should be true when updating the value")
}
newValue = oldValue + 42
op = UpdateOp
return
})
if v != 84 {
t.Fatalf("v should be 84 when updating the value: %d", v)
}
if !ok {
t.Fatal("ok should be true when updating the value")
}
// Check that NoOp doesn't update the value
v, ok = m.Compute("foobar", func(oldValue int, loaded bool) (newValue int, op ComputeOp) {
return 0, CancelOp
})
if v != 84 {
t.Fatalf("v should be 84 after using NoOp: %d", v)
}
if !ok {
t.Fatal("ok should be true when updating the value")
}
// Delete an existing value.
v, ok = m.Compute("foobar", func(oldValue int, loaded bool) (newValue int, op ComputeOp) {
if oldValue != 84 {
t.Fatalf("oldValue should be 84 when deleting the value: %d", oldValue)
}
if !loaded {
t.Fatal("loaded should be true when deleting the value")
}
op = DeleteOp
return
})
if v != 84 {
t.Fatalf("v should be 84 when deleting the value: %d", v)
}
if ok {
t.Fatal("ok should be false when deleting the value")
}
// Try to delete a non-existing value. Notice different key.
v, ok = m.Compute("barbaz", func(oldValue int, loaded bool) (newValue int, op ComputeOp) {
if oldValue != 0 {
t.Fatalf("oldValue should be 0 when trying to delete a non-existing value: %d", oldValue)
}
if loaded {
t.Fatal("loaded should be false when trying to delete a non-existing value")
}
// We're returning a non-zero value, but the map should ignore it.
newValue = 42
op = DeleteOp
return
})
if v != 0 {
t.Fatalf("v should be 0 when trying to delete a non-existing value: %d", v)
}
if ok {
t.Fatal("ok should be false when trying to delete a non-existing value")
}
// Try NoOp on a non-existing value
v, ok = m.Compute("barbaz", func(oldValue int, loaded bool) (newValue int, op ComputeOp) {
if oldValue != 0 {
t.Fatalf("oldValue should be 0 when trying to delete a non-existing value: %d", oldValue)
}
if loaded {
t.Fatal("loaded should be false when trying to delete a non-existing value")
}
// We're returning a non-zero value, but the map should ignore it.
newValue = 42
op = CancelOp
return
})
if v != 0 {
t.Fatalf("v should be 0 when trying to delete a non-existing value: %d", v)
}
if ok {
t.Fatal("ok should be false when trying to delete a non-existing value")
}
}
func TestMapStringStoreThenDelete(t *testing.T) {
const numEntries = 1000
m := NewMap[string, int]()
for i := 0; i < numEntries; i++ {
m.Store(strconv.Itoa(i), i)
}
for i := 0; i < numEntries; i++ {
m.Delete(strconv.Itoa(i))
if _, ok := m.Load(strconv.Itoa(i)); ok {
t.Fatalf("value was not expected for %d", i)
}
}
}
func TestMapIntStoreThenDelete(t *testing.T) {
const numEntries = 1000
m := NewMap[int32, int32]()
for i := 0; i < numEntries; i++ {
m.Store(int32(i), int32(i))
}
for i := 0; i < numEntries; i++ {
m.Delete(int32(i))
if _, ok := m.Load(int32(i)); ok {
t.Fatalf("value was not expected for %d", i)
}
}
}
func TestMapStructStoreThenDelete(t *testing.T) {
const numEntries = 1000
m := NewMap[point, string]()
for i := 0; i < numEntries; i++ {
m.Store(point{int32(i), 42}, strconv.Itoa(i))
}
for i := 0; i < numEntries; i++ {
m.Delete(point{int32(i), 42})
if _, ok := m.Load(point{int32(i), 42}); ok {
t.Fatalf("value was not expected for %d", i)
}
}
}
func TestMapStringStoreThenLoadAndDelete(t *testing.T) {
const numEntries = 1000
m := NewMap[string, int]()
for i := 0; i < numEntries; i++ {
m.Store(strconv.Itoa(i), i)
}
for i := 0; i < numEntries; i++ {
if v, loaded := m.LoadAndDelete(strconv.Itoa(i)); !loaded || v != i {
t.Fatalf("value was not found or different for %d: %v", i, v)
}
if _, ok := m.Load(strconv.Itoa(i)); ok {
t.Fatalf("value was not expected for %d", i)
}
}
}
func TestMapIntStoreThenLoadAndDelete(t *testing.T) {
const numEntries = 1000
m := NewMap[int, int]()
for i := 0; i < numEntries; i++ {
m.Store(i, i)
}
for i := 0; i < numEntries; i++ {
if _, loaded := m.LoadAndDelete(i); !loaded {
t.Fatalf("value was not found for %d", i)
}
if _, ok := m.Load(i); ok {
t.Fatalf("value was not expected for %d", i)
}
}
}
func TestMapStructStoreThenLoadAndDelete(t *testing.T) {
const numEntries = 1000
m := NewMap[point, int]()
for i := 0; i < numEntries; i++ {
m.Store(point{42, int32(i)}, i)
}
for i := 0; i < numEntries; i++ {
if _, loaded := m.LoadAndDelete(point{42, int32(i)}); !loaded {
t.Fatalf("value was not found for %d", i)
}
if _, ok := m.Load(point{42, int32(i)}); ok {
t.Fatalf("value was not expected for %d", i)
}
}
}
func TestMapStoreThenParallelDelete_DoesNotShrinkBelowMinTableLen(t *testing.T) {
const numEntries = 1000
m := NewMap[int, int]()
for i := 0; i < numEntries; i++ {
m.Store(i, i)
}
cdone := make(chan bool)
go func() {
for i := 0; i < numEntries; i++ {
m.Delete(i)
}
cdone <- true
}()
go func() {
for i := 0; i < numEntries; i++ {
m.Delete(i)
}
cdone <- true
}()
// Wait for the goroutines to finish.
<-cdone
<-cdone
stats := m.Stats()
if stats.RootBuckets != defaultMinMapTableLen {
t.Fatalf("table length was different from the minimum: %d", stats.RootBuckets)
}
}
func sizeBasedOnTypedRange(m *Map[string, int]) int {
size := 0
m.Range(func(key string, value int) bool {
size++
return true
})
return size
}
func TestMapSize(t *testing.T) {
const numEntries = 1000
m := NewMap[string, int]()
size := m.Size()
if size != 0 {
t.Fatalf("zero size expected: %d", size)
}
expectedSize := 0
for i := 0; i < numEntries; i++ {
m.Store(strconv.Itoa(i), i)
expectedSize++
size := m.Size()
if size != expectedSize {
t.Fatalf("size of %d was expected, got: %d", expectedSize, size)
}
rsize := sizeBasedOnTypedRange(m)
if size != rsize {
t.Fatalf("size does not match number of entries in Range: %v, %v", size, rsize)
}
}
for i := 0; i < numEntries; i++ {
m.Delete(strconv.Itoa(i))
expectedSize--
size := m.Size()
if size != expectedSize {
t.Fatalf("size of %d was expected, got: %d", expectedSize, size)
}
rsize := sizeBasedOnTypedRange(m)
if size != rsize {
t.Fatalf("size does not match number of entries in Range: %v, %v", size, rsize)
}
}
}
func TestMapClear(t *testing.T) {
const numEntries = 1000
m := NewMap[string, int]()
for i := 0; i < numEntries; i++ {
m.Store(strconv.Itoa(i), i)
}
size := m.Size()
if size != numEntries {
t.Fatalf("size of %d was expected, got: %d", numEntries, size)
}
m.Clear()
size = m.Size()
if size != 0 {
t.Fatalf("zero size was expected, got: %d", size)
}
rsize := sizeBasedOnTypedRange(m)
if rsize != 0 {
t.Fatalf("zero number of entries in Range was expected, got: %d", rsize)
}
}
func assertMapCapacity[K comparable, V any](t *testing.T, m *Map[K, V], expectedCap int) {
stats := m.Stats()
if stats.Capacity != expectedCap {
t.Fatalf("capacity was different from %d: %d", expectedCap, stats.Capacity)
}
}
func TestNewMapWithPresize(t *testing.T) {
assertMapCapacity(t, NewMap[string, string](), defaultMinMapTableLen*entriesPerMapBucket)
assertMapCapacity(t, NewMap[string, string](WithPresize(0)), defaultMinMapTableLen*entriesPerMapBucket)
assertMapCapacity(t, NewMap[string, string](WithPresize(-100)), defaultMinMapTableLen*entriesPerMapBucket)
assertMapCapacity(t, NewMap[string, string](WithPresize(500)), 1280)
assertMapCapacity(t, NewMap[int, int](WithPresize(1_000_000)), 2621440)
assertMapCapacity(t, NewMap[point, point](WithPresize(100)), 160)
}
func TestNewMapWithPresize_DoesNotShrinkBelowMinTableLen(t *testing.T) {
const minTableLen = 1024
const numEntries = int(minTableLen * entriesPerMapBucket * mapLoadFactor)
m := NewMap[int, int](WithPresize(numEntries))
for i := 0; i < 2*numEntries; i++ {
m.Store(i, i)
}
stats := m.Stats()
if stats.RootBuckets <= minTableLen {
t.Fatalf("table did not grow: %d", stats.RootBuckets)
}
for i := 0; i < 2*numEntries; i++ {
m.Delete(i)
}
stats = m.Stats()
if stats.RootBuckets != minTableLen {
t.Fatalf("table length was different from the minimum: %d", stats.RootBuckets)
}
}
func TestNewMapGrowOnly_OnlyShrinksOnClear(t *testing.T) {
const minTableLen = 128
const numEntries = minTableLen * entriesPerMapBucket
m := NewMap[int, int](WithPresize(numEntries), WithGrowOnly())
stats := m.Stats()
initialTableLen := stats.RootBuckets
for i := 0; i < 2*numEntries; i++ {
m.Store(i, i)
}
stats = m.Stats()
maxTableLen := stats.RootBuckets
if maxTableLen <= minTableLen {
t.Fatalf("table did not grow: %d", maxTableLen)
}
for i := 0; i < numEntries; i++ {
m.Delete(i)
}
stats = m.Stats()
if stats.RootBuckets != maxTableLen {
t.Fatalf("table length was different from the expected: %d", stats.RootBuckets)
}
m.Clear()
stats = m.Stats()
if stats.RootBuckets != initialTableLen {
t.Fatalf("table length was different from the initial: %d", stats.RootBuckets)
}
}
func TestMapResize(t *testing.T) {
testMapResize(t, NewMap[string, int]())
}
func testMapResize(t *testing.T, m *Map[string, int]) {
const numEntries = 100_000
for i := 0; i < numEntries; i++ {
m.Store(strconv.Itoa(i), i)
}
stats := m.Stats()
if stats.Size != numEntries {
t.Fatalf("size was too small: %d", stats.Size)
}
expectedCapacity := int(math.RoundToEven(mapLoadFactor+1)) * stats.RootBuckets * entriesPerMapBucket
if stats.Capacity > expectedCapacity {
t.Fatalf("capacity was too large: %d, expected: %d", stats.Capacity, expectedCapacity)
}
if stats.RootBuckets <= defaultMinMapTableLen {
t.Fatalf("table was too small: %d", stats.RootBuckets)
}
if stats.TotalGrowths == 0 {
t.Fatalf("non-zero total growths expected: %d", stats.TotalGrowths)
}
if stats.TotalShrinks > 0 {
t.Fatalf("zero total shrinks expected: %d", stats.TotalShrinks)
}
// This is useful when debugging table resize and occupancy.
// Use -v flag to see the output.
t.Log(stats.ToString())
for i := 0; i < numEntries; i++ {
m.Delete(strconv.Itoa(i))
}
stats = m.Stats()
if stats.Size > 0 {
t.Fatalf("zero size was expected: %d", stats.Size)
}
expectedCapacity = stats.RootBuckets * entriesPerMapBucket
if stats.Capacity != expectedCapacity {
t.Fatalf("capacity was too large: %d, expected: %d", stats.Capacity, expectedCapacity)
}
if stats.RootBuckets != defaultMinMapTableLen {
t.Fatalf("table was too large: %d", stats.RootBuckets)
}
if stats.TotalShrinks == 0 {
t.Fatalf("non-zero total shrinks expected: %d", stats.TotalShrinks)
}
t.Log(stats.ToString())
}
func TestMapResize_CounterLenLimit(t *testing.T) {
const numEntries = 1_000_000
m := NewMap[string, string]()
for i := 0; i < numEntries; i++ {
m.Store("foo"+strconv.Itoa(i), "bar"+strconv.Itoa(i))
}
stats := m.Stats()
if stats.Size != numEntries {
t.Fatalf("size was too small: %d", stats.Size)
}
if stats.CounterLen != maxMapCounterLen {
t.Fatalf("number of counter stripes was too large: %d, expected: %d",
stats.CounterLen, maxMapCounterLen)
}
}
func parallelSeqMapGrower(m *Map[int, int], numEntries int, positive bool, cdone chan bool) {
for i := 0; i < numEntries; i++ {
if positive {
m.Store(i, i)
} else {
m.Store(-i, -i)
}
}
cdone <- true
}
func TestMapParallelGrowth_GrowOnly(t *testing.T) {
const numEntries = 100_000
m := NewMap[int, int]()
cdone := make(chan bool)
go parallelSeqMapGrower(m, numEntries, true, cdone)
go parallelSeqMapGrower(m, numEntries, false, cdone)
// Wait for the goroutines to finish.
<-cdone
<-cdone
// Verify map contents.
for i := -numEntries + 1; i < numEntries; i++ {
v, ok := m.Load(i)
if !ok {
t.Fatalf("value not found for %d", i)
}
if v != i {
t.Fatalf("values do not match for %d: %v", i, v)
}
}
if s := m.Size(); s != 2*numEntries-1 {
t.Fatalf("unexpected size: %v", s)
}
}
func parallelRandMapResizer(t *testing.T, m *Map[string, int], numIters, numEntries int, cdone chan bool) {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
for i := 0; i < numIters; i++ {
coin := r.Int63n(2)
for j := 0; j < numEntries; j++ {
if coin == 1 {
m.Store(strconv.Itoa(j), j)
} else {
m.Delete(strconv.Itoa(j))
}
}
}
cdone <- true
}
func TestMapParallelGrowth(t *testing.T) {
const numIters = 1_000
const numEntries = 2 * entriesPerMapBucket * defaultMinMapTableLen
m := NewMap[string, int]()
cdone := make(chan bool)
go parallelRandMapResizer(t, m, numIters, numEntries, cdone)
go parallelRandMapResizer(t, m, numIters, numEntries, cdone)
// Wait for the goroutines to finish.
<-cdone
<-cdone
// Verify map contents.
for i := 0; i < numEntries; i++ {
v, ok := m.Load(strconv.Itoa(i))
if !ok {
// The entry may be deleted and that's ok.
continue
}
if v != i {
t.Fatalf("values do not match for %d: %v", i, v)
}
}
s := m.Size()
if s > numEntries {
t.Fatalf("unexpected size: %v", s)
}
rs := sizeBasedOnTypedRange(m)
if s != rs {
t.Fatalf("size does not match number of entries in Range: %v, %v", s, rs)
}
}
func parallelRandMapClearer(t *testing.T, m *Map[string, int], numIters, numEntries int, cdone chan bool) {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
for i := 0; i < numIters; i++ {
coin := r.Int63n(2)
for j := 0; j < numEntries; j++ {
if coin == 1 {
m.Store(strconv.Itoa(j), j)
} else {
m.Clear()
}
}
}
cdone <- true
}
func TestMapParallelClear(t *testing.T) {
const numIters = 100
const numEntries = 1_000
m := NewMap[string, int]()
cdone := make(chan bool)
go parallelRandMapClearer(t, m, numIters, numEntries, cdone)
go parallelRandMapClearer(t, m, numIters, numEntries, cdone)
// Wait for the goroutines to finish.
<-cdone
<-cdone
// Verify map size.
s := m.Size()
if s > numEntries {
t.Fatalf("unexpected size: %v", s)
}
rs := sizeBasedOnTypedRange(m)
if s != rs {
t.Fatalf("size does not match number of entries in Range: %v, %v", s, rs)
}
}
func parallelSeqMapStorer(t *testing.T, m *Map[string, int], storeEach, numIters, numEntries int, cdone chan bool) {
for i := 0; i < numIters; i++ {
for j := 0; j < numEntries; j++ {
if storeEach == 0 || j%storeEach == 0 {
m.Store(strconv.Itoa(j), j)
// Due to atomic snapshots we must see a "<j>"/j pair.
v, ok := m.Load(strconv.Itoa(j))
if !ok {
t.Errorf("value was not found for %d", j)
break
}
if v != j {
t.Errorf("value was not expected for %d: %d", j, v)
break
}
}
}
}
cdone <- true
}
func TestMapParallelStores(t *testing.T) {
const numStorers = 4
const numIters = 10_000
const numEntries = 100
m := NewMap[string, int]()
cdone := make(chan bool)
for i := 0; i < numStorers; i++ {
go parallelSeqMapStorer(t, m, i, numIters, numEntries, cdone)
}
// Wait for the goroutines to finish.
for i := 0; i < numStorers; i++ {
<-cdone
}
// Verify map contents.
for i := 0; i < numEntries; i++ {
v, ok := m.Load(strconv.Itoa(i))
if !ok {
t.Fatalf("value not found for %d", i)
}
if v != i {
t.Fatalf("values do not match for %d: %v", i, v)
}
}
}
func parallelRandMapStorer(t *testing.T, m *Map[string, int], numIters, numEntries int, cdone chan bool) {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
for i := 0; i < numIters; i++ {
j := r.Intn(numEntries)
if v, loaded := m.LoadOrStore(strconv.Itoa(j), j); loaded {
if v != j {
t.Errorf("value was not expected for %d: %d", j, v)
}
}
}
cdone <- true
}
func parallelRandMapDeleter(t *testing.T, m *Map[string, int], numIters, numEntries int, cdone chan bool) {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
for i := 0; i < numIters; i++ {
j := r.Intn(numEntries)
if v, loaded := m.LoadAndDelete(strconv.Itoa(j)); loaded {
if v != j {
t.Errorf("value was not expected for %d: %d", j, v)
}
}
}
cdone <- true
}
func parallelMapLoader(t *testing.T, m *Map[string, int], numIters, numEntries int, cdone chan bool) {
for i := 0; i < numIters; i++ {
for j := 0; j < numEntries; j++ {
// Due to atomic snapshots we must either see no entry, or a "<j>"/j pair.
if v, ok := m.Load(strconv.Itoa(j)); ok {
if v != j {
t.Errorf("value was not expected for %d: %d", j, v)
}
}
}
}
cdone <- true
}
func TestMapAtomicSnapshot(t *testing.T) {
const numIters = 100_000
const numEntries = 100
m := NewMap[string, int]()
cdone := make(chan bool)
// Update or delete random entry in parallel with loads.
go parallelRandMapStorer(t, m, numIters, numEntries, cdone)
go parallelRandMapDeleter(t, m, numIters, numEntries, cdone)
go parallelMapLoader(t, m, numIters, numEntries, cdone)
// Wait for the goroutines to finish.
for i := 0; i < 3; i++ {
<-cdone
}
}
func TestMapParallelStoresAndDeletes(t *testing.T) {
const numWorkers = 2
const numIters = 100_000
const numEntries = 1000
m := NewMap[string, int]()
cdone := make(chan bool)
// Update random entry in parallel with deletes.
for i := 0; i < numWorkers; i++ {
go parallelRandMapStorer(t, m, numIters, numEntries, cdone)
go parallelRandMapDeleter(t, m, numIters, numEntries, cdone)
}
// Wait for the goroutines to finish.
for i := 0; i < 2*numWorkers; i++ {
<-cdone
}
}
func parallelMapComputer(m *Map[uint64, uint64], numIters, numEntries int, cdone chan bool) {
for i := 0; i < numIters; i++ {
for j := 0; j < numEntries; j++ {
m.Compute(uint64(j), func(oldValue uint64, loaded bool) (newValue uint64, op ComputeOp) {
return oldValue + 1, UpdateOp
})
}
}
cdone <- true
}
func TestMapParallelComputes(t *testing.T) {
const numWorkers = 4 // Also stands for numEntries.
const numIters = 10_000
m := NewMap[uint64, uint64]()
cdone := make(chan bool)
for i := 0; i < numWorkers; i++ {
go parallelMapComputer(m, numIters, numWorkers, cdone)
}
// Wait for the goroutines to finish.
for i := 0; i < numWorkers; i++ {
<-cdone
}
// Verify map contents.
for i := 0; i < numWorkers; i++ {
v, ok := m.Load(uint64(i))
if !ok {
t.Fatalf("value not found for %d", i)
}
if v != numWorkers*numIters {
t.Fatalf("values do not match for %d: %v", i, v)
}
}
}
func parallelRangeMapStorer(m *Map[int, int], numEntries int, stopFlag *int64, cdone chan bool) {
for {
for i := 0; i < numEntries; i++ {
m.Store(i, i)
}
if atomic.LoadInt64(stopFlag) != 0 {
break
}
}
cdone <- true
}
func parallelRangeMapDeleter(m *Map[int, int], numEntries int, stopFlag *int64, cdone chan bool) {
for {
for i := 0; i < numEntries; i++ {
m.Delete(i)
}
if atomic.LoadInt64(stopFlag) != 0 {
break
}
}
cdone <- true
}
func TestMapParallelRange(t *testing.T) {
const numEntries = 10_000
m := NewMap[int, int](WithPresize(numEntries))
for i := 0; i < numEntries; i++ {
m.Store(i, i)
}
// Start goroutines that would be storing and deleting items in parallel.
cdone := make(chan bool)
stopFlag := int64(0)
go parallelRangeMapStorer(m, numEntries, &stopFlag, cdone)
go parallelRangeMapDeleter(m, numEntries, &stopFlag, cdone)
// Iterate the map and verify that no duplicate keys were met.
met := make(map[int]int)
m.Range(func(key int, value int) bool {
if key != value {
t.Fatalf("got unexpected value for key %d: %d", key, value)
return false
}
met[key] += 1
return true
})
if len(met) == 0 {
t.Fatal("no entries were met when iterating")
}
for k, c := range met {
if c != 1 {
t.Fatalf("met key %d multiple times: %d", k, c)
}
}
// Make sure that both goroutines finish.
atomic.StoreInt64(&stopFlag, 1)
<-cdone
<-cdone
}
func parallelMapShrinker(t *testing.T, m *Map[uint64, *point], numIters, numEntries int, stopFlag *int64, cdone chan bool) {
for i := 0; i < numIters; i++ {
for j := 0; j < numEntries; j++ {
if p, loaded := m.LoadOrStore(uint64(j), &point{int32(j), int32(j)}); loaded {
t.Errorf("value was present for %d: %v", j, p)
}
}
for j := 0; j < numEntries; j++ {
m.Delete(uint64(j))
}
}
atomic.StoreInt64(stopFlag, 1)
cdone <- true
}
func parallelMapUpdater(t *testing.T, m *Map[uint64, *point], idx int, stopFlag *int64, cdone chan bool) {
for atomic.LoadInt64(stopFlag) != 1 {
sleepUs := int(randv2.Uint64() % 10)
if p, loaded := m.LoadOrStore(uint64(idx), &point{int32(idx), int32(idx)}); loaded {
t.Errorf("value was present for %d: %v", idx, p)
}
time.Sleep(time.Duration(sleepUs) * time.Microsecond)
if _, ok := m.Load(uint64(idx)); !ok {
t.Errorf("value was not found for %d", idx)
}
m.Delete(uint64(idx))
}
cdone <- true
}
func TestMapDoesNotLoseEntriesOnResize(t *testing.T) {
const numIters = 10_000
const numEntries = 128
m := NewMap[uint64, *point]()
cdone := make(chan bool)
stopFlag := int64(0)
go parallelMapShrinker(t, m, numIters, numEntries, &stopFlag, cdone)
go parallelMapUpdater(t, m, numEntries, &stopFlag, cdone)
// Wait for the goroutines to finish.
<-cdone
<-cdone
// Verify map contents.
if s := m.Size(); s != 0 {
t.Fatalf("map is not empty: %d", s)
}
}
func TestMapStats(t *testing.T) {
m := NewMap[int, int]()
stats := m.Stats()
if stats.RootBuckets != defaultMinMapTableLen {
t.Fatalf("unexpected number of root buckets: %d", stats.RootBuckets)
}
if stats.TotalBuckets != stats.RootBuckets {
t.Fatalf("unexpected number of total buckets: %d", stats.TotalBuckets)
}
if stats.EmptyBuckets != stats.RootBuckets {
t.Fatalf("unexpected number of empty buckets: %d", stats.EmptyBuckets)
}
if stats.Capacity != entriesPerMapBucket*defaultMinMapTableLen {
t.Fatalf("unexpected capacity: %d", stats.Capacity)
}
if stats.Size != 0 {
t.Fatalf("unexpected size: %d", stats.Size)
}
if stats.Counter != 0 {
t.Fatalf("unexpected counter: %d", stats.Counter)
}
if stats.CounterLen != 8 {
t.Fatalf("unexpected counter length: %d", stats.CounterLen)
}
for i := 0; i < 200; i++ {
m.Store(i, i)
}
stats = m.Stats()
if stats.RootBuckets != 2*defaultMinMapTableLen {
t.Fatalf("unexpected number of root buckets: %d", stats.RootBuckets)
}
if stats.TotalBuckets < stats.RootBuckets {
t.Fatalf("unexpected number of total buckets: %d", stats.TotalBuckets)
}
if stats.EmptyBuckets >= stats.RootBuckets {
t.Fatalf("unexpected number of empty buckets: %d", stats.EmptyBuckets)
}
if stats.Capacity < 2*entriesPerMapBucket*defaultMinMapTableLen {
t.Fatalf("unexpected capacity: %d", stats.Capacity)
}
if stats.Size != 200 {
t.Fatalf("unexpected size: %d", stats.Size)
}
if stats.Counter != 200 {
t.Fatalf("unexpected counter: %d", stats.Counter)
}
if stats.CounterLen != 8 {
t.Fatalf("unexpected counter length: %d", stats.CounterLen)
}
}
func TestToPlainMap_NilPointer(t *testing.T) {
pm := ToPlainMap[int, int](nil)
if len(pm) != 0 {
t.Fatalf("got unexpected size of nil map copy: %d", len(pm))
}
}
func TestToPlainMap(t *testing.T) {
const numEntries = 1000
m := NewMap[int, int]()
for i := 0; i < numEntries; i++ {
m.Store(i, i)
}
pm := ToPlainMap[int, int](m)
if len(pm) != numEntries {
t.Fatalf("got unexpected size of nil map copy: %d", len(pm))
}
for i := 0; i < numEntries; i++ {
if v := pm[i]; v != i {
t.Fatalf("unexpected value for key %d: %d", i, v)
}
}
}
func BenchmarkMap_NoWarmUp(b *testing.B) {
for _, bc := range benchmarkCases {
if bc.readPercentage == 100 {
// This benchmark doesn't make sense without a warm-up.
continue
}
b.Run(bc.name, func(b *testing.B) {
m := NewMap[string, int]()
benchmarkMapStringKeys(b, func(k string) (int, bool) {
return m.Load(k)
}, func(k string, v int) {
m.Store(k, v)
}, func(k string) {
m.Delete(k)
}, bc.readPercentage)
})
}
}
func BenchmarkMap_WarmUp(b *testing.B) {
for _, bc := range benchmarkCases {
b.Run(bc.name, func(b *testing.B) {
m := NewMap[string, int](WithPresize(benchmarkNumEntries))
for i := 0; i < benchmarkNumEntries; i++ {
m.Store(benchmarkKeyPrefix+strconv.Itoa(i), i)
}
b.ResetTimer()
benchmarkMapStringKeys(b, func(k string) (int, bool) {
return m.Load(k)
}, func(k string, v int) {
m.Store(k, v)
}, func(k string) {
m.Delete(k)
}, bc.readPercentage)
})
}
}
func benchmarkMapStringKeys(
b *testing.B,
loadFn func(k string) (int, bool),
storeFn func(k string, v int),
deleteFn func(k string),
readPercentage int,
) {
runParallel(b, func(pb *testing.PB) {
// convert percent to permille to support 99% case
storeThreshold := 10 * readPercentage
deleteThreshold := 10*readPercentage + ((1000 - 10*readPercentage) / 2)
for pb.Next() {
op := int(randv2.Uint64() % 1000)
i := int(randv2.Uint64() % benchmarkNumEntries)
if op >= deleteThreshold {
deleteFn(benchmarkKeys[i])
} else if op >= storeThreshold {
storeFn(benchmarkKeys[i], i)
} else {
loadFn(benchmarkKeys[i])
}
}
})
}
func BenchmarkMapInt_NoWarmUp(b *testing.B) {
for _, bc := range benchmarkCases {
if bc.readPercentage == 100 {
// This benchmark doesn't make sense without a warm-up.
continue
}
b.Run(bc.name, func(b *testing.B) {
m := NewMap[int, int]()
benchmarkMapIntKeys(b, func(k int) (int, bool) {
return m.Load(k)
}, func(k int, v int) {
m.Store(k, v)
}, func(k int) {
m.Delete(k)
}, bc.readPercentage)
})
}
}
func BenchmarkMapInt_WarmUp(b *testing.B) {
for _, bc := range benchmarkCases {
b.Run(bc.name, func(b *testing.B) {
m := NewMap[int, int](WithPresize(benchmarkNumEntries))
for i := 0; i < benchmarkNumEntries; i++ {
m.Store(i, i)
}
b.ResetTimer()
benchmarkMapIntKeys(b, func(k int) (int, bool) {
return m.Load(k)
}, func(k int, v int) {
m.Store(k, v)
}, func(k int) {
m.Delete(k)
}, bc.readPercentage)
})
}
}
func BenchmarkIntMapStandard_NoWarmUp(b *testing.B) {
for _, bc := range benchmarkCases {
if bc.readPercentage == 100 {
// This benchmark doesn't make sense without a warm-up.
continue
}
b.Run(bc.name, func(b *testing.B) {
var m sync.Map
benchmarkMapIntKeys(b, func(k int) (value int, ok bool) {
v, ok := m.Load(k)
if ok {
return v.(int), ok
} else {
return 0, false
}
}, func(k int, v int) {
m.Store(k, v)
}, func(k int) {
m.Delete(k)
}, bc.readPercentage)
})
}
}
// This is a nice scenario for sync.Map since a lot of updates
// will hit the readOnly part of the map.
func BenchmarkIntMapStandard_WarmUp(b *testing.B) {
for _, bc := range benchmarkCases {
b.Run(bc.name, func(b *testing.B) {
var m sync.Map
for i := 0; i < benchmarkNumEntries; i++ {
m.Store(i, i)
}
b.ResetTimer()
benchmarkMapIntKeys(b, func(k int) (value int, ok bool) {
v, ok := m.Load(k)
if ok {
return v.(int), ok
} else {
return 0, false
}
}, func(k int, v int) {
m.Store(k, v)
}, func(k int) {
m.Delete(k)
}, bc.readPercentage)
})
}
}
func benchmarkMapIntKeys(
b *testing.B,
loadFn func(k int) (int, bool),
storeFn func(k int, v int),
deleteFn func(k int),
readPercentage int,
) {
runParallel(b, func(pb *testing.PB) {
// convert percent to permille to support 99% case
storeThreshold := 10 * readPercentage
deleteThreshold := 10*readPercentage + ((1000 - 10*readPercentage) / 2)
for pb.Next() {
op := int(randv2.Uint64() % 1000)
i := int(randv2.Uint64() % benchmarkNumEntries)
if op >= deleteThreshold {
deleteFn(i)
} else if op >= storeThreshold {
storeFn(i, i)
} else {
loadFn(i)
}
}
})
}
func BenchmarkMapRange(b *testing.B) {
m := NewMap[string, int](WithPresize(benchmarkNumEntries))
for i := 0; i < benchmarkNumEntries; i++ {
m.Store(benchmarkKeys[i], i)
}
b.ResetTimer()
runParallel(b, func(pb *testing.PB) {
foo := 0
for pb.Next() {
m.Range(func(key string, value int) bool {
foo++
return true
})
_ = foo
}
})
}
// Benchmarks noop performance of Compute
func BenchmarkCompute(b *testing.B) {
tests := []struct {
Name string
Op ComputeOp
}{
{
Name: "UpdateOp",
Op: UpdateOp,
},
{
Name: "CancelOp",
Op: CancelOp,
},
}
for _, test := range tests {
b.Run("op="+test.Name, func(b *testing.B) {
m := NewMap[struct{}, bool]()
m.Store(struct{}{}, true)
b.ResetTimer()
for i := 0; i < b.N; i++ {
m.Compute(struct{}{}, func(oldValue bool, loaded bool) (newValue bool, op ComputeOp) {
return oldValue, test.Op
})
}
})
}
}
func TestNextPowOf2(t *testing.T) {
if nextPowOf2(0) != 1 {
t.Error("nextPowOf2 failed")
}
if nextPowOf2(1) != 1 {
t.Error("nextPowOf2 failed")
}
if nextPowOf2(2) != 2 {
t.Error("nextPowOf2 failed")
}
if nextPowOf2(3) != 4 {
t.Error("nextPowOf2 failed")
}
}
func TestBroadcast(t *testing.T) {
testCases := []struct {
input uint8
expected uint64
}{
{
input: 0,
expected: 0,
},
{
input: 1,
expected: 0x0101010101010101,
},
{
input: 2,
expected: 0x0202020202020202,
},
{
input: 42,
expected: 0x2a2a2a2a2a2a2a2a,
},
{
input: 127,
expected: 0x7f7f7f7f7f7f7f7f,
},
{
input: 255,
expected: 0xffffffffffffffff,
},
}
for _, tc := range testCases {
t.Run(strconv.Itoa(int(tc.input)), func(t *testing.T) {
if broadcast(tc.input) != tc.expected {
t.Errorf("unexpected result: %x", broadcast(tc.input))
}
})
}
}
func TestFirstMarkedByteIndex(t *testing.T) {
testCases := []struct {
input uint64
expected int
}{
{
input: 0,
expected: 8,
},
{
input: 0x8080808080808080,
expected: 0,
},
{
input: 0x0000000000000080,
expected: 0,
},
{
input: 0x0000000000008000,
expected: 1,
},
{
input: 0x0000000000800000,
expected: 2,
},
{
input: 0x0000000080000000,
expected: 3,
},
{
input: 0x0000008000000000,
expected: 4,
},
{
input: 0x0000800000000000,
expected: 5,
},
{
input: 0x0080000000000000,
expected: 6,
},
{
input: 0x8000000000000000,
expected: 7,
},
}
for _, tc := range testCases {
t.Run(strconv.Itoa(int(tc.input)), func(t *testing.T) {
if firstMarkedByteIndex(tc.input) != tc.expected {
t.Errorf("unexpected result: %x", firstMarkedByteIndex(tc.input))
}
})
}
}
func TestMarkZeroBytes(t *testing.T) {
testCases := []struct {
input uint64
expected uint64
}{
{
input: 0xffffffffffffffff,
expected: 0,
},
{
input: 0,
expected: 0x8080808080808080,
},
{
input: 1,
expected: 0x8080808080808000,
},
{
input: 1 << 9,
expected: 0x8080808080800080,
},
{
input: 1 << 17,
expected: 0x8080808080008080,
},
{
input: 1 << 25,
expected: 0x8080808000808080,
},
{
input: 1 << 33,
expected: 0x8080800080808080,
},
{
input: 1 << 41,
expected: 0x8080008080808080,
},
{
input: 1 << 49,
expected: 0x8000808080808080,
},
{
input: 1 << 57,
expected: 0x0080808080808080,
},
// false positive
{
input: 0x0100,
expected: 0x8080808080808080,
},
}
for _, tc := range testCases {
t.Run(strconv.Itoa(int(tc.input)), func(t *testing.T) {
if markZeroBytes(tc.input) != tc.expected {
t.Errorf("unexpected result: %x", markZeroBytes(tc.input))
}
})
}
}
func TestSetByte(t *testing.T) {
testCases := []struct {
word uint64
b uint8
idx int
expected uint64
}{
{
word: 0xffffffffffffffff,
b: 0,
idx: 0,
expected: 0xffffffffffffff00,
},
{
word: 0xffffffffffffffff,
b: 1,
idx: 1,
expected: 0xffffffffffff01ff,
},
{
word: 0xffffffffffffffff,
b: 2,
idx: 2,
expected: 0xffffffffff02ffff,
},
{
word: 0xffffffffffffffff,
b: 3,
idx: 3,
expected: 0xffffffff03ffffff,
},
{
word: 0xffffffffffffffff,
b: 4,
idx: 4,
expected: 0xffffff04ffffffff,
},
{
word: 0xffffffffffffffff,
b: 5,
idx: 5,
expected: 0xffff05ffffffffff,
},
{
word: 0xffffffffffffffff,
b: 6,
idx: 6,
expected: 0xff06ffffffffffff,
},
{
word: 0xffffffffffffffff,
b: 7,
idx: 7,
expected: 0x07ffffffffffffff,
},
{
word: 0,
b: 0xff,
idx: 7,
expected: 0xff00000000000000,
},
}
for _, tc := range testCases {
t.Run(strconv.Itoa(int(tc.word)), func(t *testing.T) {
if setByte(tc.word, tc.b, tc.idx) != tc.expected {
t.Errorf("unexpected result: %x", setByte(tc.word, tc.b, tc.idx))
}
})
}
}