1
0
mirror of https://github.com/MetaCubeX/mihomo.git synced 2026-10-10 20:23:11 +08:00
Files
mihomo/common/xsync/map_test.go
T

2427 lines
61 KiB
Go

package xsync
import (
"math"
"math/rand"
"runtime"
"strconv"
"sync"
"sync/atomic"
"testing"
"time"
"unsafe"
"github.com/metacubex/mihomo/common/maphash"
"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
}
type integerKey interface {
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 | uintptr
}
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{})
if size != 64 {
t.Fatalf("size of 64B (one cache line) is expected, got: %d", size)
}
size = unsafe.Sizeof(bucketPadded{})
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)
}
}
// TestMapHashUint64_NoDifferentialBias verifies that hashUint64 does
// not exhibit seed-independent differential bias for XOR deltas
// (see https://github.com/puzpuzpuz/xsync/issues/192).
func TestMapHashUint64_NoDifferentialBias(t *testing.T) {
const (
nBuckets = 256
mask = nBuckets - 1
nTrials = 100_000
threshold = 3.0
)
expected := float64(nTrials) / float64(nBuckets)
// Deltas from the issue report.
issueDeltas := []uint64{
0x0000015000000000,
0x0000004F00000000,
0x000000A300000000,
0x00000D0000000000,
}
// Structured deltas across various shift amounts.
var shiftedDeltas []uint64
for _, shift := range []uint{0, 8, 16, 24, 32, 40, 48} {
for k := uint64(1); k <= 64; k++ {
shiftedDeltas = append(shiftedDeltas, k<<shift)
}
}
// Single-bit and two-bit (Hamming distance 1-2) deltas.
var hammingDeltas []uint64
for b := 0; b < 64; b++ {
hammingDeltas = append(hammingDeltas, 1<<b)
}
for b1 := 0; b1 < 64; b1 += 4 {
for b2 := b1 + 1; b2 < 64; b2 += 4 {
hammingDeltas = append(hammingDeltas, (1<<b1)|(1<<b2))
}
}
allDeltas := make([]uint64, 0, len(issueDeltas)+len(shiftedDeltas)+len(hammingDeltas))
allDeltas = append(allDeltas, issueDeltas...)
allDeltas = append(allDeltas, shiftedDeltas...)
allDeltas = append(allDeltas, hammingDeltas...)
seeds := []uint64{0, 42, 0xDEADBEEF}
for _, seed := range seeds {
for _, delta := range allDeltas {
if delta == 0 {
continue
}
collisions := 0
for i := 0; i < nTrials; i++ {
v := uint64(i) * 0x9E3779B97F4A7C15 // spread values
seed := *(*maphash.Seed)(unsafe.Pointer(&seed))
h1 := hashUint64(seed, v)
h2 := hashUint64(seed, v^delta)
if (h1 & mask) == (h2 & mask) {
collisions++
}
}
ratio := float64(collisions) / expected
if ratio > threshold {
t.Errorf("seed=0x%x delta=0x%x: got %.2fx expected collision rate (threshold=%.1fx)",
seed, delta, ratio, threshold)
}
}
}
}
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 TestMapAll(t *testing.T) {
m := NewMap[string, int]()
m.All()(func(key string, value int) bool {
t.Fatal("got an iteration on empty map")
return true
})
for i := 0; i < 1000; i++ {
m.Store(strconv.Itoa(i), i)
}
iters := 0
met := make(map[string]int)
m.All()(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 != 1000 {
t.Fatalf("got unexpected number of iterations: %d", iters)
}
for i := 0; i < 1000; i++ {
if c := met[strconv.Itoa(i)]; c != 1 {
t.Fatalf("range did not iterate correctly over %d: %d", i, c)
}
}
}
func TestMapAll_Break(t *testing.T) {
m := NewMap[string, int]()
for i := 0; i < 100; i++ {
m.Store(strconv.Itoa(i), i)
}
iters := 0
m.All()(func(key string, value int) bool {
iters++
if iters == 50 {
return false
}
return true
})
if iters != 50 {
t.Fatalf("got unexpected number of iterations: %d", iters)
}
}
func TestMapAll_NestedDelete(t *testing.T) {
const numEntries = 256
m := NewMap[string, int]()
for i := 0; i < numEntries; i++ {
m.Store(strconv.Itoa(i), i)
}
m.All()(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 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 {
if key != strconv.Itoa(value) {
t.Fatalf("got unexpected key/value for iteration %d: %v/%v", iters, key, value)
}
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)
}
iters := 0
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)
}
m.Delete(key)
iters++
return true
})
for i := 0; i < numEntries; i++ {
if _, ok := m.Load(strconv.Itoa(i)); ok {
t.Fatalf("value found for %d", i)
}
}
}
func TestMapRangeRelaxed(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.RangeRelaxed(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 TestMapRangeRelaxed_FalseReturned(t *testing.T) {
m := NewMap[string, int]()
for i := 0; i < 100; i++ {
m.Store(strconv.Itoa(i), i)
}
iters := 0
m.RangeRelaxed(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)
}
iters++
return iters != 13
})
if iters != 13 {
t.Fatalf("got unexpected number of iterations: %d", iters)
}
}
func TestMapRangeRelaxed_NestedDelete(t *testing.T) {
const numEntries = 256
m := NewMap[string, int]()
for i := 0; i < numEntries; i++ {
m.Store(strconv.Itoa(i), i)
}
m.RangeRelaxed(func(key string, value int) bool {
if key != strconv.Itoa(value) {
t.Fatalf("got unexpected key/value: %v/%v", key, value)
}
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 TestMapAllRelaxed(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.AllRelaxed()(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)
}
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("all did not iterate correctly over %d: %d", i, c)
}
}
}
func TestMapDeleteMatching(t *testing.T) {
const numEntries = 1000
m := NewMap[string, int]()
for i := 0; i < numEntries; i++ {
m.Store(strconv.Itoa(i), i)
}
// Delete even values.
deleted := m.DeleteMatching(func(key string, value int) (del, stop bool) {
return value%2 == 0, false
})
if deleted != numEntries/2 {
t.Fatalf("expected %d deleted, got %d", numEntries/2, deleted)
}
if m.Size() != numEntries/2 {
t.Fatalf("expected size %d, got %d", numEntries/2, m.Size())
}
// Verify only odd values remain.
for i := 0; i < numEntries; i++ {
_, ok := m.Load(strconv.Itoa(i))
if i%2 == 0 && ok {
t.Fatalf("even value %d should have been deleted", i)
}
if i%2 != 0 && !ok {
t.Fatalf("odd value %d should not have been deleted", i)
}
}
}
func TestMapDeleteMatching_Cancel(t *testing.T) {
const numEntries = 100
m := NewMap[string, int]()
for i := 0; i < numEntries; i++ {
m.Store(strconv.Itoa(i), i)
}
// Delete entries and cancel after 10 deletions.
callCount := 0
deleted := m.DeleteMatching(func(key string, value int) (del, stop bool) {
callCount++
if callCount == 10 {
return true, true // delete this one and cancel
}
return true, false
})
if deleted != 10 {
t.Fatalf("expected 10 deleted, got %d", deleted)
}
if callCount != 10 {
t.Fatalf("expected f to be called 10 times, got %d", callCount)
}
if m.Size() != numEntries-10 {
t.Fatalf("expected size %d, got %d", numEntries-10, m.Size())
}
}
func TestMapDeleteMatching_EmptyMap(t *testing.T) {
m := NewMap[string, int]()
callCount := 0
deleted := m.DeleteMatching(func(key string, value int) (del, stop bool) {
callCount++
return false, false
})
if deleted != 0 {
t.Fatalf("expected 0 deleted on empty map, got %d", deleted)
}
if callCount != 0 {
t.Fatalf("expected f to be called 0 times, got %d", callCount)
}
}
func TestMapDeleteMatching_NoDeletions(t *testing.T) {
const numEntries = 100
m := NewMap[string, int]()
for i := 0; i < numEntries; i++ {
m.Store(strconv.Itoa(i), i)
}
callCount := 0
deleted := m.DeleteMatching(func(key string, value int) (del, stop bool) {
callCount++
return false, false // never delete
})
if deleted != 0 {
t.Fatalf("expected 0 deleted, got %d", deleted)
}
if callCount != numEntries {
t.Fatalf("expected f to be called %d times, got %d", numEntries, callCount)
}
if m.Size() != numEntries {
t.Fatalf("expected size %d, got %d", numEntries, m.Size())
}
}
func TestMapDeleteMatching_AllDeleted(t *testing.T) {
const numEntries = 256
m := NewMap[string, int]()
for i := 0; i < numEntries; i++ {
m.Store(strconv.Itoa(i), i)
}
deleted := m.DeleteMatching(func(key string, value int) (del, stop bool) {
return true, false // delete all
})
if deleted != numEntries {
t.Fatalf("expected %d deleted, got %d", numEntries, deleted)
}
if m.Size() != 0 {
t.Fatalf("expected size 0, got %d", m.Size())
}
}
func testParallelRangeRelaxed(t *testing.T, numGoroutines int) {
const numEntries = 10000
const numIterations = 50
m := NewMap[int, int]()
for i := 0; i < numEntries; i++ {
m.Store(i, i)
}
var wg sync.WaitGroup
var totalIterations atomic.Int64
// Launch goroutines that iterate using RangeRelaxed.
for i := 0; i < numGoroutines/2; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < numIterations; i++ {
m.RangeRelaxed(func(key int, value int) bool {
if key != value {
t.Errorf("key %d != value %d", key, value)
}
totalIterations.Add(1)
return true
})
}
}()
}
// Launch goroutines that modify the map.
for i := 0; i < numGoroutines/2; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < numIterations; i++ {
for i := 0; i < numEntries; i++ {
m.Store(i, i)
if i%10 == 0 {
m.Delete(i)
m.Store(i, i)
}
}
}
}()
}
wg.Wait()
if totalIterations.Load() == 0 {
t.Error("expected some iterations to occur")
}
}
func TestMapParallelRangeRelaxed(t *testing.T) {
testParallelRangeRelaxed(t, 2)
testParallelRangeRelaxed(t, runtime.GOMAXPROCS(0))
testParallelRangeRelaxed(t, 100)
}
func testParallelDeleteMatching(t *testing.T, numGoroutines int) {
const numEntries = 10000
const numIterations = 50
m := NewMap[int, int]()
for i := 0; i < numEntries; i++ {
m.Store(i, i)
}
var wg sync.WaitGroup
var totalDeleted atomic.Int64
// Launch goroutines that delete even numbers.
for i := 0; i < numGoroutines/2; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < numIterations; i++ {
deleted := m.DeleteMatching(func(key int, value int) (del, stop bool) {
return key%2 == 0, false
})
totalDeleted.Add(int64(deleted))
}
}()
}
// Launch goroutines that re-add entries.
for i := 0; i < numGoroutines/2; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < numIterations; i++ {
for i := 0; i < numEntries; i++ {
m.Store(i, i)
}
}
}()
}
wg.Wait()
// Verify map is in consistent state.
size := m.Size()
rangeCount := 0
m.Range(func(key int, value int) bool {
if key != value {
t.Errorf("key %d != value %d", key, value)
}
rangeCount++
return true
})
if size != rangeCount {
t.Errorf("size %d != range count %d", size, rangeCount)
}
if totalDeleted.Load() == 0 {
t.Error("expected some deletions to occur")
}
}
func TestMapParallelDeleteMatching(t *testing.T) {
testParallelDeleteMatching(t, 2)
testParallelDeleteMatching(t, runtime.GOMAXPROCS(0))
testParallelDeleteMatching(t, 100)
}
func TestMapDeleteMatching_ConcurrentResize(t *testing.T) {
// This test attempts to cover the resize paths in DeleteMatching:
// 1. Resize in progress during DeleteMatching iteration
// 2. Table changed (resize completed) during DeleteMatching iteration
const numIterations = 1000
const numEntries = 100
for iter := 0; iter < numIterations; iter++ {
// Start with minimal size to maximize resize frequency
m := NewMap[int, int]()
// Pre-fill just enough to have entries
for i := 0; i < numEntries; i++ {
m.Store(i, i)
}
var wg sync.WaitGroup
start := make(chan struct{})
// Goroutines that trigger resize by adding many new entries
for i := 0; i < 8; i++ {
wg.Add(1)
go func() {
defer wg.Done()
<-start
for i := numEntries; i < numEntries*10; i++ {
m.Store(i, i)
}
}()
}
// Goroutines that call DeleteMatching repeatedly during resize
for i := 0; i < 8; i++ {
wg.Add(1)
go func() {
defer wg.Done()
<-start
for i := 0; i < 10; i++ {
m.DeleteMatching(func(key int, value int) (del, stop bool) {
return key%5 == 0, false
})
}
}()
}
// Start all goroutines simultaneously
close(start)
wg.Wait()
// Verify map consistency
size := m.Size()
rangeCount := 0
m.Range(func(key int, value int) bool {
rangeCount++
return true
})
if size != rangeCount {
t.Errorf("iteration %d: size %d != range count %d", iter, size, rangeCount)
}
}
}
func testMapStoreKeys[K integerKey](t *testing.T, numEntries int) {
t.Helper()
m := NewMap[K, K]()
for i := 0; i < numEntries; i++ {
m.Store(K(i), K(i))
}
for i := 0; i < numEntries; i++ {
v, ok := m.Load(K(i))
if !ok {
t.Fatalf("value not found for %d", i)
}
if v != K(i) {
t.Fatalf("values do not match for %d: %v", i, v)
}
}
}
func TestMapStore_TypedKeys(t *testing.T) {
const n = 128
t.Run("int", func(t *testing.T) { testMapStoreKeys[int](t, n) })
t.Run("uint", func(t *testing.T) { testMapStoreKeys[uint](t, n) })
t.Run("int8", func(t *testing.T) { testMapStoreKeys[int8](t, n) })
t.Run("uint8", func(t *testing.T) { testMapStoreKeys[uint8](t, n) })
t.Run("int16", func(t *testing.T) { testMapStoreKeys[int16](t, n) })
t.Run("uint16", func(t *testing.T) { testMapStoreKeys[uint16](t, n) })
t.Run("int32", func(t *testing.T) { testMapStoreKeys[int32](t, n) })
t.Run("uint32", func(t *testing.T) { testMapStoreKeys[uint32](t, n) })
t.Run("int64", func(t *testing.T) { testMapStoreKeys[int64](t, n) })
t.Run("uint64", func(t *testing.T) { testMapStoreKeys[uint64](t, n) })
t.Run("uintptr", func(t *testing.T) { testMapStoreKeys[uintptr](t, n) })
t.Run("string", func(t *testing.T) {
m := NewMap[string, int]()
for i := 0; i < n; i++ {
m.Store(strconv.Itoa(i), i)
}
for i := 0; i < n; 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)
}
}
})
t.Run("struct_intValues", func(t *testing.T) {
m := NewMap[point, int]()
for i := 0; i < n; i++ {
m.Store(point{int32(i), -int32(i)}, i)
}
for i := 0; i < n; 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)
}
}
})
t.Run("struct_structValues", func(t *testing.T) {
m := NewMap[point, point]()
for i := 0; i < n; i++ {
m.Store(point{int32(i), -int32(i)}, point{-int32(i), int32(i)})
}
for i := 0; i < n; 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 TestMapLoadOrCompute_ExistingKey(t *testing.T) {
m := NewMap[string, int]()
m.Store("key", 42)
v, loaded := m.LoadOrCompute("key", func() (int, bool) {
t.Fatal("value func should not be called for existing key")
return 100, false
})
if !loaded {
t.Fatal("expected loaded to be true")
}
if v != 42 {
t.Fatalf("expected value 42, got %d", v)
}
}
func TestMapLoadOrCompute_ConcurrentExistingKey(t *testing.T) {
// This test attempts to cover the race condition where:
// 1. LoadOrCompute's fast path doesn't find the key
// 2. Another goroutine inserts the key
// 3. LoadOrCompute acquires the lock and finds the key
const numIters = 10000
for i := 0; i < numIters; i++ {
m := NewMap[int, int]()
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
m.Store(1, 42)
}()
go func() {
defer wg.Done()
m.LoadOrCompute(1, func() (int, bool) {
return 100, false
})
}()
wg.Wait()
v, ok := m.Load(1)
if !ok {
t.Fatal("key should exist")
}
if v != 42 && v != 100 {
t.Fatalf("unexpected value: %d", v)
}
}
}
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 TestMapCompute_CancelOpOnOverflowBucket(t *testing.T) {
// This test covers the CancelOp path when inserting into a new overflow bucket.
// We need to fill buckets completely so that a new key requires creating
// an overflow bucket, then return CancelOp to abort the insertion.
const numAttempts = 1000
const sentinel = 999
for attempt := 0; attempt < numAttempts; attempt++ {
m := NewMap[int, int]()
// Fill the map to create conditions where some bucket chains are full.
// Insert entries - some will hash to the same bucket creating overflow.
numEntries := 100 + (attempt % 100) // Vary the fill level
for i := 0; i < numEntries; i++ {
m.Store(i, i)
}
// Try Compute with CancelOp for new keys.
baseKey := 10000 + attempt*1000
for i := 0; i < 50; i++ {
key := baseKey + i
v, ok := m.Compute(key, func(oldValue int, loaded bool) (newValue int, op ComputeOp) {
if loaded {
t.Fatal("key should not exist")
}
return sentinel, CancelOp
})
if ok {
t.Fatalf("ok should be false when CancelOp is returned for new key")
}
// Both code paths should return zero value for CancelOp
if v != 0 {
t.Fatalf("expected zero value for CancelOp, got: %d", v)
}
// Verify the key was not inserted
if _, exists := m.Load(key); exists {
t.Fatalf("key %d should not exist after CancelOp", key)
}
}
}
}
func testMapStoreThenDeleteKeys[K integerKey](t *testing.T, numEntries int) {
t.Helper()
m := NewMap[K, K]()
for i := 0; i < numEntries; i++ {
m.Store(K(i), K(i))
}
for i := 0; i < numEntries; i++ {
m.Delete(K(i))
if _, ok := m.Load(K(i)); ok {
t.Fatalf("value was not expected for %d", i)
}
}
}
func TestMapStoreThenDelete_TypedKeys(t *testing.T) {
const n = 1000
const nByte = 200
t.Run("int", func(t *testing.T) { testMapStoreThenDeleteKeys[int](t, n) })
t.Run("uint", func(t *testing.T) { testMapStoreThenDeleteKeys[uint](t, n) })
t.Run("int8", func(t *testing.T) { testMapStoreThenDeleteKeys[int8](t, nByte) })
t.Run("uint8", func(t *testing.T) { testMapStoreThenDeleteKeys[uint8](t, nByte) })
t.Run("int16", func(t *testing.T) { testMapStoreThenDeleteKeys[int16](t, n) })
t.Run("uint16", func(t *testing.T) { testMapStoreThenDeleteKeys[uint16](t, n) })
t.Run("int32", func(t *testing.T) { testMapStoreThenDeleteKeys[int32](t, n) })
t.Run("uint32", func(t *testing.T) { testMapStoreThenDeleteKeys[uint32](t, n) })
t.Run("int64", func(t *testing.T) { testMapStoreThenDeleteKeys[int64](t, n) })
t.Run("uint64", func(t *testing.T) { testMapStoreThenDeleteKeys[uint64](t, n) })
t.Run("uintptr", func(t *testing.T) { testMapStoreThenDeleteKeys[uintptr](t, n) })
t.Run("string", func(t *testing.T) {
m := NewMap[string, int]()
for i := 0; i < n; i++ {
m.Store(strconv.Itoa(i), i)
}
for i := 0; i < n; i++ {
m.Delete(strconv.Itoa(i))
if _, ok := m.Load(strconv.Itoa(i)); ok {
t.Fatalf("value was not expected for %d", i)
}
}
})
t.Run("struct", func(t *testing.T) {
m := NewMap[point, string]()
for i := 0; i < n; i++ {
m.Store(point{int32(i), 42}, strconv.Itoa(i))
}
for i := 0; i < n; 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 testMapStoreThenLoadAndDeleteKeys[K integerKey](t *testing.T, numEntries int) {
t.Helper()
m := NewMap[K, K]()
for i := 0; i < numEntries; i++ {
m.Store(K(i), K(i))
}
for i := 0; i < numEntries; i++ {
if _, loaded := m.LoadAndDelete(K(i)); !loaded {
t.Fatalf("value was not found for %d", i)
}
if _, ok := m.Load(K(i)); ok {
t.Fatalf("value was not expected for %d", i)
}
}
}
func TestMapStoreThenLoadAndDelete_TypedKeys(t *testing.T) {
const n = 1000
const nByte = 200
t.Run("int", func(t *testing.T) { testMapStoreThenLoadAndDeleteKeys[int](t, n) })
t.Run("uint", func(t *testing.T) { testMapStoreThenLoadAndDeleteKeys[uint](t, n) })
t.Run("int8", func(t *testing.T) { testMapStoreThenLoadAndDeleteKeys[int8](t, nByte) })
t.Run("uint8", func(t *testing.T) { testMapStoreThenLoadAndDeleteKeys[uint8](t, nByte) })
t.Run("int16", func(t *testing.T) { testMapStoreThenLoadAndDeleteKeys[int16](t, n) })
t.Run("uint16", func(t *testing.T) { testMapStoreThenLoadAndDeleteKeys[uint16](t, n) })
t.Run("int32", func(t *testing.T) { testMapStoreThenLoadAndDeleteKeys[int32](t, n) })
t.Run("uint32", func(t *testing.T) { testMapStoreThenLoadAndDeleteKeys[uint32](t, n) })
t.Run("int64", func(t *testing.T) { testMapStoreThenLoadAndDeleteKeys[int64](t, n) })
t.Run("uint64", func(t *testing.T) { testMapStoreThenLoadAndDeleteKeys[uint64](t, n) })
t.Run("uintptr", func(t *testing.T) { testMapStoreThenLoadAndDeleteKeys[uintptr](t, n) })
t.Run("string", func(t *testing.T) {
m := NewMap[string, int]()
for i := 0; i < n; i++ {
m.Store(strconv.Itoa(i), i)
}
for i := 0; i < n; 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)
}
}
})
t.Run("struct", func(t *testing.T) {
m := NewMap[point, int]()
for i := 0; i < n; i++ {
m.Store(point{42, int32(i)}, i)
}
for i := 0; i < n; 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) {
const defaultMinMapTableCap = defaultMinMapTableLen * entriesPerMapBucket
assertMapCapacity(t, NewMap[string, string](), defaultMinMapTableCap)
assertMapCapacity(t, NewMap[string, string](WithPresize(0)), defaultMinMapTableCap)
assertMapCapacity(t, NewMap[string, string](WithPresize(-100)), defaultMinMapTableCap)
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) {
m := NewMap[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 testParallelResize(t *testing.T, numGoroutines int) {
m := NewMap[int, int]()
// Fill the map to trigger resizing
const initialEntries = 10000
const newEntries = 5000
for i := 0; i < initialEntries; i++ {
m.Store(i, i*2)
}
// Start concurrent operations that should trigger helping behavior
var wg sync.WaitGroup
// Launch goroutines that will encounter resize operations
for g := 0; g < numGoroutines; g++ {
wg.Add(1)
go func(goroutineID int) {
defer wg.Done()
// Perform many operations to trigger resize and helping
for i := 0; i < newEntries; i++ {
key := goroutineID*newEntries + i + initialEntries
m.Store(key, key*2)
// Verify the value
if val, ok := m.Load(key); !ok || val != key*2 {
t.Errorf("Failed to load key %d: got %v, %v", key, val, ok)
return
}
}
}(g)
}
wg.Wait()
// Verify all entries are present
finalSize := m.Size()
expectedSize := initialEntries + numGoroutines*newEntries
if finalSize != expectedSize {
t.Errorf("Expected size %d, got %d", expectedSize, finalSize)
}
stats := m.Stats()
if stats.TotalGrowths == 0 {
t.Error("Expected at least one table growth due to concurrent operations")
}
}
func TestMapParallelResize(t *testing.T) {
testParallelResize(t, 1)
testParallelResize(t, runtime.GOMAXPROCS(0))
testParallelResize(t, 100)
}
func testParallelResizeWithSameKeys(t *testing.T, numGoroutines int) {
m := NewMap[int, int]()
// Fill the map to trigger resizing
const entries = 1000
for i := 0; i < entries; i++ {
m.Store(2*i, 2*i)
}
// Start concurrent operations that should trigger helping behavior
var wg sync.WaitGroup
// Launch goroutines that will encounter resize operations
for g := 0; g < numGoroutines; g++ {
wg.Add(1)
go func(goroutineID int) {
defer wg.Done()
for i := 0; i < 10*entries; i++ {
m.Store(i, i)
}
}(g)
}
wg.Wait()
// Verify all entries are present
finalSize := m.Size()
expectedSize := 10 * entries
if finalSize != expectedSize {
t.Errorf("Expected size %d, got %d", expectedSize, finalSize)
}
stats := m.Stats()
if stats.TotalGrowths == 0 {
t.Error("Expected at least one table growth due to concurrent operations")
}
}
func TestMapParallelResize_IntersectingKeys(t *testing.T) {
testParallelResizeWithSameKeys(t, 1)
testParallelResizeWithSameKeys(t, runtime.GOMAXPROCS(0))
testParallelResizeWithSameKeys(t, 100)
}
func testParallelShrinking(t *testing.T, numGoroutines int) {
m := NewMap[int, int]()
// Fill the map to trigger resizing
const entries = 100000
for i := 0; i < entries; i++ {
m.Store(i, i)
}
// Start concurrent operations that should trigger helping behavior
var wg sync.WaitGroup
// Launch goroutines that will encounter resize operations
for g := 0; g < numGoroutines; g++ {
wg.Add(1)
go func(goroutineID int) {
defer wg.Done()
for i := 0; i < entries; i++ {
m.Delete(i)
}
}(g)
}
wg.Wait()
// Verify all entries are present
finalSize := m.Size()
if finalSize != 0 {
t.Errorf("Expected size 0, got %d", finalSize)
}
stats := m.Stats()
if stats.TotalShrinks == 0 {
t.Error("Expected at least one table shrinking due to concurrent operations")
}
}
func TestMapParallelShrinking(t *testing.T) {
testParallelShrinking(t, 1)
testParallelShrinking(t, runtime.GOMAXPROCS(0))
testParallelShrinking(t, 100)
}
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 := 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
}
})
}
func BenchmarkMapRangeRelaxed(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.RangeRelaxed(func(key string, value int) bool {
foo++
return true
})
_ = foo
}
})
}
// Benchmarks noop performance of Compute
func BenchmarkMapCompute(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 BenchmarkMapParallelRehashing(b *testing.B) {
tests := []struct {
name string
goroutines int
numEntries int
}{
{"1goroutine_10M", 1, 10_000_000},
{"4goroutines_10M", 4, 10_000_000},
{"8goroutines_10M", 8, 10_000_000},
}
for _, test := range tests {
b.Run(test.name, func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
m := NewMap[int, int]()
var wg sync.WaitGroup
entriesPerGoroutine := test.numEntries / test.goroutines
start := time.Now()
for g := 0; g < test.goroutines; g++ {
wg.Add(1)
go func(goroutineID int) {
defer wg.Done()
base := goroutineID * entriesPerGoroutine
for j := 0; j < entriesPerGoroutine; j++ {
key := base + j
m.Store(key, key)
}
}(g)
}
wg.Wait()
duration := time.Since(start)
b.ReportMetric(float64(test.numEntries)/duration.Seconds(), "entries/s")
finalSize := m.Size()
if finalSize != test.numEntries {
b.Fatalf("Expected size %d, got %d", test.numEntries, finalSize)
}
stats := m.Stats()
if stats.TotalGrowths == 0 {
b.Error("Expected at least one table growth during rehashing")
}
}
})
}
}
func BenchmarkMapDeleteMatching(b *testing.B) {
tests := []struct {
name string
numEntries int
deletePercent int
}{
{"entries=1000_delete=10%", 1000, 10},
{"entries=1000_delete=50%", 1000, 50},
{"entries=1000_delete=100%", 1000, 100},
{"entries=100000_delete=10%", 100000, 10},
{"entries=100000_delete=50%", 100000, 50},
{"entries=100000_delete=100%", 100000, 100},
{"entries=1000000_delete=10%", 1000000, 10},
{"entries=1000000_delete=50%", 1000000, 50},
{"entries=1000000_delete=100%", 1000000, 100},
}
for _, test := range tests {
b.Run(test.name, func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
m := NewMap[int, int](WithPresize(test.numEntries))
for i := 0; i < test.numEntries; i++ {
m.Store(i, i)
}
threshold := test.numEntries * test.deletePercent / 100
m.DeleteMatching(func(key int, value int) (del, stop bool) {
return key < threshold, false
})
}
})
}
}
func BenchmarkMapRangeDelete(b *testing.B) {
tests := []struct {
name string
numEntries int
deletePercent int
}{
{"entries=1000_delete=10%", 1000, 10},
{"entries=1000_delete=50%", 1000, 50},
{"entries=1000_delete=100%", 1000, 100},
{"entries=100000_delete=10%", 100000, 10},
{"entries=100000_delete=50%", 100000, 50},
{"entries=100000_delete=100%", 100000, 100},
{"entries=1000000_delete=10%", 1000000, 10},
{"entries=1000000_delete=50%", 1000000, 50},
{"entries=1000000_delete=100%", 1000000, 100},
}
for _, test := range tests {
b.Run(test.name, func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
m := NewMap[int, int](WithPresize(test.numEntries))
for i := 0; i < test.numEntries; i++ {
m.Store(i, i)
}
threshold := test.numEntries * test.deletePercent / 100
m.Range(func(key int, value int) bool {
if key < threshold {
m.Delete(key)
}
return true
})
}
})
}
}