diff --git a/core/hotspot/cache/concurrent_cache.go b/core/hotspot/cache/concurrent_cache.go index de96e6c65..704fc1be3 100644 --- a/core/hotspot/cache/concurrent_cache.go +++ b/core/hotspot/cache/concurrent_cache.go @@ -14,6 +14,8 @@ package cache +import "github.com/alibaba/sentinel-golang/core/hotspot/cache/stats" + // ConcurrentCounterCache cache the hotspot parameter type ConcurrentCounterCache interface { // Add add a value to the cache, @@ -43,4 +45,7 @@ type ConcurrentCounterCache interface { // Purge clears all cache entries. Purge() + + // Stats copies cache stats. + Stats() (*stats.CacheStats, error) } diff --git a/core/hotspot/cache/concurrent_lru.go b/core/hotspot/cache/lru/concurrent_lru.go similarity index 71% rename from core/hotspot/cache/concurrent_lru.go rename to core/hotspot/cache/lru/concurrent_lru.go index ad8de8e48..80fa7f9f1 100644 --- a/core/hotspot/cache/concurrent_lru.go +++ b/core/hotspot/cache/lru/concurrent_lru.go @@ -12,30 +12,33 @@ // See the License for the specific language governing permissions and // limitations under the License. -package cache +package lru import ( "sync" + + "github.com/alibaba/sentinel-golang/core/hotspot/cache" + "github.com/alibaba/sentinel-golang/core/hotspot/cache/stats" ) // LruCacheMap use LRU strategy to cache the most frequently accessed hotspot parameter type LruCacheMap struct { // Not thread safe - lru *LRU - lock *sync.RWMutex + lru *LRU + sync.RWMutex } func (c *LruCacheMap) Add(key interface{}, value *int64) { - c.lock.Lock() - defer c.lock.Unlock() + c.Lock() + defer c.Unlock() c.lru.Add(key, value) return } func (c *LruCacheMap) AddIfAbsent(key interface{}, value *int64) (priorValue *int64) { - c.lock.Lock() - defer c.lock.Unlock() + c.Lock() + defer c.Unlock() val := c.lru.AddIfAbsent(key, value) if val == nil { return nil @@ -45,8 +48,8 @@ func (c *LruCacheMap) AddIfAbsent(key interface{}, value *int64) (priorValue *in } func (c *LruCacheMap) Get(key interface{}) (value *int64, isFound bool) { - c.lock.Lock() - defer c.lock.Unlock() + c.Lock() + defer c.Unlock() val, found := c.lru.Get(key) if found { @@ -56,47 +59,52 @@ func (c *LruCacheMap) Get(key interface{}) (value *int64, isFound bool) { } func (c *LruCacheMap) Remove(key interface{}) (isFound bool) { - c.lock.Lock() - defer c.lock.Unlock() + c.Lock() + defer c.Unlock() return c.lru.Remove(key) } func (c *LruCacheMap) Contains(key interface{}) (ok bool) { - c.lock.RLock() - defer c.lock.RUnlock() + c.RLock() + defer c.RUnlock() return c.lru.Contains(key) } func (c *LruCacheMap) Keys() []interface{} { - c.lock.RLock() - defer c.lock.RUnlock() + c.RLock() + defer c.RUnlock() return c.lru.Keys() } func (c *LruCacheMap) Len() int { - c.lock.RLock() - defer c.lock.RUnlock() + c.RLock() + defer c.RUnlock() return c.lru.Len() } func (c *LruCacheMap) Purge() { - c.lock.Lock() - defer c.lock.Unlock() + c.Lock() + defer c.Unlock() c.lru.Purge() } -func NewLRUCacheMap(size int) ConcurrentCounterCache { - lru, err := NewLRU(size, nil) +func (c *LruCacheMap) Stats() (*stats.CacheStats, error) { + c.RUnlock() + defer c.RUnlock() + return c.lru.Stats() +} + +func NewLRUCacheMap(size int, isRecordingStats bool) cache.ConcurrentCounterCache { + lru, err := NewLRU(size, nil, isRecordingStats) if err != nil { return nil } return &LruCacheMap{ - lru: lru, - lock: new(sync.RWMutex), + lru: lru, } } diff --git a/core/hotspot/cache/concurrent_lru_benchmark_test.go b/core/hotspot/cache/lru/concurrent_lru_benchmark_test.go similarity index 93% rename from core/hotspot/cache/concurrent_lru_benchmark_test.go rename to core/hotspot/cache/lru/concurrent_lru_benchmark_test.go index 0d07d90dd..630c9d254 100644 --- a/core/hotspot/cache/concurrent_lru_benchmark_test.go +++ b/core/hotspot/cache/lru/concurrent_lru_benchmark_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package cache +package lru import ( "strconv" @@ -22,7 +22,7 @@ import ( const CacheSize = 50000 func Benchmark_LRU_AddIfAbsent(b *testing.B) { - c := NewLRUCacheMap(CacheSize) + c := NewLRUCacheMap(CacheSize, false) for a := 1; a <= CacheSize; a++ { val := new(int64) *val = int64(a) @@ -42,7 +42,7 @@ func Benchmark_LRU_AddIfAbsent(b *testing.B) { } func Benchmark_LRU_Add(b *testing.B) { - c := NewLRUCacheMap(CacheSize) + c := NewLRUCacheMap(CacheSize, false) for a := 1; a <= CacheSize; a++ { val := new(int64) *val = int64(a) @@ -59,7 +59,7 @@ func Benchmark_LRU_Add(b *testing.B) { } func Benchmark_LRU_Get(b *testing.B) { - c := NewLRUCacheMap(CacheSize) + c := NewLRUCacheMap(CacheSize, false) for a := 1; a <= CacheSize; a++ { val := new(int64) *val = int64(a) diff --git a/core/hotspot/cache/concurrent_lru_test.go b/core/hotspot/cache/lru/concurrent_lru_test.go similarity index 93% rename from core/hotspot/cache/concurrent_lru_test.go rename to core/hotspot/cache/lru/concurrent_lru_test.go index cdec3081a..628fee6c9 100644 --- a/core/hotspot/cache/concurrent_lru_test.go +++ b/core/hotspot/cache/lru/concurrent_lru_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package cache +package lru import ( "strconv" @@ -23,7 +23,7 @@ import ( func Test_concurrentLruCounterCacheMap_Add_Get(t *testing.T) { t.Run("Test_concurrentLruCounterCacheMap_Add_Get", func(t *testing.T) { - c := NewLRUCacheMap(100) + c := NewLRUCacheMap(100, false) for i := 1; i <= 100; i++ { val := int64(i) c.Add(strconv.Itoa(i), &val) @@ -36,7 +36,7 @@ func Test_concurrentLruCounterCacheMap_Add_Get(t *testing.T) { func Test_concurrentLruCounterCacheMap_AddIfAbsent(t *testing.T) { t.Run("Test_concurrentLruCounterCacheMap_AddIfAbsent", func(t *testing.T) { - c := NewLRUCacheMap(100) + c := NewLRUCacheMap(100, false) for i := 1; i <= 99; i++ { val := int64(i) c.Add(strconv.Itoa(i), &val) @@ -52,7 +52,7 @@ func Test_concurrentLruCounterCacheMap_AddIfAbsent(t *testing.T) { func Test_concurrentLruCounterCacheMap_Contains(t *testing.T) { t.Run("Test_concurrentLruCounterCacheMap_Contains", func(t *testing.T) { - c := NewLRUCacheMap(100) + c := NewLRUCacheMap(100, false) for i := 1; i <= 100; i++ { val := int64(i) c.Add(strconv.Itoa(i), &val) @@ -69,7 +69,7 @@ func Test_concurrentLruCounterCacheMap_Contains(t *testing.T) { func Test_concurrentLruCounterCacheMap_Keys(t *testing.T) { t.Run("Test_concurrentLruCounterCacheMap_Add", func(t *testing.T) { - c := NewLRUCacheMap(100) + c := NewLRUCacheMap(100, false) for i := 1; i <= 100; i++ { val := int64(i) c.Add(strconv.Itoa(i), &val) @@ -82,7 +82,7 @@ func Test_concurrentLruCounterCacheMap_Keys(t *testing.T) { func Test_concurrentLruCounterCacheMap_Purge(t *testing.T) { t.Run("Test_concurrentLruCounterCacheMap_Add", func(t *testing.T) { - c := NewLRUCacheMap(100) + c := NewLRUCacheMap(100, false) for i := 1; i <= 100; i++ { val := int64(i) c.Add(strconv.Itoa(i), &val) @@ -95,7 +95,7 @@ func Test_concurrentLruCounterCacheMap_Purge(t *testing.T) { func Test_concurrentLruCounterCacheMap_Remove(t *testing.T) { t.Run("Test_concurrentLruCounterCacheMap_Add", func(t *testing.T) { - c := NewLRUCacheMap(100) + c := NewLRUCacheMap(100, false) for i := 1; i <= 100; i++ { val := int64(i) c.Add(strconv.Itoa(i), &val) diff --git a/core/hotspot/cache/lru.go b/core/hotspot/cache/lru/lru.go similarity index 86% rename from core/hotspot/cache/lru.go rename to core/hotspot/cache/lru/lru.go index f1ef53033..19149b033 100644 --- a/core/hotspot/cache/lru.go +++ b/core/hotspot/cache/lru/lru.go @@ -12,11 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -package cache +package lru import ( "container/list" + "github.com/alibaba/sentinel-golang/core/hotspot/cache/stats" "github.com/pkg/errors" ) @@ -29,6 +30,7 @@ type LRU struct { evictList *list.List items map[interface{}]*list.Element onEvict EvictCallback + stats *stats.CacheStats } // entry is used to hold a value in the evictList @@ -38,15 +40,20 @@ type entry struct { } // NewLRU constructs an LRU of the given size -func NewLRU(size int, onEvict EvictCallback) (*LRU, error) { +func NewLRU(size int, onEvict EvictCallback, isRecordingStats bool) (*LRU, error) { if size <= 0 { return nil, errors.New("must provide a positive size") } + var statsCache *stats.CacheStats + if isRecordingStats { + statsCache = stats.NewCacheStats() + } c := &LRU{ size: size, evictList: list.New(), items: make(map[interface{}]*list.Element, 64), onEvict: onEvict, + stats: statsCache, } return c, nil } @@ -78,6 +85,9 @@ func (c *LRU) Add(key, value interface{}) { evict := c.evictList.Len() > c.size // Verify size not exceeded if evict { + if c.stats != nil { + c.stats.RecordEviction() + } c.removeOldest() } return @@ -100,6 +110,9 @@ func (c *LRU) AddIfAbsent(key interface{}, value interface{}) (priorValue interf evict := c.evictList.Len() > c.size // Verify size not exceeded if evict { + if c.stats != nil { + c.stats.RecordEviction() + } c.removeOldest() } return nil @@ -110,10 +123,19 @@ func (c *LRU) Get(key interface{}) (value interface{}, isFound bool) { if ent, ok := c.items[key]; ok { c.evictList.MoveToFront(ent) if ent.Value.(*entry) == nil { + if c.stats != nil { + c.stats.RecordMisses() + } return nil, false } + if c.stats != nil { + c.stats.RecordHits() + } return ent.Value.(*entry).value, true } + if c.stats != nil { + c.stats.RecordMisses() + } return } @@ -217,3 +239,11 @@ func (c *LRU) removeElement(e *list.Element) { c.onEvict(kv.key, kv.value) } } + +// Stats copies cache stats. +func (c LRU) Stats() (*stats.CacheStats, error) { + if c.stats == nil { + return nil, errors.New("RecordingStats Must be enabled") + } + return c.stats.Snapshot(), nil +} diff --git a/core/hotspot/cache/stats/cache_stats.go b/core/hotspot/cache/stats/cache_stats.go new file mode 100644 index 000000000..be4f1d4c3 --- /dev/null +++ b/core/hotspot/cache/stats/cache_stats.go @@ -0,0 +1,90 @@ +// Copyright 1999-2020 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package stats + +import ( + "sync/atomic" +) + +// CacheStats is statistics about a cache. +type CacheStats struct { + hitCount *uint64 + missCount *uint64 + evictionCount *uint64 +} + +func NewCacheStats() *CacheStats { + return createCacheStats(0, 0, 0) +} + +func createCacheStats(hitCount uint64, missCount uint64, evictionCount uint64) *CacheStats { + cs := &CacheStats{ + hitCount: new(uint64), + missCount: new(uint64), + evictionCount: new(uint64), + } + *cs.hitCount = hitCount + *cs.missCount = missCount + *cs.evictionCount = evictionCount + return cs +} + +func (s *CacheStats) HitCount() uint64 { + return atomic.LoadUint64(s.hitCount) +} + +func (s *CacheStats) MissCount() uint64 { + return atomic.LoadUint64(s.missCount) +} + +func (s *CacheStats) RequestCount() uint64 { + return s.HitCount() + s.MissCount() +} + +func (s *CacheStats) EvictionCount() uint64 { + return atomic.LoadUint64(s.evictionCount) +} + +func (s *CacheStats) HitRate() float64 { + requestCount := s.RequestCount() + if requestCount == 0 { + return 1.0 + } + return float64(s.HitCount()) / float64(requestCount) +} + +func (s *CacheStats) MissRate() float64 { + requestCount := s.RequestCount() + if requestCount == 0 { + return 0.0 + } + return float64(s.MissCount()) / float64(requestCount) +} + +func (s *CacheStats) RecordHits() { + atomic.AddUint64(s.hitCount, 1) +} + +func (s *CacheStats) RecordMisses() { + atomic.AddUint64(s.missCount, 1) +} + +func (s *CacheStats) RecordEviction() { + atomic.AddUint64(s.evictionCount, 1) +} + +func (s *CacheStats) Snapshot() *CacheStats { + return createCacheStats(s.HitCount(), s.MissCount(), s.EvictionCount()) +} diff --git a/core/hotspot/cache/stats/cache_stats_test.go b/core/hotspot/cache/stats/cache_stats_test.go new file mode 100644 index 000000000..e47967865 --- /dev/null +++ b/core/hotspot/cache/stats/cache_stats_test.go @@ -0,0 +1,54 @@ +// Copyright 1999-2020 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package stats + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCacheStats(t *testing.T) { + t.Run("Test_CacheStats", func(t *testing.T) { + cs := NewCacheStats() + for i := 0; i < 100; i++ { + cs.RecordMisses() + } + assert.True(t, cs.MissCount() == 100) + assert.True(t, cs.MissRate() == 1.0) + assert.True(t, cs.HitRate() == 0.0) + + for i := 0; i < 100; i++ { + cs.RecordHits() + } + + assert.True(t, cs.MissCount() == 100) + assert.True(t, cs.HitCount() == 100) + assert.True(t, cs.MissRate() == 0.5) + assert.True(t, cs.HitRate() == 0.5) + + for i := 0; i < 50; i++ { + cs.RecordEviction() + } + assert.True(t, cs.EvictionCount() == 50) + + csNap := cs.Snapshot() + for i := 0; i < 50; i++ { + cs.RecordEviction() + } + assert.True(t, cs.EvictionCount() == 100) + assert.True(t, csNap.EvictionCount() == 50) + }) +} diff --git a/core/hotspot/cache/wtinylfu/concurrent_tinylfu.go b/core/hotspot/cache/wtinylfu/concurrent_tinylfu.go new file mode 100644 index 000000000..65f7d3caf --- /dev/null +++ b/core/hotspot/cache/wtinylfu/concurrent_tinylfu.go @@ -0,0 +1,110 @@ +// Copyright 1999-2020 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package wtinylfu + +import ( + "sync" + + "github.com/alibaba/sentinel-golang/core/hotspot/cache" + "github.com/alibaba/sentinel-golang/core/hotspot/cache/stats" +) + +// TinyLfuCacheMap use tinyLfu strategy to cache the most frequently accessed hotspot parameter +type TinyLfuCacheMap struct { + // Not thread safe + tinyLfu *TinyLfu + sync.RWMutex +} + +func (c *TinyLfuCacheMap) Add(key interface{}, value *int64) { + c.Lock() + defer c.Unlock() + + c.tinyLfu.Add(key, value) + return +} + +func (c *TinyLfuCacheMap) AddIfAbsent(key interface{}, value *int64) (priorValue *int64) { + c.Lock() + defer c.Unlock() + val := c.tinyLfu.AddIfAbsent(key, value) + if val == nil { + return nil + } + priorValue = val.(*int64) + return +} + +func (c *TinyLfuCacheMap) Get(key interface{}) (value *int64, isFound bool) { + c.Lock() + defer c.Unlock() + + val, found := c.tinyLfu.Get(key) + if found { + return val.(*int64), true + } + return nil, false +} + +func (c *TinyLfuCacheMap) Remove(key interface{}) (isFound bool) { + c.Lock() + defer c.Unlock() + + return c.tinyLfu.Remove(key) +} + +func (c *TinyLfuCacheMap) Contains(key interface{}) (ok bool) { + c.RLock() + defer c.RUnlock() + + return c.tinyLfu.Contains(key) +} + +func (c *TinyLfuCacheMap) Keys() []interface{} { + c.RLock() + defer c.RUnlock() + + return c.tinyLfu.Keys() +} + +func (c *TinyLfuCacheMap) Len() int { + c.RLock() + defer c.RUnlock() + + return c.tinyLfu.Len() +} + +func (c *TinyLfuCacheMap) Purge() { + c.Lock() + defer c.Unlock() + + c.tinyLfu.Purge() +} + +func (c *TinyLfuCacheMap) Stats() (*stats.CacheStats, error) { + c.RUnlock() + defer c.RUnlock() + return c.tinyLfu.Stats() +} + +func NewTinyLfuCacheMap(size int, isRecordingStats bool) cache.ConcurrentCounterCache { + tinyLfu, err := NewTinyLfu(size, isRecordingStats) + if err != nil { + return nil + } + return &TinyLfuCacheMap{ + tinyLfu: tinyLfu, + } +} diff --git a/core/hotspot/cache/wtinylfu/concurrent_tinylfu_benchmark_test.go b/core/hotspot/cache/wtinylfu/concurrent_tinylfu_benchmark_test.go new file mode 100644 index 000000000..84042ba2e --- /dev/null +++ b/core/hotspot/cache/wtinylfu/concurrent_tinylfu_benchmark_test.go @@ -0,0 +1,77 @@ +// Copyright 1999-2020 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package wtinylfu + +import ( + "strconv" + "testing" +) + +const Size = 50000 + +func Benchmark_TINYLFU_AddIfAbsent(b *testing.B) { + c := NewTinyLfuCacheMap(Size, false) + for a := 1; a <= Size; a++ { + val := new(int64) + *val = int64(a) + c.Add(strconv.Itoa(a), val) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + for j := 1000; j <= 1001; j++ { + newVal := new(int64) + *newVal = int64(j) + prior := c.AddIfAbsent(strconv.Itoa(j), newVal) + if *prior != int64(j) { + b.Fatal("error!") + } + } + } +} + +func Benchmark_TINYLFU_Add(b *testing.B) { + c := NewTinyLfuCacheMap(Size, false) + for a := 1; a <= Size; a++ { + val := new(int64) + *val = int64(a) + c.Add(strconv.Itoa(a), val) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + for j := Size - 100; j <= Size-99; j++ { + newVal := new(int64) + *newVal = int64(j) + c.Add(strconv.Itoa(j), newVal) + } + } +} + +func Benchmark_TINYLFU_Get(b *testing.B) { + c := NewTinyLfuCacheMap(Size, false) + for a := 1; a <= Size; a++ { + val := new(int64) + *val = int64(a) + c.Add(strconv.Itoa(a), val) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + for j := Size - 100; j <= Size-99; j++ { + val, found := c.Get(strconv.Itoa(j)) + if *val != int64(j) || !found { + b.Fatal("error") + } + } + } +} diff --git a/core/hotspot/cache/wtinylfu/count_min_sketch.go b/core/hotspot/cache/wtinylfu/count_min_sketch.go new file mode 100644 index 000000000..a41db10f1 --- /dev/null +++ b/core/hotspot/cache/wtinylfu/count_min_sketch.go @@ -0,0 +1,101 @@ +// Copyright 1999-2020 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package wtinylfu + +const sketchDepth = 4 + +const resetMask = 0x7777777777777777 + +// countMinSketch is an implementation of count-min sketch with 4-bit counters. +type countMinSketch struct { + counters []uint64 + mask uint32 +} + +// init initialize count-min sketch with the given width. +func newCountMinSketch(width int) *countMinSketch { + size := nextPowerOfTwo(uint32(width)) >> 2 + if size < 1 { + size = 1 + } + countMinSketch := countMinSketch{ + make([]uint64, size), + size - 1, + } + return &countMinSketch +} + +// add increase counters with given hash +func (c *countMinSketch) add(h uint64) { + hash1, hash2 := uint32(h), uint32(h>>32) + + for i := uint32(0); i < sketchDepth; i++ { + combinedHash := hash1 + (i * hash2) + idx, off := c.position(combinedHash) + c.inc(idx, (16*i)+off) + } +} + +// estimate returns minimum value of counters associated with the given hash. +func (c *countMinSketch) estimate(h uint64) uint8 { + hash1, hash2 := uint32(h), uint32(h>>32) + + var min uint8 = 0xFF + for i := uint32(0); i < sketchDepth; i++ { + combinedHash := hash1 + (i * hash2) + idx, off := c.position(combinedHash) + count := c.val(idx, (16*i)+off) + if count < min { + min = count + } + } + return min +} + +func (c *countMinSketch) reset() { + for i, v := range c.counters { + if v != 0 { + //divides all by two. + c.counters[i] = (v >> 1) & resetMask + } + } +} + +func (c *countMinSketch) position(h uint32) (idx uint32, off uint32) { + idx = (h >> 2) & c.mask + off = (h & 3) << 2 + return +} + +// inc increases value at index idx. +func (c *countMinSketch) inc(idx, off uint32) { + v := c.counters[idx] + count := uint8(v>>off) & 0x0F + if count < 15 { + c.counters[idx] = v + (1 << off) + } +} + +// val returns value at index idx. +func (c *countMinSketch) val(idx, off uint32) uint8 { + v := c.counters[idx] + return uint8(v>>off) & 0x0F +} + +func (c *countMinSketch) clear() { + for i := range c.counters { + c.counters[i] = 0 + } +} diff --git a/core/hotspot/cache/wtinylfu/count_min_sketch_test.go b/core/hotspot/cache/wtinylfu/count_min_sketch_test.go new file mode 100644 index 000000000..b50d008f2 --- /dev/null +++ b/core/hotspot/cache/wtinylfu/count_min_sketch_test.go @@ -0,0 +1,44 @@ +// Copyright 1999-2020 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package wtinylfu + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCountMinSketch(t *testing.T) { + t.Run("Test_CountMinSketch", func(t *testing.T) { + max := 15 + cm4 := newCountMinSketch(max) + for i := 0; i < max; i++ { + for j := i; j > 0; j-- { + cm4.add(uint64(i)) + } + assert.True(t, uint64(i) == uint64(cm4.estimate(uint64(i)))) + } + + cm4.reset() + for i := 0; i < max; i++ { + assert.True(t, uint64(i)/2 == uint64(cm4.estimate(uint64(i)))) + } + + cm4.clear() + for i := 0; i < max; i++ { + assert.True(t, 0 == uint64(cm4.estimate(uint64(i)))) + } + }) +} diff --git a/core/hotspot/cache/wtinylfu/doorkeeper.go b/core/hotspot/cache/wtinylfu/doorkeeper.go new file mode 100644 index 000000000..af907bede --- /dev/null +++ b/core/hotspot/cache/wtinylfu/doorkeeper.go @@ -0,0 +1,111 @@ +// Copyright 1999-2020 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package wtinylfu + +import "math" + +// doorkeeper is Bloom Filter implementation. +type doorkeeper struct { + // distinct hash functions needed + numHashes uint32 + // size of bit vector in bits + numBits uint32 + // doorkeeper bit vector + bits []uint64 +} + +// init initializes doorkeeper with the given expected insertions ins and +// false positive probability falsePositiveRate. +func newDoorkeeper(ins int, falsePositiveRate float64) *doorkeeper { + numBits := nextPowerOfTwo(uint32(float64(ins) * -math.Log(falsePositiveRate) / (math.Log(2.0) * math.Log(2.0)))) + if numBits < 1024 { + numBits = 1024 + } + d := doorkeeper{} + d.numBits = numBits + + if ins == 0 { + d.numHashes = 2 + } else { + d.numHashes = uint32(math.Log(2.0) * float64(numBits) / float64(ins)) + if d.numHashes < 2 { + d.numHashes = 2 + } + } + + d.bits = make([]uint64, int(numBits+63)/64) + return &d +} + +// put inserts a hash value into the bloom filter. +// returns true if the value may already in the doorkeeper. +func (d *doorkeeper) put(h uint64) bool { + //only two hash functions are necessary to effectively + //implement a Bloom filter without any loss in the asymptotic false positive probability + //split up 64-bit hashcode into protectedLs 32-bit hashcode + hash1, hash2 := uint32(h), uint32(h>>32) + var o uint = 1 + for i := uint32(0); i < d.numHashes; i++ { + combinedHash := hash1 + (i * hash2) + o &= d.getSet(combinedHash & (d.numBits - 1)) + } + return o == 1 +} + +//contains returns true if the given hash is may be in the filter. +func (d *doorkeeper) contains(h uint64) bool { + h1, h2 := uint32(h), uint32(h>>32) + var o uint = 1 + for i := uint32(0); i < d.numHashes; i++ { + o &= d.get((h1 + (i * h2)) & (d.numBits - 1)) + } + return o == 1 +} + +// set bit at index i and returns previous value. +func (d *doorkeeper) getSet(i uint32) uint { + idx, shift := i/64, i%64 + v := d.bits[idx] + m := uint64(1) << shift + d.bits[idx] |= m + return uint((v & m) >> shift) +} + +// get returns bit set at index i. +func (d *doorkeeper) get(i uint32) uint { + idx, shift := i/64, i%64 + val := d.bits[idx] + mask := uint64(1) << shift + return uint((val & mask) >> shift) +} + +// reset clears the doorkeeper. +func (d *doorkeeper) reset() { + for i := range d.bits { + d.bits[i] = 0 + } +} + +// return the integer >= i which is a power of two +func nextPowerOfTwo(i uint32) uint32 { + n := i - 1 + n |= n >> 1 + n |= n >> 2 + n |= n >> 4 + n |= n >> 8 + n |= n >> 16 + n++ + return n +} diff --git a/core/hotspot/cache/wtinylfu/doorkeeper_test.go b/core/hotspot/cache/wtinylfu/doorkeeper_test.go new file mode 100644 index 000000000..9ca724eaf --- /dev/null +++ b/core/hotspot/cache/wtinylfu/doorkeeper_test.go @@ -0,0 +1,36 @@ +// Copyright 1999-2020 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package wtinylfu + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDoorkeeper(t *testing.T) { + t.Run("Test_Doorkeeper", func(t *testing.T) { + max := 1500 + filter := newDoorkeeper(1500, 0.001) + for i := 0; i < max; i++ { + filter.put(uint64(i)) + assert.True(t, true == filter.contains(uint64(i))) + } + filter.reset() + for i := 0; i < max; i++ { + assert.True(t, false == filter.contains(uint64(i))) + } + }) +} diff --git a/core/hotspot/cache/wtinylfu/hash.go b/core/hotspot/cache/wtinylfu/hash.go new file mode 100644 index 000000000..16839b09b --- /dev/null +++ b/core/hotspot/cache/wtinylfu/hash.go @@ -0,0 +1,108 @@ +// Copyright 1999-2020 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package wtinylfu + +import ( + "encoding/binary" + "fmt" + "hash/fnv" + "math" + "reflect" +) + +var ( + fnv64 = fnv.New64() + byteSum = make([]byte, 0, 8) +) + +func sum(k interface{}) uint64 { + switch h := k.(type) { + case int: + return hashU64(uint64(h)) + case int8: + return hashU64(uint64(h)) + case int16: + return hashU64(uint64(h)) + case int32: + return hashU64(uint64(h)) + case int64: + return hashU64(uint64(h)) + case uint: + return hashU64(uint64(h)) + case uint8: + return hashU64(uint64(h)) + case uint16: + return hashU64(uint64(h)) + case uint32: + return hashU64(uint64(h)) + case uint64: + return hashU64(h) + case uintptr: + return hashU64(uint64(h)) + case float32: + return hashU64(uint64(math.Float32bits(h))) + case float64: + return hashU64(math.Float64bits(h)) + case bool: + if h { + return 1 + } + return 0 + case string: + return hashString(h) + } + if h, ok := hashPointer(k); ok { + return h + } + if h, ok := hashOtherWithSprintf(k); ok { + return h + } + return 0 +} + +func hashU64(data uint64) uint64 { + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, data) + return hashByteArray(b) +} + +func hashString(data string) uint64 { + return hashByteArray([]byte(data)) +} + +func hashOtherWithSprintf(data interface{}) (uint64, bool) { + v := fmt.Sprintf("%v", data) + return hashString(v), true +} + +func hashByteArray(bytes []byte) uint64 { + _, err := fnv64.Write(bytes) + if err != nil { + return 0 + } + hash := binary.LittleEndian.Uint64(fnv64.Sum(byteSum)) + fnv64.Reset() + return hash +} + +func hashPointer(k interface{}) (uint64, bool) { + v := reflect.ValueOf(k) + switch v.Kind() { + case reflect.Ptr, reflect.UnsafePointer, reflect.Func, reflect.Slice, reflect.Map, reflect.Chan: + return hashU64(uint64(v.Pointer())), true + default: + return 0, false + } +} diff --git a/core/hotspot/cache/wtinylfu/hash_benchmark_test.go b/core/hotspot/cache/wtinylfu/hash_benchmark_test.go new file mode 100644 index 000000000..b61e4f231 --- /dev/null +++ b/core/hotspot/cache/wtinylfu/hash_benchmark_test.go @@ -0,0 +1,57 @@ +// Copyright 1999-2020 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package wtinylfu + +import ( + "testing" +) + +func Benchmark_Hash_Num(b *testing.B) { + num := 100020000 + b.ResetTimer() + for i := 0; i < b.N; i++ { + sum(num) + } +} +func Benchmark_Hash_String(b *testing.B) { + str := "test" + b.ResetTimer() + for i := 0; i < b.N; i++ { + sum(str) + } +} + +func Benchmark_Hash_Pointer(b *testing.B) { + num := 100020000 + pointer := &num + b.ResetTimer() + for i := 0; i < b.N; i++ { + sum(pointer) + } +} +func Benchmark_Hash_OtherWithSprintf(b *testing.B) { + type test struct { + test1 uint32 + test2 string + } + s := test{ + 1, + "test2222", + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + sum(s) + } +} diff --git a/core/hotspot/cache/wtinylfu/hitrate_test.go b/core/hotspot/cache/wtinylfu/hitrate_test.go new file mode 100644 index 000000000..d142c31c8 --- /dev/null +++ b/core/hotspot/cache/wtinylfu/hitrate_test.go @@ -0,0 +1,69 @@ +// Copyright 1999-2020 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package wtinylfu + +import ( + "fmt" + "math/rand" + "testing" + + lru2 "github.com/alibaba/sentinel-golang/core/hotspot/cache/lru" +) + +func testBySize(cacheSize int, zipf *rand.Zipf) { + lfu, _ := NewTinyLfu(cacheSize, true) + lru, _ := lru2.NewLRU(cacheSize, nil, true) + for i := 0; i < 2000000; i++ { + key := zipf.Uint64() + _, ok := lfu.Get(key) + if !ok { + lfu.Add(key, key) + } + } + s, _ := lfu.Stats() + + fmt.Printf("tinyLfu cache size %d, hit %d, miss %d, evictionCount %d, hitRate %f \n", cacheSize, s.HitCount(), + s.MissCount(), s.EvictionCount(), s.HitRate()) + + for i := 0; i < 2000000; i++ { + key := zipf.Uint64() + _, ok := lru.Get(key) + if !ok { + lru.Add(key, key) + } + } + st, _ := lru.Stats() + fmt.Printf("lru cache size %d, hit %d, miss %d, evictionCount %d, hitRate %f \n", cacheSize, st.HitCount(), + st.MissCount(), st.EvictionCount(), st.HitRate()) +} + +func TestHitRate(t *testing.T) { + t.Run("Test_HitRate", func(t *testing.T) { + r := rand.New(rand.NewSource(1)) + zipf := rand.NewZipf( + r, + 1.01, + 1.0, + 1<<18-1, + ) + testBySize(100, zipf) + testBySize(500, zipf) + testBySize(1000, zipf) + testBySize(5000, zipf) + testBySize(10000, zipf) + testBySize(20000, zipf) + testBySize(50000, zipf) + }) +} diff --git a/core/hotspot/cache/wtinylfu/slru.go b/core/hotspot/cache/wtinylfu/slru.go new file mode 100644 index 000000000..0b98ccf94 --- /dev/null +++ b/core/hotspot/cache/wtinylfu/slru.go @@ -0,0 +1,189 @@ +// Copyright 1999-2020 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package wtinylfu + +import "container/list" + +type listType uint8 + +const ( + admissionWindow listType = iota + probationSegment + protectedSegment +) + +const protectedRatio = 0.8 + +type slruItem struct { + listType listType + key interface{} + value interface{} + keyHash uint64 +} + +// slru is a segmented LRU. +type slru struct { + data map[interface{}]*list.Element + probationCap, protectedCap int + probationLs, protectedLs *list.List +} + +func newSLRU(cap int, data map[interface{}]*list.Element) *slru { + protectedCap := int(float64(cap) * protectedRatio) + probationCap := cap - protectedCap + return &slru{ + data: data, + probationCap: probationCap, + probationLs: list.New(), + protectedCap: protectedCap, + protectedLs: list.New(), + } +} + +// access access a value from the cache +func (slru *slru) access(v *list.Element) { + item := v.Value.(*slruItem) + if item.listType == protectedSegment { + slru.protectedLs.MoveToFront(v) + return + } + if slru.protectedLs.Len() < slru.protectedCap { + slru.probationLs.Remove(v) + item.listType = protectedSegment + slru.data[item.key] = slru.protectedLs.PushFront(item) + return + } + back := slru.protectedLs.Back() + backItem := back.Value.(*slruItem) + + // swap the two item + *backItem, *item = *item, *backItem + backItem.listType = protectedSegment + item.listType = probationSegment + slru.data[item.key] = v + slru.data[backItem.key] = back + + // move the elements to the front of their lists + slru.probationLs.MoveToFront(v) + slru.protectedLs.MoveToFront(back) +} + +// add set a value in the cache +func (slru *slru) add(newItem slruItem) { + newItem.listType = probationSegment + if slru.probationLs.Len() < slru.probationCap || slru.Len() < slru.probationCap+slru.protectedCap { + slru.data[newItem.key] = slru.probationLs.PushFront(&newItem) + return + } + back := slru.probationLs.Back() + item := back.Value.(*slruItem) + delete(slru.data, item.key) + *item = newItem + slru.data[item.key] = back + slru.probationLs.MoveToFront(back) +} + +func (slru *slru) victim() *slruItem { + if slru.Len() < slru.probationCap+slru.protectedCap { + return nil + } + v := slru.probationLs.Back() + return v.Value.(*slruItem) +} + +// Len returns the total number of items in the cache +func (slru *slru) Len() int { + return slru.probationLs.Len() + slru.protectedLs.Len() +} + +// Remove removes an item from the cache, returning the item and a boolean indicating if it was found +func (slru *slru) Remove(key interface{}) (interface{}, bool) { + v, ok := slru.data[key] + if !ok { + return nil, false + } + item := v.Value.(*slruItem) + if item.listType == protectedSegment { + slru.protectedLs.Remove(v) + } else { + slru.probationLs.Remove(v) + } + delete(slru.data, key) + return item.value, true +} + +func (slru *slru) clear() { + slru.probationLs.Init() + slru.protectedLs.Init() +} + +// lru is an LRU cache. +type lru struct { + data map[interface{}]*list.Element + cap int + evictList *list.List +} + +func newLRU(cap int, data map[interface{}]*list.Element) *lru { + return &lru{ + data: data, + cap: cap, + evictList: list.New(), + } +} + +// access access a value from the cache +func (lru *lru) access(v *list.Element) { + lru.evictList.MoveToFront(v) +} + +// Set a value in the cache +func (lru *lru) add(newItem slruItem) (oldItem slruItem, evicted bool) { + if lru.evictList.Len() < lru.cap { + lru.data[newItem.key] = lru.evictList.PushFront(&newItem) + return slruItem{}, false + } + + // reuse the item + e := lru.evictList.Back() + item := e.Value.(*slruItem) + delete(lru.data, item.key) + oldItem = *item + *item = newItem + lru.data[item.key] = e + lru.evictList.MoveToFront(e) + return oldItem, true +} + +// Len returns the number of items in the cache. +func (lru *lru) Len() int { + return lru.evictList.Len() +} + +// Remove removes the provided key from the cache +func (lru *lru) Remove(key interface{}) (interface{}, bool) { + v, ok := lru.data[key] + if !ok { + return nil, false + } + item := v.Value.(*slruItem) + lru.evictList.Remove(v) + delete(lru.data, key) + return item.value, true +} + +func (lru *lru) clear() { + lru.evictList.Init() +} diff --git a/core/hotspot/cache/wtinylfu/tinylfu.go b/core/hotspot/cache/wtinylfu/tinylfu.go new file mode 100644 index 000000000..00f6ebef6 --- /dev/null +++ b/core/hotspot/cache/wtinylfu/tinylfu.go @@ -0,0 +1,223 @@ +// Copyright 1999-2020 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package wtinylfu + +import ( + "container/list" + "errors" + + "github.com/alibaba/sentinel-golang/core/hotspot/cache/stats" +) + +const ( + samplesFactor = 8 + doorkeeperFactor = 8 + countersFactor = 1 + falsePositiveProbability = 0.1 + lruRatio = 0.01 +) + +// TinyLfu is an implementation of the TinyLfu algorithm: https://arxiv.org/pdf/1512.00727.pdf +// Window Cache Victim .---------. Main Cache Victim +// .------------------->| TinyLFU |<-----------------. +// | `---------' | +// .-------------------. | .------------------. +// | Window Cache (1%) | | | Main Cache (99%) | +// | (LRU) | | | (SLRU) | +// `-------------------' | `------------------' +// ^ | ^ +// | `---------------' +// new item Winner +type TinyLfu struct { + countMinSketch *countMinSketch + doorkeeper *doorkeeper + additions int + samples int + lru *lru + slru *slru + items map[interface{}]*list.Element + stats *stats.CacheStats +} + +func NewTinyLfu(cap int, isRecordingStats bool) (*TinyLfu, error) { + if cap <= 0 { + return nil, errors.New("Must provide a positive size") + } + if cap < 100 { + cap = 100 + } + lruCap := int(float64(cap) * lruRatio) + slruSize := cap - lruCap + var statsCache *stats.CacheStats + if isRecordingStats { + statsCache = stats.NewCacheStats() + } + items := make(map[interface{}]*list.Element) + return &TinyLfu{ + countMinSketch: newCountMinSketch(countersFactor * cap), + additions: 0, + samples: samplesFactor * cap, + doorkeeper: newDoorkeeper(doorkeeperFactor*cap, falsePositiveProbability), + items: items, + lru: newLRU(lruCap, items), + slru: newSLRU(slruSize, items), + stats: statsCache, + }, nil +} + +// Get looks up a key's value from the cache. +func (t *TinyLfu) Get(key interface{}) (interface{}, bool) { + return t.get(key, false) +} + +func (t *TinyLfu) get(key interface{}, isInternal bool) (interface{}, bool) { + t.additions++ + if t.additions == t.samples { + t.countMinSketch.reset() + t.doorkeeper.reset() + t.additions = 0 + } + + val, ok := t.items[key] + if !ok { + keyHash := sum(key) + if t.doorkeeper.put(keyHash) { + t.countMinSketch.add(keyHash) + } + if !isInternal && t.stats != nil { + t.stats.RecordMisses() + } + return nil, false + } + item := val.Value.(*slruItem) + if t.doorkeeper.put(item.keyHash) { + t.countMinSketch.add(item.keyHash) + } + + v := item.value + if item.listType == admissionWindow { + t.lru.access(val) + } else { + t.slru.access(val) + } + if !isInternal && t.stats != nil { + t.stats.RecordHits() + } + return v, true +} + +// Contains checks if a key is in the cache without updating +func (t *TinyLfu) Contains(key interface{}) (ok bool) { + _, ok = t.items[key] + return ok +} + +func (t *TinyLfu) Add(key interface{}, val interface{}) { + t.AddIfAbsent(key, val) +} + +// AddIfAbsent adds item only if key is not existed. +func (t *TinyLfu) AddIfAbsent(key interface{}, val interface{}) (priorValue interface{}) { + + // Check for existing item + if v, ok := t.get(key, true); ok { + return v + } + + newItem := slruItem{admissionWindow, key, val, sum(key)} + candidate, evicted := t.lru.add(newItem) + if !evicted { + return nil + } + + // Estimate count of what will be evicted from slru + victim := t.slru.victim() + if victim == nil { + t.slru.add(candidate) + return nil + } + + victimCount := t.estimate(victim.keyHash) + candidateCount := t.estimate(candidate.keyHash) + if candidateCount > victimCount { + t.slru.add(candidate) + } + if t.stats != nil { + t.stats.RecordEviction() + } + return nil +} + +// estimate estimates frequency of the given hash value. +func (t *TinyLfu) estimate(h uint64) uint8 { + freq := t.countMinSketch.estimate(h) + if t.doorkeeper.contains(h) { + freq++ + } + return freq +} + +func (t *TinyLfu) Remove(key interface{}) (isFound bool) { + // Check for existing item + val, ok := t.items[key] + if !ok { + return false + } + + item := val.Value.(*slruItem) + if item.listType == admissionWindow { + t.lru.Remove(key) + return true + } else { + t.slru.Remove(key) + return true + } +} + +// Keys returns a slice of the keys in the cache, from oldest to newest. +func (t *TinyLfu) Keys() []interface{} { + i := 0 + keys := make([]interface{}, len(t.items)) + for k := range t.items { + keys[i] = k + i++ + } + return keys +} + +// Len returns the number of items in the cache. +func (t *TinyLfu) Len() int { + return len(t.items) +} + +// Purge is used to completely clear the cache. +func (t *TinyLfu) Purge() { + for k := range t.items { + delete(t.items, k) + } + t.slru.clear() + t.additions = 0 + t.lru.clear() + t.doorkeeper.reset() + t.countMinSketch.clear() +} + +// Stats copies cache stats. +func (t *TinyLfu) Stats() (*stats.CacheStats, error) { + if t.stats == nil { + return nil, errors.New("RecordingStats Must be enabled") + } + return t.stats.Snapshot(), nil +} diff --git a/core/hotspot/cache/wtinylfu/tinylfu_test.go b/core/hotspot/cache/wtinylfu/tinylfu_test.go new file mode 100644 index 000000000..38087525e --- /dev/null +++ b/core/hotspot/cache/wtinylfu/tinylfu_test.go @@ -0,0 +1,101 @@ +// Copyright 1999-2020 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package wtinylfu + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +type tinyLFUTest struct { + lfu *TinyLfu + t *testing.T +} + +func (t *tinyLFUTest) assertCap(n int) { + assert.True(t.t, t.lfu.lru.cap+t.lfu.slru.protectedCap+t.lfu.slru.probationCap == n) +} + +func (t *tinyLFUTest) assertLen(admission, protected, probation int) { + sz := t.lfu.Len() + tz := t.lfu.slru.protectedLs.Len() + bz := t.lfu.slru.probationLs.Len() + assert.True(t.t, sz == admission+protected+probation && tz == protected && bz == probation) +} + +func (t *tinyLFUTest) assertLRUValue(k int, id listType) { + v := t.lfu.items[k].Value.(*slruItem).value + assert.True(t.t, v != nil) + ak := k + av := v + listType := t.lfu.items[k].Value.(*slruItem).listType + assert.True(t.t, ak == av && listType == id) +} + +func TestTinyLFU(t *testing.T) { + t.Run("Test_TinyLFU", func(t *testing.T) { + + s := tinyLFUTest{t: t} + s.lfu, _ = NewTinyLfu(200, false) + s.assertCap(200) + s.lfu.slru.protectedCap = 2 + s.lfu.slru.probationCap = 1 + for i := 0; i < 5; i++ { + e := s.lfu.AddIfAbsent(i, i) + assert.True(t, e == nil) + } + // 4 3 | - | 2 1 0 + s.assertLen(2, 0, 3) + s.assertLRUValue(4, admissionWindow) + s.assertLRUValue(3, admissionWindow) + s.assertLRUValue(2, probationSegment) + s.assertLRUValue(1, probationSegment) + s.assertLRUValue(0, probationSegment) + + s.lfu.Get(1) + s.lfu.Get(2) + // 4 3 | 2 1 | 0 + s.assertLen(2, 2, 1) + s.assertLRUValue(2, protectedSegment) + s.assertLRUValue(1, protectedSegment) + s.assertLRUValue(0, probationSegment) + + s.lfu.AddIfAbsent(5, 5) + // 5 4 | 2 1 | 0 + s.assertLRUValue(5, admissionWindow) + s.assertLRUValue(4, admissionWindow) + s.assertLRUValue(2, protectedSegment) + s.assertLRUValue(1, protectedSegment) + s.assertLRUValue(0, probationSegment) + + s.lfu.Get(4) + s.lfu.Get(5) + s.lfu.AddIfAbsent(6, 6) + // 6 5 | 2 1 | 4 + s.assertLRUValue(6, admissionWindow) + s.assertLRUValue(5, admissionWindow) + s.assertLRUValue(2, protectedSegment) + s.assertLRUValue(1, protectedSegment) + s.assertLRUValue(4, probationSegment) + s.assertLen(2, 2, 1) + n := s.lfu.estimate(sum(1)) + assert.True(t, n == 2) + s.lfu.Get(2) + s.lfu.Get(2) + n = s.lfu.estimate(sum(2)) + assert.True(t, n == 4) + }) +} diff --git a/core/hotspot/rule_manager_test.go b/core/hotspot/rule_manager_test.go index 9571dde13..50ac4b68a 100644 --- a/core/hotspot/rule_manager_test.go +++ b/core/hotspot/rule_manager_test.go @@ -19,7 +19,7 @@ import ( "math" "testing" - "github.com/alibaba/sentinel-golang/core/hotspot/cache" + "github.com/alibaba/sentinel-golang/core/hotspot/cache/wtinylfu" "github.com/stretchr/testify/assert" ) @@ -74,9 +74,9 @@ func Test_tcGenFuncMap(t *testing.T) { size = ParamsMaxCapacity } metric := &ParamsMetric{ - RuleTimeCounter: cache.NewLRUCacheMap(size), - RuleTokenCounter: cache.NewLRUCacheMap(size), - ConcurrencyCounter: cache.NewLRUCacheMap(ConcurrencyMaxCount), + RuleTimeCounter: wtinylfu.NewTinyLfuCacheMap(size, false), + RuleTokenCounter: wtinylfu.NewTinyLfuCacheMap(size, false), + ConcurrencyCounter: wtinylfu.NewTinyLfuCacheMap(ConcurrencyMaxCount, false), } tc := generator(r1, metric) diff --git a/core/hotspot/traffic_shaping.go b/core/hotspot/traffic_shaping.go index 7ba5865c3..c29ba4cb7 100644 --- a/core/hotspot/traffic_shaping.go +++ b/core/hotspot/traffic_shaping.go @@ -22,7 +22,7 @@ import ( "time" "github.com/alibaba/sentinel-golang/core/base" - "github.com/alibaba/sentinel-golang/core/hotspot/cache" + "github.com/alibaba/sentinel-golang/core/hotspot/cache/wtinylfu" "github.com/alibaba/sentinel-golang/logging" "github.com/alibaba/sentinel-golang/util" "github.com/pkg/errors" @@ -84,8 +84,8 @@ func newBaseTrafficShapingController(r *Rule) *baseTrafficShapingController { size = ParamsMaxCapacity } metric := &ParamsMetric{ - RuleTimeCounter: cache.NewLRUCacheMap(size), - RuleTokenCounter: cache.NewLRUCacheMap(size), + RuleTimeCounter: wtinylfu.NewTinyLfuCacheMap(size, false), + RuleTokenCounter: wtinylfu.NewTinyLfuCacheMap(size, false), } return newBaseTrafficShapingControllerWithMetric(r, metric) case Concurrency: @@ -96,7 +96,7 @@ func newBaseTrafficShapingController(r *Rule) *baseTrafficShapingController { size = ConcurrencyMaxCount } metric := &ParamsMetric{ - ConcurrencyCounter: cache.NewLRUCacheMap(size), + ConcurrencyCounter: wtinylfu.NewTinyLfuCacheMap(size, false), } return newBaseTrafficShapingControllerWithMetric(r, metric) default: diff --git a/core/hotspot/traffic_shaping_test.go b/core/hotspot/traffic_shaping_test.go index 8ab99fadb..344c4a352 100644 --- a/core/hotspot/traffic_shaping_test.go +++ b/core/hotspot/traffic_shaping_test.go @@ -19,6 +19,8 @@ import ( "testing" "time" + "github.com/alibaba/sentinel-golang/core/hotspot/cache/stats" + "github.com/alibaba/sentinel-golang/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -76,6 +78,11 @@ func (c *counterCacheMock) Purge() { return } +func (c *counterCacheMock) Stats() (*stats.CacheStats, error) { + _ = c.Called() + return nil, nil +} + func Test_baseTrafficShapingController_performCheckingForConcurrencyMetric(t *testing.T) { t.Run("Test_baseTrafficShapingController_performCheckingForConcurrencyMetric", func(t *testing.T) { goCounter := &counterCacheMock{}