github.com/fufuok/freelru@v0.13.3/lru_test.go (about) 1 // Licensed to Elasticsearch B.V. under one or more contributor 2 // license agreements. See the NOTICE file distributed with 3 // this work for additional information regarding copyright 4 // ownership. Elasticsearch B.V. licenses this file to you under 5 // the Apache License, Version 2.0 (the "License"); you may 6 // not use this file except in compliance with the License. 7 // You may obtain a copy of the License at 8 // 9 // http://www.apache.org/licenses/LICENSE-2.0 10 // 11 // Unless required by applicable law or agreed to in writing, 12 // software distributed under the License is distributed on an 13 // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 // KIND, either express or implied. See the License for the 15 // specific language governing permissions and limitations 16 // under the License. 17 18 package freelru 19 20 import ( 21 "encoding/binary" 22 "math/rand" 23 "testing" 24 "time" 25 ) 26 27 const ( 28 // FNV-1a 29 offset32 = uint32(2166136261) 30 prime32 = uint32(16777619) 31 32 // Init32 is what 32 bits hash values should be initialized with. 33 Init32 = offset32 34 ) 35 36 func hashUint64(i uint64) uint32 { 37 b := [8]byte{} 38 binary.BigEndian.PutUint64(b[:], i) 39 h := Init32 40 h = (h ^ uint32(b[0])) * prime32 41 h = (h ^ uint32(b[1])) * prime32 42 h = (h ^ uint32(b[2])) * prime32 43 h = (h ^ uint32(b[3])) * prime32 44 h = (h ^ uint32(b[4])) * prime32 45 h = (h ^ uint32(b[5])) * prime32 46 h = (h ^ uint32(b[6])) * prime32 47 h = (h ^ uint32(b[7])) * prime32 48 return h 49 } 50 51 func setupCache(t *testing.T, cache Cache[uint64, uint64], evictCounter *uint64) Cache[uint64, uint64] { 52 onEvict := func(k uint64, v uint64) { 53 FatalIf(t, k+1 != v, "Evict value not matching (%v+1 != %v)", k, v) 54 if evictCounter != nil { 55 *evictCounter++ 56 } 57 } 58 59 cache.SetOnEvict(onEvict) 60 61 return cache 62 } 63 64 func makeCacheWithHasher(t *testing.T, capacity uint32, evictCounter *uint64) Cache[uint64, uint64] { 65 cache, err := New[uint64, uint64](capacity, hashUint64) 66 FatalIf(t, err != nil, "Failed to create LRU: %v", err) 67 68 return setupCache(t, cache, evictCounter) 69 } 70 71 func makeCache(t *testing.T, capacity uint32, evictCounter *uint64) Cache[uint64, uint64] { 72 cache, err := NewDefault[uint64, uint64](capacity) 73 FatalIf(t, err != nil, "Failed to create LRU: %v", err) 74 75 return setupCache(t, cache, evictCounter) 76 } 77 78 func makeSyncedLRUWithHasher(t *testing.T, capacity uint32, evictCounter *uint64) Cache[uint64, uint64] { 79 cache, err := NewSynced[uint64, uint64](capacity, hashUint64) 80 FatalIf(t, err != nil, "Failed to create SyncedLRU: %v", err) 81 82 return setupCache(t, cache, evictCounter) 83 } 84 85 func makeSyncedLRU(t *testing.T, capacity uint32, evictCounter *uint64) Cache[uint64, uint64] { 86 cache, err := NewSyncedDefault[uint64, uint64](capacity) 87 FatalIf(t, err != nil, "Failed to create SyncedLRU: %v", err) 88 89 return setupCache(t, cache, evictCounter) 90 } 91 92 func TestLRU(t *testing.T) { 93 const CAP = 32 94 95 evictCounter := uint64(0) 96 testCache(t, CAP, makeCache(t, CAP, &evictCounter), &evictCounter) 97 evictCounter = uint64(0) 98 testCache(t, CAP, makeCacheWithHasher(t, CAP, &evictCounter), &evictCounter) 99 } 100 101 func TestSyncedLRU(t *testing.T) { 102 const CAP = 32 103 104 evictCounter := uint64(0) 105 testCache(t, CAP, makeSyncedLRU(t, CAP, &evictCounter), &evictCounter) 106 evictCounter = uint64(0) 107 testCache(t, CAP, makeSyncedLRUWithHasher(t, CAP, &evictCounter), &evictCounter) 108 } 109 110 func testCache(t *testing.T, cAP uint64, cache Cache[uint64, uint64], evictCounter *uint64) { //nolint:unparam 111 for i := uint64(0); i < cAP*2; i++ { 112 cache.Add(i, i+1) 113 } 114 FatalIf(t, cache.Len() != int(cAP), "Unexpected number of entries: %v (!= %d)", cache.Len(), cAP) 115 FatalIf(t, *evictCounter != cAP, "Unexpected number of evictions: %v (!= %d)", evictCounter, cAP) 116 117 keys := cache.Keys() 118 for i, k := range keys { 119 if v, ok := cache.Get(k); !ok || v != k+1 || v != uint64(i+int(cAP)+1) { 120 t.Fatalf("Mismatch of key %v (ok=%v v=%v)\n%v", k, ok, v, keys) 121 } 122 } 123 124 for i := uint64(0); i < cAP; i++ { 125 if _, ok := cache.Get(i); ok { 126 t.Fatalf("Missing eviction of %d", i) 127 } 128 } 129 130 for i := cAP; i < cAP*2; i++ { 131 if _, ok := cache.Get(i); !ok { 132 t.Fatalf("Unexpected eviction of %d", i) 133 } 134 } 135 136 FatalIf(t, cache.Remove(cAP*2), "Unexpected success removing %d", cAP*2) 137 FatalIf(t, !cache.Remove(cAP*2-1), "Failed to remove most recent entry %d", cAP*2-1) 138 FatalIf(t, !cache.Remove(cAP), "Failed to remove oldest entry %d", cAP) 139 FatalIf(t, *evictCounter != cAP+2, "Unexpected # of evictions: %d (!= %d)", evictCounter, cAP+2) 140 FatalIf(t, cache.Len() != int(cAP-2), "Unexpected # of entries: %d (!= %d)", cache.Len(), cAP-2) 141 } 142 143 func TestLRU_Add(t *testing.T) { 144 evictCounter := uint64(0) 145 cache := makeCache(t, 1, &evictCounter) 146 147 FatalIf(t, cache.Add(1, 2) == true || evictCounter != 0, "Unexpected eviction") 148 FatalIf(t, cache.Add(3, 4) == false || evictCounter != 1, "Missing eviction") 149 } 150 151 func TestSyncedLRU_Add(t *testing.T) { 152 evictCounter := uint64(0) 153 cache := makeSyncedLRU(t, 1, &evictCounter) 154 155 FatalIf(t, cache.Add(1, 2) == true || evictCounter != 0, "Unexpected eviction") 156 FatalIf(t, cache.Add(3, 4) == false || evictCounter != 1, "Missing eviction") 157 } 158 159 func TestLRU_Remove(t *testing.T) { 160 evictCounter := uint64(0) 161 cache := makeCache(t, 2, &evictCounter) 162 cache.Add(1, 2) 163 cache.Add(3, 4) 164 165 FatalIf(t, !cache.Remove(1), "Failed to remove most recent entry %d", 1) 166 FatalIf(t, !cache.Remove(3), "Failed to remove most recent entry %d", 3) 167 FatalIf(t, evictCounter != 2, "Unexpected # of evictions: %d (!= %d)", evictCounter, 2) 168 FatalIf(t, cache.Len() != 0, "Unexpected # of entries: %d (!= %d)", cache.Len(), 0) 169 } 170 171 func TestSyncedLRU_Remove(t *testing.T) { 172 evictCounter := uint64(0) 173 cache := makeSyncedLRU(t, 2, &evictCounter) 174 cache.Add(1, 2) 175 cache.Add(3, 4) 176 177 FatalIf(t, !cache.Remove(1), "Failed to remove most recent entry %d", 1) 178 FatalIf(t, !cache.Remove(3), "Failed to remove most recent entry %d", 3) 179 FatalIf(t, evictCounter != 2, "Unexpected # of evictions: %d (!= %d)", evictCounter, 2) 180 FatalIf(t, cache.Len() != 0, "Unexpected # of entries: %d (!= %d)", cache.Len(), 0) 181 } 182 183 func testCacheAddWithExpire(t *testing.T, cache Cache[uint64, uint64]) { 184 // check for LRU default lifetime + element specific override 185 cache.SetLifetime(100 * time.Millisecond) 186 cache.Add(1, 2) 187 cache.AddWithLifetime(3, 4, 200*time.Millisecond) 188 _, ok := cache.Get(1) 189 FatalIf(t, !ok, "Failed to get") 190 time.Sleep(101 * time.Millisecond) 191 _, ok = cache.Get(1) 192 FatalIf(t, ok, "Expected expiration did not happen") 193 _, ok = cache.Get(3) 194 FatalIf(t, !ok, "Failed to get") 195 time.Sleep(100 * time.Millisecond) 196 _, ok = cache.Get(3) 197 FatalIf(t, ok, "Expected expiration did not happen") 198 199 // check for element specific lifetime 200 cache.Purge() 201 cache.SetLifetime(0) 202 cache.AddWithLifetime(1, 2, 100*time.Millisecond) 203 _, ok = cache.Get(1) 204 FatalIf(t, !ok, "Failed to get") 205 time.Sleep(101 * time.Millisecond) 206 _, ok = cache.Get(1) 207 FatalIf(t, ok, "Expected expiration did not happen") 208 } 209 210 func TestLRU_AddWithExpire(t *testing.T) { 211 testCacheAddWithExpire(t, makeCache(t, 2, nil)) 212 } 213 214 func TestSyncedLRU_AddWithExpire(t *testing.T) { 215 testCacheAddWithExpire(t, makeSyncedLRU(t, 2, nil)) 216 } 217 218 func TestLRUMatch(t *testing.T) { 219 testCacheMatch(t, makeCache(t, 2, nil), 128) 220 } 221 222 func TestSyncedLRUMatch(t *testing.T) { 223 testCacheMatch(t, makeCache(t, 2, nil), 128) 224 } 225 226 // Test that Go map and the Cache stay in sync when adding 227 // and randomly removing elements. 228 func testCacheMatch(t *testing.T, cache Cache[uint64, uint64], cAP int) { 229 backup := make(map[uint64]uint64, cAP) 230 231 onEvict := func(k uint64, v uint64) { 232 FatalIf(t, k != v, "Evict value not matching (%v != %v)", k, v) 233 delete(backup, k) 234 } 235 236 cache.SetOnEvict(onEvict) 237 238 for i := uint64(0); i < 100000; i++ { 239 cache.Add(i, i) 240 backup[i] = i 241 242 // ~33% chance to remove a random element 243 r := i - uint64(rand.Int()%(cAP*3)) // nolint:gosec 244 cache.Remove(r) 245 246 FatalIf(t, cache.Len() != len(backup), "Len does not match (%d vs %d)", 247 cache.Len(), len(backup)) 248 249 keys := cache.Keys() 250 FatalIf(t, len(keys) != len(backup), "Number of keys does not match (%d vs %d)", 251 len(keys), len(backup)) 252 253 for _, key := range keys { 254 backupVal, ok := backup[key] 255 FatalIf(t, !ok, "Failed to find key %d in map", key) 256 257 val, ok := cache.Peek(key) 258 FatalIf(t, !ok, "Failed to find key %d in Cache", key) 259 260 FatalIf(t, backupVal != val, "Unexpected mismatch of values: %#v %#v", 261 backupVal, val) 262 } 263 264 for k, v := range backup { 265 val, ok := cache.Peek(k) 266 FatalIf(t, !ok, "Failed to find key %d in Cache (i=%d)", k, i) 267 FatalIf(t, v != val, "Unexpected mismatch of values: %#v %#v", v, val) 268 } 269 } 270 } 271 272 func FatalIf(t *testing.T, fail bool, fmt string, args ...any) { 273 if fail { 274 t.Logf(fmt, args...) 275 panic(fail) 276 } 277 } 278 279 // This following tests are for memory comparison. 280 // See also TestSimpleLRUAdd in the bench/ directory. 281 282 const count = 1000 283 284 // GOGC=off go test -memprofile=mem.out -test.memprofilerate=1 -count 1 -run TestMapAdd 285 // go tool pprof mem.out 286 // (then check the top10) 287 func TestMapAdd(_ *testing.T) { 288 cache := make(map[uint64]uint64, count) 289 290 var val uint64 291 for i := uint64(0); i < count; i++ { 292 cache[i] = val 293 } 294 } 295 296 // GOGC=off go test -memprofile=mem.out -test.memprofilerate=1 -count 1 -run TestLRUAdd 297 // go tool pprof mem.out 298 // (then check the top10) 299 func TestLRUAdd(t *testing.T) { 300 cache := makeCache(t, count, nil) 301 302 var val uint64 303 for i := uint64(0); i < count; i++ { 304 cache.Add(i, val) 305 } 306 } 307 308 // GOGC=off go test -memprofile=mem.out -test.memprofilerate=1 -count 1 -run TestSyncedLRUAdd 309 // go tool pprof mem.out 310 // (then check the top10) 311 func TestSyncedLRUAdd(t *testing.T) { 312 cache := makeSyncedLRU(t, count, nil) 313 314 var val uint64 315 for i := uint64(0); i < count; i++ { 316 cache.Add(i, val) 317 } 318 } 319 320 func TestLRUMetrics(t *testing.T) { 321 cache := makeCache(t, 1, nil) 322 testMetrics(t, cache) 323 324 lru, _ := NewDefault[string, struct{}](3, time.Second*3) 325 m := lru.Metrics() 326 FatalIf(t, m.Capacity != 3, "Unexpected capacity: %d (!= %d)", m.Capacity, 3) 327 FatalIf(t, m.Lifetime != "3s", "Unexpected lifetime: %s (!= %s)", m.Lifetime, "3s") 328 329 lru.ResetMetrics() 330 m = lru.Metrics() 331 FatalIf(t, m.Capacity != 3, "Unexpected capacity: %d (!= %d)", m.Capacity, 3) 332 FatalIf(t, m.Lifetime != "3s", "Unexpected lifetime: %s (!= %s)", m.Lifetime, "3s") 333 } 334 335 func testMetrics(t *testing.T, cache Cache[uint64, uint64]) { 336 cache.Add(1, 2) // insert 337 cache.Add(3, 4) // insert and eviction 338 cache.Get(1) // miss 339 cache.Get(3) // hit 340 cache.Remove(3) // removal 341 342 m := cache.Metrics() 343 FatalIf(t, m.Inserts != 2, "Unexpected inserts: %d (!= %d)", m.Inserts, 2) 344 FatalIf(t, m.Hits != 1, "Unexpected hits: %d (!= %d)", m.Hits, 1) 345 FatalIf(t, m.Misses != 1, "Unexpected misses: %d (!= %d)", m.Misses, 1) 346 FatalIf(t, m.Evictions != 1, "Unexpected evictions: %d (!= %d)", m.Evictions, 1) 347 FatalIf(t, m.Removals != 1, "Unexpected evictions: %d (!= %d)", m.Removals, 1) 348 FatalIf(t, m.Collisions != 0, "Unexpected collisions: %d (!= %d)", m.Collisions, 0) 349 FatalIf(t, m.Capacity != 1, "Unexpected capacity: %d (!= %d)", m.Capacity, 1) 350 FatalIf(t, m.Lifetime != "0s", "Unexpected lifetime: %s (!= %s)", m.Lifetime, "0s") 351 FatalIf(t, m.Len != 0, "Unexpected len: %d (!= %d)", m.Len, 0) 352 353 cache.Add(4, 4) 354 m = cache.Metrics() 355 FatalIf(t, m.Len != 1, "Unexpected len: %d (!= %d)", m.Len, 1) 356 357 cache.Purge() 358 m = cache.Metrics() 359 FatalIf(t, m.Inserts != 0, "Unexpected inserts: %d (!= %d)", m.Inserts, 0) 360 FatalIf(t, m.Hits != 0, "Unexpected hits: %d (!= %d)", m.Hits, 0) 361 FatalIf(t, m.Misses != 0, "Unexpected misses: %d (!= %d)", m.Misses, 0) 362 FatalIf(t, m.Evictions != 0, "Unexpected evictions: %d (!= %d)", m.Evictions, 0) 363 FatalIf(t, m.Removals != 0, "Unexpected evictions: %d (!= %d)", m.Removals, 0) 364 FatalIf(t, m.Collisions != 0, "Unexpected collisions: %d (!= %d)", m.Collisions, 0) 365 FatalIf(t, m.Capacity != 1, "Unexpected capacity: %d (!= %d)", m.Capacity, 1) 366 FatalIf(t, m.Lifetime != "0s", "Unexpected lifetime: %s (!= %s)", m.Lifetime, "0s") 367 FatalIf(t, m.Len != 0, "Unexpected len: %d (!= %d)", m.Len, 0) 368 }