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  }