forked from WhoJave/clash
-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
15 changed files
with
437 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,241 @@ | ||
package arc | ||
|
||
import ( | ||
"sync" | ||
"time" | ||
|
||
list "github.com/bahlo/generic-list-go" | ||
"github.com/samber/lo" | ||
) | ||
|
||
//modify from https://github.com/alexanderGugel/arc | ||
|
||
// Option is part of Functional Options Pattern | ||
type Option[K comparable, V any] func(*ARC[K, V]) | ||
|
||
func WithSize[K comparable, V any](maxSize int) Option[K, V] { | ||
return func(a *ARC[K, V]) { | ||
a.c = maxSize | ||
} | ||
} | ||
|
||
type ARC[K comparable, V any] struct { | ||
p int | ||
c int | ||
t1 *list.List[*entry[K, V]] | ||
b1 *list.List[*entry[K, V]] | ||
t2 *list.List[*entry[K, V]] | ||
b2 *list.List[*entry[K, V]] | ||
mutex sync.Mutex | ||
len int | ||
cache map[K]*entry[K, V] | ||
} | ||
|
||
// New returns a new Adaptive Replacement Cache (ARC). | ||
func New[K comparable, V any](options ...Option[K, V]) *ARC[K, V] { | ||
arc := &ARC[K, V]{} | ||
arc.Clear() | ||
|
||
for _, option := range options { | ||
option(arc) | ||
} | ||
return arc | ||
} | ||
|
||
func (a *ARC[K, V]) Clear() { | ||
a.mutex.Lock() | ||
defer a.mutex.Unlock() | ||
|
||
a.p = 0 | ||
a.t1 = list.New[*entry[K, V]]() | ||
a.b1 = list.New[*entry[K, V]]() | ||
a.t2 = list.New[*entry[K, V]]() | ||
a.b2 = list.New[*entry[K, V]]() | ||
a.len = 0 | ||
a.cache = make(map[K]*entry[K, V]) | ||
} | ||
|
||
// Set inserts a new key-value pair into the cache. | ||
// This optimizes future access to this entry (side effect). | ||
func (a *ARC[K, V]) Set(key K, value V) { | ||
a.mutex.Lock() | ||
defer a.mutex.Unlock() | ||
|
||
a.set(key, value) | ||
} | ||
|
||
func (a *ARC[K, V]) set(key K, value V) { | ||
a.setWithExpire(key, value, time.Unix(0, 0)) | ||
} | ||
|
||
// SetWithExpire stores any representation of a response for a given key and given expires. | ||
// The expires time will round to second. | ||
func (a *ARC[K, V]) SetWithExpire(key K, value V, expires time.Time) { | ||
a.mutex.Lock() | ||
defer a.mutex.Unlock() | ||
|
||
a.setWithExpire(key, value, expires) | ||
} | ||
|
||
func (a *ARC[K, V]) setWithExpire(key K, value V, expires time.Time) { | ||
ent, ok := a.cache[key] | ||
if !ok { | ||
a.len++ | ||
ent := &entry[K, V]{key: key, value: value, ghost: false, expires: expires.Unix()} | ||
a.req(ent) | ||
a.cache[key] = ent | ||
return | ||
} | ||
|
||
if ent.ghost { | ||
a.len++ | ||
} | ||
|
||
ent.value = value | ||
ent.ghost = false | ||
ent.expires = expires.Unix() | ||
a.req(ent) | ||
} | ||
|
||
// Get retrieves a previously via Set inserted entry. | ||
// This optimizes future access to this entry (side effect). | ||
func (a *ARC[K, V]) Get(key K) (value V, ok bool) { | ||
a.mutex.Lock() | ||
defer a.mutex.Unlock() | ||
|
||
ent, ok := a.get(key) | ||
if !ok { | ||
return lo.Empty[V](), false | ||
} | ||
return ent.value, true | ||
} | ||
|
||
func (a *ARC[K, V]) get(key K) (e *entry[K, V], ok bool) { | ||
ent, ok := a.cache[key] | ||
if !ok { | ||
return ent, false | ||
} | ||
a.req(ent) | ||
return ent, !ent.ghost | ||
} | ||
|
||
// GetWithExpire returns any representation of a cached response, | ||
// a time.Time Give expected expires, | ||
// and a bool set to true if the key was found. | ||
// This method will NOT update the expires. | ||
func (a *ARC[K, V]) GetWithExpire(key K) (V, time.Time, bool) { | ||
a.mutex.Lock() | ||
defer a.mutex.Unlock() | ||
|
||
ent, ok := a.get(key) | ||
if !ok { | ||
return lo.Empty[V](), time.Time{}, false | ||
} | ||
|
||
return ent.value, time.Unix(ent.expires, 0), true | ||
} | ||
|
||
// Len determines the number of currently cached entries. | ||
// This method is side-effect free in the sense that it does not attempt to optimize random cache access. | ||
func (a *ARC[K, V]) Len() int { | ||
a.mutex.Lock() | ||
defer a.mutex.Unlock() | ||
|
||
return a.len | ||
} | ||
|
||
func (a *ARC[K, V]) req(ent *entry[K, V]) { | ||
switch { | ||
case ent.ll == a.t1 || ent.ll == a.t2: | ||
// Case I | ||
ent.setMRU(a.t2) | ||
case ent.ll == a.b1: | ||
// Case II | ||
// Cache Miss in t1 and t2 | ||
|
||
// Adaptation | ||
var d int | ||
if a.b1.Len() >= a.b2.Len() { | ||
d = 1 | ||
} else { | ||
d = a.b2.Len() / a.b1.Len() | ||
} | ||
a.p = min(a.p+d, a.c) | ||
|
||
a.replace(ent) | ||
ent.setMRU(a.t2) | ||
case ent.ll == a.b2: | ||
// Case III | ||
// Cache Miss in t1 and t2 | ||
|
||
// Adaptation | ||
var d int | ||
if a.b2.Len() >= a.b1.Len() { | ||
d = 1 | ||
} else { | ||
d = a.b1.Len() / a.b2.Len() | ||
} | ||
a.p = max(a.p-d, 0) | ||
|
||
a.replace(ent) | ||
ent.setMRU(a.t2) | ||
case ent.ll == nil && a.t1.Len()+a.b1.Len() == a.c: | ||
// Case IV A | ||
if a.t1.Len() < a.c { | ||
a.delLRU(a.b1) | ||
a.replace(ent) | ||
} else { | ||
a.delLRU(a.t1) | ||
} | ||
ent.setMRU(a.t1) | ||
case ent.ll == nil && a.t1.Len()+a.b1.Len() < a.c: | ||
// Case IV B | ||
if a.t1.Len()+a.t2.Len()+a.b1.Len()+a.b2.Len() >= a.c { | ||
if a.t1.Len()+a.t2.Len()+a.b1.Len()+a.b2.Len() == 2*a.c { | ||
a.delLRU(a.b2) | ||
} | ||
a.replace(ent) | ||
} | ||
ent.setMRU(a.t1) | ||
case ent.ll == nil: | ||
// Case IV, not A nor B | ||
ent.setMRU(a.t1) | ||
} | ||
} | ||
|
||
func (a *ARC[K, V]) delLRU(list *list.List[*entry[K, V]]) { | ||
lru := list.Back() | ||
list.Remove(lru) | ||
a.len-- | ||
delete(a.cache, lru.Value.key) | ||
} | ||
|
||
func (a *ARC[K, V]) replace(ent *entry[K, V]) { | ||
if a.t1.Len() > 0 && ((a.t1.Len() > a.p) || (ent.ll == a.b2 && a.t1.Len() == a.p)) { | ||
lru := a.t1.Back().Value | ||
lru.value = lo.Empty[V]() | ||
lru.ghost = true | ||
a.len-- | ||
lru.setMRU(a.b1) | ||
} else { | ||
lru := a.t2.Back().Value | ||
lru.value = lo.Empty[V]() | ||
lru.ghost = true | ||
a.len-- | ||
lru.setMRU(a.b2) | ||
} | ||
} | ||
|
||
func min(a, b int) int { | ||
if a < b { | ||
return a | ||
} | ||
return b | ||
} | ||
|
||
func max(a int, b int) int { | ||
if a < b { | ||
return b | ||
} | ||
return a | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
package arc | ||
|
||
import ( | ||
"testing" | ||
) | ||
|
||
func TestInsertion(t *testing.T) { | ||
cache := New[string, string](WithSize[string, string](3)) | ||
if got, want := cache.Len(), 0; got != want { | ||
t.Errorf("empty cache.Len(): got %d want %d", cache.Len(), want) | ||
} | ||
|
||
const ( | ||
k1 = "Hello" | ||
k2 = "Hallo" | ||
k3 = "Ciao" | ||
k4 = "Salut" | ||
|
||
v1 = "World" | ||
v2 = "Worlds" | ||
v3 = "Welt" | ||
) | ||
|
||
// Insert the first value | ||
cache.Set(k1, v1) | ||
if got, want := cache.Len(), 1; got != want { | ||
t.Errorf("insertion of key #%d: cache.Len(): got %d want %d", want, cache.Len(), want) | ||
} | ||
if got, ok := cache.Get(k1); !ok || got != v1 { | ||
t.Errorf("cache.Get(%v): got (%v,%t) want (%v,true)", k1, got, ok, v1) | ||
} | ||
|
||
// Replace existing value for a given key | ||
cache.Set(k1, v2) | ||
if got, want := cache.Len(), 1; got != want { | ||
t.Errorf("re-insertion: cache.Len(): got %d want %d", cache.Len(), want) | ||
} | ||
if got, ok := cache.Get(k1); !ok || got != v2 { | ||
t.Errorf("re-insertion: cache.Get(%v): got (%v,%t) want (%v,true)", k1, got, ok, v2) | ||
} | ||
|
||
// Add a second different key | ||
cache.Set(k2, v3) | ||
if got, want := cache.Len(), 2; got != want { | ||
t.Errorf("insertion of key #%d: cache.Len(): got %d want %d", want, cache.Len(), want) | ||
} | ||
if got, ok := cache.Get(k1); !ok || got != v2 { | ||
t.Errorf("cache.Get(%v): got (%v,%t) want (%v,true)", k1, got, ok, v2) | ||
} | ||
if got, ok := cache.Get(k2); !ok || got != v3 { | ||
t.Errorf("cache.Get(%v): got (%v,%t) want (%v,true)", k2, got, ok, v3) | ||
} | ||
|
||
// Fill cache | ||
cache.Set(k3, v1) | ||
if got, want := cache.Len(), 3; got != want { | ||
t.Errorf("insertion of key #%d: cache.Len(): got %d want %d", want, cache.Len(), want) | ||
} | ||
|
||
// Exceed size, this should not exceed size: | ||
cache.Set(k4, v1) | ||
if got, want := cache.Len(), 3; got != want { | ||
t.Errorf("insertion of key out of size: cache.Len(): got %d want %d", cache.Len(), want) | ||
} | ||
} | ||
|
||
func TestEviction(t *testing.T) { | ||
size := 3 | ||
cache := New[string, string](WithSize[string, string](size)) | ||
if got, want := cache.Len(), 0; got != want { | ||
t.Errorf("empty cache.Len(): got %d want %d", cache.Len(), want) | ||
} | ||
|
||
tests := []struct { | ||
k, v string | ||
}{ | ||
{"k1", "v1"}, | ||
{"k2", "v2"}, | ||
{"k3", "v3"}, | ||
{"k4", "v4"}, | ||
} | ||
for i, tt := range tests[:size] { | ||
cache.Set(tt.k, tt.v) | ||
if got, want := cache.Len(), i+1; got != want { | ||
t.Errorf("insertion of key #%d: cache.Len(): got %d want %d", want, cache.Len(), want) | ||
} | ||
} | ||
|
||
// Exceed size and check we don't outgrow it: | ||
cache.Set(tests[size].k, tests[size].v) | ||
if got := cache.Len(); got != size { | ||
t.Errorf("insertion of overflow key #%d: cache.Len(): got %d want %d", 4, cache.Len(), size) | ||
} | ||
|
||
// Check that LRU got evicted: | ||
if got, ok := cache.Get(tests[0].k); ok || got != "" { | ||
t.Errorf("cache.Get(%v): got (%v,%t) want (<nil>,true)", tests[0].k, got, ok) | ||
} | ||
|
||
for _, tt := range tests[1:] { | ||
if got, ok := cache.Get(tt.k); !ok || got != tt.v { | ||
t.Errorf("cache.Get(%v): got (%v,%t) want (%v,true)", tt.k, got, ok, tt.v) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package arc | ||
|
||
import ( | ||
list "github.com/bahlo/generic-list-go" | ||
) | ||
|
||
type entry[K comparable, V any] struct { | ||
key K | ||
value V | ||
ll *list.List[*entry[K, V]] | ||
el *list.Element[*entry[K, V]] | ||
ghost bool | ||
expires int64 | ||
} | ||
|
||
func (e *entry[K, V]) setLRU(list *list.List[*entry[K, V]]) { | ||
e.detach() | ||
e.ll = list | ||
e.el = e.ll.PushBack(e) | ||
} | ||
|
||
func (e *entry[K, V]) setMRU(list *list.List[*entry[K, V]]) { | ||
e.detach() | ||
e.ll = list | ||
e.el = e.ll.PushFront(e) | ||
} | ||
|
||
func (e *entry[K, V]) detach() { | ||
if e.ll != nil { | ||
e.ll.Remove(e.el) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
package cache | ||
package lru | ||
|
||
// Modified by https://github.com/die-net/lrucache | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
package cache | ||
package lru | ||
|
||
import ( | ||
"testing" | ||
|
Oops, something went wrong.