Repository: puzpuzpuz/xsync
Pull request #133 (2024-06-29 17:03:30)

Expose Map/MapOf statistics
Map/MapOf statistics are available via `m.Stats()`. They may be used for diagnostic purposes.


Patch:
diff --git a/map.go b/map.go
index 92d73ac..13360fc 100644
--- a/map.go
+++ b/map.go
@@ -766,23 +766,51 @@ func (table *mapTable) sumSize() int64 {
 	return sum
 }
 
-type mapStats struct {
-	RootBuckets  int
+// MapStats is Map/MapOf statistics.
+//
+// Warning: map statistics are intented to be used for diagnostic
+// purposes, not for production code. This means that breaking changes
+// may be introduced into this struct even between minor releases.
+type MapStats struct {
+	// RootBuckets is the number of root buckets in the hash table.
+	// Each bucket holds a few entries.
+	RootBuckets int
+	// TotalBuckets is the total number of buckets in the hash table,
+	// including root and their chained buckets. Each bucket holds
+	// a few entries.
 	TotalBuckets int
+	// EmptyBuckets is the number of buckets that hold no entries.
 	EmptyBuckets int
-	Capacity     int
-	Size         int // calculated number of entries
-	Counter      int // number of entries according to table counter
-	CounterLen   int // number of counter stripes
-	MinEntries   int // min entries per chain of buckets
-	MaxEntries   int // max entries per chain of buckets
+	// Capacity is the Map/MapOf capacity, i.e. the total number of
+	// entries that all buckets can physically hold. This number
+	// does not consider the load factor.
+	Capacity int
+	// Size is the exact number of entries stored in the map.
+	Size int
+	// Counter is the number of entries stored in the map according
+	// to the internal atomic counter. In case of concurrent map
+	// modifications this number may be different from Size.
+	Counter int
+	// CounterLen is the number of internal atomic counter stripes.
+	// This number may grow with the map capacity to improve
+	// multithreaded scalability.
+	CounterLen int
+	// MinEntries is the minimum number of entries per a chain of
+	// buckets, i.e. a root bucket and its chained buckets.
+	MinEntries int
+	// MinEntries is the maximum number of entries per a chain of
+	// buckets, i.e. a root bucket and its chained buckets.
+	MaxEntries int
+	// TotalGrowths is the number of times the hash table grew.
 	TotalGrowths int64
+	// TotalGrowths is the number of times the hash table shrinked.
 	TotalShrinks int64
 }
 
-func (s *mapStats) ToString() string {
+// ToString returns string representation of map stats.
+func (s *MapStats) ToString() string {
 	var sb strings.Builder
-	sb.WriteString("\n---\n")
+	sb.WriteString("MapStats{\n")
 	sb.WriteString(fmt.Sprintf("RootBuckets:  %d\n", s.RootBuckets))
 	sb.WriteString(fmt.Sprintf("TotalBuckets: %d\n", s.TotalBuckets))
 	sb.WriteString(fmt.Sprintf("EmptyBuckets: %d\n", s.EmptyBuckets))
@@ -794,13 +822,15 @@ func (s *mapStats) ToString() string {
 	sb.WriteString(fmt.Sprintf("MaxEntries:   %d\n", s.MaxEntries))
 	sb.WriteString(fmt.Sprintf("TotalGrowths: %d\n", s.TotalGrowths))
 	sb.WriteString(fmt.Sprintf("TotalShrinks: %d\n", s.TotalShrinks))
-	sb.WriteString("---\n")
+	sb.WriteString("}\n")
 	return sb.String()
 }
 
-// O(N) operation; use for debug purposes only
-func (m *Map) stats() mapStats {
-	stats := mapStats{
+// Stats returns statistics for the Map. Just like other map
+// methods, this one is thread-safe. Yet it's an O(N) operation,
+// so it should be used only for diagnostics or debugging purposes.
+func (m *Map) Stats() MapStats {
+	stats := MapStats{
 		TotalGrowths: atomic.LoadInt64(&m.totalGrowths),
 		TotalShrinks: atomic.LoadInt64(&m.totalShrinks),
 		MinEntries:   math.MaxInt32,
@@ -829,7 +859,7 @@ func (m *Map) stats() mapStats {
 			if b.next == nil {
 				break
 			}
-			b = (*bucketPadded)(b.next)
+			b = (*bucketPadded)(atomic.LoadPointer(&b.next))
 			stats.TotalBuckets++
 		}
 		if nentries < stats.MinEntries {
diff --git a/mapof.go b/mapof.go
index a41213a..13737b7 100644
--- a/mapof.go
+++ b/mapof.go
@@ -639,9 +639,11 @@ func shiftHash(h uint64) uint64 {
 	return h
 }
 
-// O(N) operation; use for debug purposes only
-func (m *MapOf[K, V]) stats() mapStats {
-	stats := mapStats{
+// Stats returns statistics for the MapOf. Just like other map
+// methods, this one is thread-safe. Yet it's an O(N) operation,
+// so it should be used only for diagnostics or debugging purposes.
+func (m *MapOf[K, V]) Stats() MapStats {
+	stats := MapStats{
 		TotalGrowths: atomic.LoadInt64(&m.totalGrowths),
 		TotalShrinks: atomic.LoadInt64(&m.totalShrinks),
 		MinEntries:   math.MaxInt32,
@@ -670,7 +672,7 @@ func (m *MapOf[K, V]) stats() mapStats {
 			if b.next == nil {
 				break
 			}
-			b = (*bucketOfPadded)(b.next)
+			b = (*bucketOfPadded)(atomic.LoadPointer(&b.next))
 			stats.TotalBuckets++
 		}
 		if nentries < stats.MinEntries {


Test patch:
diff --git a/export_test.go b/export_test.go
index b35b34e..e0abb64 100644
--- a/export_test.go
+++ b/export_test.go
@@ -13,14 +13,6 @@ type (
 	BucketOfPadded = bucketOfPadded
 )
 
-type MapStats struct {
-	mapStats
-}
-
-func CollectMapStats(m *Map) MapStats {
-	return MapStats{m.stats()}
-}
-
 func LockBucket(mu *uint64) {
 	lockBucket(mu)
 }
@@ -69,10 +61,6 @@ func MakeHasher[T comparable]() func(T, uint64) uint64 {
 	return makeHasher[T]()
 }
 
-func CollectMapOfStats[K comparable, V any](m *MapOf[K, V]) MapStats {
-	return MapStats{m.stats()}
-}
-
 func NewMapOfWithHasher[K comparable, V any](
 	hasher func(K, uint64) uint64,
 	options ...func(*MapConfig),
diff --git a/map_test.go b/map_test.go
index ff75bea..a371c75 100644
--- a/map_test.go
+++ b/map_test.go
@@ -495,7 +495,7 @@ func TestMapStoreThenParallelDelete_DoesNotShrinkBelowMinTableLen(t *testing.T)
 	<-cdone
 	<-cdone
 
-	stats := CollectMapStats(m)
+	stats := m.Stats()
 	if stats.RootBuckets != DefaultMinMapTableLen {
 		t.Fatalf("table length was different from the minimum: %d", stats.RootBuckets)
 	}
@@ -566,7 +566,7 @@ func TestMapClear(t *testing.T) {
 }
 
 func assertMapCapacity(t *testing.T, m *Map, expectedCap int) {
-	stats := CollectMapStats(m)
+	stats := m.Stats()
 	if stats.Capacity != expectedCap {
 		t.Fatalf("capacity was different from %d: %d", expectedCap, stats.Capacity)
 	}
@@ -590,7 +590,7 @@ func TestNewMapPresized_DoesNotShrinkBelowMinTableLen(t *testing.T) {
 		m.Store(strconv.Itoa(i), i)
 	}
 
-	stats := CollectMapStats(m)
+	stats := m.Stats()
 	if stats.RootBuckets <= minTableLen {
 		t.Fatalf("table did not grow: %d", stats.RootBuckets)
 	}
@@ -599,7 +599,7 @@ func TestNewMapPresized_DoesNotShrinkBelowMinTableLen(t *testing.T) {
 		m.Delete(strconv.Itoa(int(i)))
 	}
 
-	stats = CollectMapStats(m)
+	stats = m.Stats()
 	if stats.RootBuckets != minTableLen {
 		t.Fatalf("table length was different from the minimum: %d", stats.RootBuckets)
 	}
@@ -610,13 +610,13 @@ func TestNewMapGrowOnly_OnlyShrinksOnClear(t *testing.T) {
 	const numEntries = minTableLen * EntriesPerMapBucket
 	m := NewMap(WithPresize(numEntries), WithGrowOnly())
 
-	stats := CollectMapStats(m)
+	stats := m.Stats()
 	initialTableLen := stats.RootBuckets
 
 	for i := 0; i < 2*numEntries; i++ {
 		m.Store(strconv.Itoa(i), i)
 	}
-	stats = CollectMapStats(m)
+	stats = m.Stats()
 	maxTableLen := stats.RootBuckets
 	if maxTableLen <= minTableLen {
 		t.Fatalf("table did not grow: %d", maxTableLen)
@@ -625,13 +625,13 @@ func TestNewMapGrowOnly_OnlyShrinksOnClear(t *testing.T) {
 	for i := 0; i < numEntries; i++ {
 		m.Delete(strconv.Itoa(int(i)))
 	}
-	stats = CollectMapStats(m)
+	stats = m.Stats()
 	if stats.RootBuckets != maxTableLen {
 		t.Fatalf("table length was different from the expected: %d", stats.RootBuckets)
 	}
 
 	m.Clear()
-	stats = CollectMapStats(m)
+	stats = m.Stats()
 	if stats.RootBuckets != initialTableLen {
 		t.Fatalf("table length was different from the initial: %d", stats.RootBuckets)
 	}
@@ -644,7 +644,7 @@ func TestMapResize(t *testing.T) {
 	for i := 0; i < numEntries; i++ {
 		m.Store(strconv.Itoa(i), i)
 	}
-	stats := CollectMapStats(m)
+	stats := m.Stats()
 	if stats.Size != numEntries {
 		t.Fatalf("size was too small: %d", stats.Size)
 	}
@@ -668,7 +668,7 @@ func TestMapResize(t *testing.T) {
 	for i := 0; i < numEntries; i++ {
 		m.Delete(strconv.Itoa(i))
 	}
-	stats = CollectMapStats(m)
+	stats = m.Stats()
 	if stats.Size > 0 {
 		t.Fatalf("zero size was expected: %d", stats.Size)
 	}
@@ -692,7 +692,7 @@ func TestMapResize_CounterLenLimit(t *testing.T) {
 	for i := 0; i < numEntries; i++ {
 		m.Store("foo"+strconv.Itoa(i), "bar"+strconv.Itoa(i))
 	}
-	stats := CollectMapStats(m)
+	stats := m.Stats()
 	if stats.Size != numEntries {
 		t.Fatalf("size was too small: %d", stats.Size)
 	}
@@ -702,7 +702,7 @@ func TestMapResize_CounterLenLimit(t *testing.T) {
 	}
 }
 
-func parallelSeqResizer(t *testing.T, m *Map, numEntries int, positive bool, cdone chan bool) {
+func parallelSeqResizer(m *Map, numEntries int, positive bool, cdone chan bool) {
 	for i := 0; i < numEntries; i++ {
 		if positive {
 			m.Store(strconv.Itoa(i), i)
@@ -717,8 +717,8 @@ func TestMapParallelResize_GrowOnly(t *testing.T) {
 	const numEntries = 100_000
 	m := NewMap()
 	cdone := make(chan bool)
-	go parallelSeqResizer(t, m, numEntries, true, cdone)
-	go parallelSeqResizer(t, m, numEntries, false, cdone)
+	go parallelSeqResizer(m, numEntries, true, cdone)
+	go parallelSeqResizer(m, numEntries, false, cdone)
 	// Wait for the goroutines to finish.
 	<-cdone
 	<-cdone
@@ -937,7 +937,7 @@ func TestMapParallelStoresAndDeletes(t *testing.T) {
 	}
 }
 
-func parallelComputer(t *testing.T, m *Map, numIters, numEntries int, cdone chan bool) {
+func parallelComputer(m *Map, numIters, numEntries int, cdone chan bool) {
 	for i := 0; i < numIters; i++ {
 		for j := 0; j < numEntries; j++ {
 			m.Compute(strconv.Itoa(j), func(oldValue interface{}, loaded bool) (newValue interface{}, delete bool) {
@@ -957,7 +957,7 @@ func TestMapParallelComputes(t *testing.T) {
 	m := NewMap()
 	cdone := make(chan bool)
 	for i := 0; i < numWorkers; i++ {
-		go parallelComputer(t, m, numIters, numWorkers, cdone)
+		go parallelComputer(m, numIters, numWorkers, cdone)
 	}
 	// Wait for the goroutines to finish.
 	for i := 0; i < numWorkers; i++ {
@@ -975,7 +975,7 @@ func TestMapParallelComputes(t *testing.T) {
 	}
 }
 
-func parallelRangeStorer(t *testing.T, m *Map, numEntries int, stopFlag *int64, cdone chan bool) {
+func parallelRangeStorer(m *Map, numEntries int, stopFlag *int64, cdone chan bool) {
 	for {
 		for i := 0; i < numEntries; i++ {
 			m.Store(strconv.Itoa(i), i)
@@ -987,7 +987,7 @@ func parallelRangeStorer(t *testing.T, m *Map, numEntries int, stopFlag *int64,
 	cdone <- true
 }
 
-func parallelRangeDeleter(t *testing.T, m *Map, numEntries int, stopFlag *int64, cdone chan bool) {
+func parallelRangeDeleter(m *Map, numEntries int, stopFlag *int64, cdone chan bool) {
 	for {
 		for i := 0; i < numEntries; i++ {
 			m.Delete(strconv.Itoa(i))
@@ -1008,8 +1008,8 @@ func TestMapParallelRange(t *testing.T) {
 	// Start goroutines that would be storing and deleting items in parallel.
 	cdone := make(chan bool)
 	stopFlag := int64(0)
-	go parallelRangeStorer(t, m, numEntries, &stopFlag, cdone)
-	go parallelRangeDeleter(t, m, numEntries, &stopFlag, cdone)
+	go parallelRangeStorer(m, numEntries, &stopFlag, cdone)
+	go parallelRangeDeleter(m, numEntries, &stopFlag, cdone)
 	// Iterate the map and verify that no duplicate keys were met.
 	met := make(map[string]int)
 	m.Range(func(key string, value interface{}) bool {
@@ -1211,6 +1211,60 @@ func testMapTopHashMutex_StoreAfterErase(t *testing.T, topHashes *uint64) {
 	}
 }
 
+func TestMapStats(t *testing.T) {
+	m := NewMap()
+
+	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 < 100; i++ {
+		m.Store(strconv.Itoa(int(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 != 100 {
+		t.Fatalf("unexpected size: %d", stats.Size)
+	}
+	if stats.Counter != 100 {
+		t.Fatalf("unexpected counter: %d", stats.Counter)
+	}
+	if stats.CounterLen != 8 {
+		t.Fatalf("unexpected counter length: %d", stats.CounterLen)
+	}
+}
+
 func BenchmarkMap_NoWarmUp(b *testing.B) {
 	for _, bc := range benchmarkCases {
 		if bc.readPercentage == 100 {
diff --git a/mapof_test.go b/mapof_test.go
index c9912be..69bd886 100644
--- a/mapof_test.go
+++ b/mapof_test.go
@@ -540,7 +540,7 @@ func TestMapOfStoreThenParallelDelete_DoesNotShrinkBelowMinTableLen(t *testing.T
 	<-cdone
 	<-cdone
 
-	stats := CollectMapOfStats(m)
+	stats := m.Stats()
 	if stats.RootBuckets != DefaultMinMapTableLen {
 		t.Fatalf("table length was different from the minimum: %d", stats.RootBuckets)
 	}
@@ -611,7 +611,7 @@ func TestMapOfClear(t *testing.T) {
 }
 
 func assertMapOfCapacity[K comparable, V any](t *testing.T, m *MapOf[K, V], expectedCap int) {
-	stats := CollectMapOfStats(m)
+	stats := m.Stats()
 	if stats.Capacity != expectedCap {
 		t.Fatalf("capacity was different from %d: %d", expectedCap, stats.Capacity)
 	}
@@ -639,7 +639,7 @@ func TestNewMapOfPresized_DoesNotShrinkBelowMinTableLen(t *testing.T) {
 		m.Store(i, i)
 	}
 
-	stats := CollectMapOfStats(m)
+	stats := m.Stats()
 	if stats.RootBuckets <= minTableLen {
 		t.Fatalf("table did not grow: %d", stats.RootBuckets)
 	}
@@ -648,7 +648,7 @@ func TestNewMapOfPresized_DoesNotShrinkBelowMinTableLen(t *testing.T) {
 		m.Delete(i)
 	}
 
-	stats = CollectMapOfStats(m)
+	stats = m.Stats()
 	if stats.RootBuckets != minTableLen {
 		t.Fatalf("table length was different from the minimum: %d", stats.RootBuckets)
 	}
@@ -659,13 +659,13 @@ func TestNewMapOfGrowOnly_OnlyShrinksOnClear(t *testing.T) {
 	const numEntries = minTableLen * EntriesPerMapBucket
 	m := NewMapOf[int, int](WithPresize(numEntries), WithGrowOnly())
 
-	stats := CollectMapOfStats(m)
+	stats := m.Stats()
 	initialTableLen := stats.RootBuckets
 
 	for i := 0; i < 2*numEntries; i++ {
 		m.Store(i, i)
 	}
-	stats = CollectMapOfStats(m)
+	stats = m.Stats()
 	maxTableLen := stats.RootBuckets
 	if maxTableLen <= minTableLen {
 		t.Fatalf("table did not grow: %d", maxTableLen)
@@ -674,13 +674,13 @@ func TestNewMapOfGrowOnly_OnlyShrinksOnClear(t *testing.T) {
 	for i := 0; i < numEntries; i++ {
 		m.Delete(i)
 	}
-	stats = CollectMapOfStats(m)
+	stats = m.Stats()
 	if stats.RootBuckets != maxTableLen {
 		t.Fatalf("table length was different from the expected: %d", stats.RootBuckets)
 	}
 
 	m.Clear()
-	stats = CollectMapOfStats(m)
+	stats = m.Stats()
 	if stats.RootBuckets != initialTableLen {
 		t.Fatalf("table length was different from the initial: %d", stats.RootBuckets)
 	}
@@ -693,7 +693,7 @@ func TestMapOfResize(t *testing.T) {
 	for i := 0; i < numEntries; i++ {
 		m.Store(strconv.Itoa(i), i)
 	}
-	stats := CollectMapOfStats(m)
+	stats := m.Stats()
 	if stats.Size != numEntries {
 		t.Fatalf("size was too small: %d", stats.Size)
 	}
@@ -717,7 +717,7 @@ func TestMapOfResize(t *testing.T) {
 	for i := 0; i < numEntries; i++ {
 		m.Delete(strconv.Itoa(i))
 	}
-	stats = CollectMapOfStats(m)
+	stats = m.Stats()
 	if stats.Size > 0 {
 		t.Fatalf("zero size was expected: %d", stats.Size)
 	}
@@ -741,7 +741,7 @@ func TestMapOfResize_CounterLenLimit(t *testing.T) {
 	for i := 0; i < numEntries; i++ {
 		m.Store("foo"+strconv.Itoa(i), "bar"+strconv.Itoa(i))
 	}
-	stats := CollectMapOfStats(m)
+	stats := m.Stats()
 	if stats.Size != numEntries {
 		t.Fatalf("size was too small: %d", stats.Size)
 	}
@@ -751,7 +751,7 @@ func TestMapOfResize_CounterLenLimit(t *testing.T) {
 	}
 }
 
-func parallelSeqTypedResizer(t *testing.T, m *MapOf[int, int], numEntries int, positive bool, cdone chan bool) {
+func parallelSeqTypedResizer(m *MapOf[int, int], numEntries int, positive bool, cdone chan bool) {
 	for i := 0; i < numEntries; i++ {
 		if positive {
 			m.Store(i, i)
@@ -766,8 +766,8 @@ func TestMapOfParallelResize_GrowOnly(t *testing.T) {
 	const numEntries = 100_000
 	m := NewMapOf[int, int]()
 	cdone := make(chan bool)
-	go parallelSeqTypedResizer(t, m, numEntries, true, cdone)
-	go parallelSeqTypedResizer(t, m, numEntries, false, cdone)
+	go parallelSeqTypedResizer(m, numEntries, true, cdone)
+	go parallelSeqTypedResizer(m, numEntries, false, cdone)
 	// Wait for the goroutines to finish.
 	<-cdone
 	<-cdone
@@ -986,7 +986,7 @@ func TestMapOfParallelStoresAndDeletes(t *testing.T) {
 	}
 }
 
-func parallelTypedComputer(t *testing.T, m *MapOf[uint64, uint64], numIters, numEntries int, cdone chan bool) {
+func parallelTypedComputer(m *MapOf[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, delete bool) {
@@ -1003,7 +1003,7 @@ func TestMapOfParallelComputes(t *testing.T) {
 	m := NewMapOf[uint64, uint64]()
 	cdone := make(chan bool)
 	for i := 0; i < numWorkers; i++ {
-		go parallelTypedComputer(t, m, numIters, numWorkers, cdone)
+		go parallelTypedComputer(m, numIters, numWorkers, cdone)
 	}
 	// Wait for the goroutines to finish.
 	for i := 0; i < numWorkers; i++ {
@@ -1021,7 +1021,7 @@ func TestMapOfParallelComputes(t *testing.T) {
 	}
 }
 
-func parallelTypedRangeStorer(t *testing.T, m *MapOf[int, int], numEntries int, stopFlag *int64, cdone chan bool) {
+func parallelTypedRangeStorer(m *MapOf[int, int], numEntries int, stopFlag *int64, cdone chan bool) {
 	for {
 		for i := 0; i < numEntries; i++ {
 			m.Store(i, i)
@@ -1033,7 +1033,7 @@ func parallelTypedRangeStorer(t *testing.T, m *MapOf[int, int], numEntries int,
 	cdone <- true
 }
 
-func parallelTypedRangeDeleter(t *testing.T, m *MapOf[int, int], numEntries int, stopFlag *int64, cdone chan bool) {
+func parallelTypedRangeDeleter(m *MapOf[int, int], numEntries int, stopFlag *int64, cdone chan bool) {
 	for {
 		for i := 0; i < numEntries; i++ {
 			m.Delete(i)
@@ -1054,8 +1054,8 @@ func TestMapOfParallelRange(t *testing.T) {
 	// Start goroutines that would be storing and deleting items in parallel.
 	cdone := make(chan bool)
 	stopFlag := int64(0)
-	go parallelTypedRangeStorer(t, m, numEntries, &stopFlag, cdone)
-	go parallelTypedRangeDeleter(t, m, numEntries, &stopFlag, cdone)
+	go parallelTypedRangeStorer(m, numEntries, &stopFlag, cdone)
+	go parallelTypedRangeDeleter(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 {
@@ -1127,6 +1127,60 @@ func TestMapOfDoesNotLoseEntriesOnResize(t *testing.T) {
 	}
 }
 
+func TestMapOfStats(t *testing.T) {
+	m := NewMapOf[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 < 100; 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 != 100 {
+		t.Fatalf("unexpected size: %d", stats.Size)
+	}
+	if stats.Counter != 100 {
+		t.Fatalf("unexpected counter: %d", stats.Counter)
+	}
+	if stats.CounterLen != 8 {
+		t.Fatalf("unexpected counter length: %d", stats.CounterLen)
+	}
+}
+
 func BenchmarkMapOf_NoWarmUp(b *testing.B) {
 	for _, bc := range benchmarkCases {
 		if bc.readPercentage == 100 {