-
Notifications
You must be signed in to change notification settings - Fork 0
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
1 parent
0e2b011
commit 395c321
Showing
4 changed files
with
297 additions
and
0 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,3 @@ | ||
module github.com/Andrew-M-C/go.hotspotcache | ||
|
||
go 1.13 |
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,127 @@ | ||
package hotspotcache | ||
|
||
import ( | ||
"bytes" | ||
"container/list" | ||
"fmt" | ||
"runtime" | ||
"sync" | ||
) | ||
|
||
// Cache is the hotspot cache object | ||
type Cache struct { | ||
c *cache | ||
} | ||
|
||
type cache struct { | ||
maxSize int | ||
values sync.Map | ||
agingList *list.List // one in the front is the hottest one | ||
agingMap map[interface{}]*list.Element | ||
access chan interface{} | ||
stop chan bool | ||
} | ||
|
||
// New returns a initialized hotspot cache | ||
func New(maxSize int) *Cache { | ||
if maxSize <= 0 { | ||
maxSize = 10240 | ||
} | ||
|
||
c := newCache(maxSize) | ||
ret := Cache{c: c} | ||
|
||
go c.run() | ||
runtime.SetFinalizer(&ret, stopRunning) | ||
|
||
return &ret | ||
} | ||
|
||
// Load read a value in the cache. If value does not exist in cache, the return exist will be false, otherwise true. If value exists, its hotspot will also be set to the top. | ||
func (c *Cache) Load(key interface{}) (value interface{}, exist bool) { | ||
value, exist = c.c.values.Load(key) | ||
if exist { | ||
// log.Printf("Key %v not exists\n", key) | ||
c.c.access <- key | ||
} | ||
// log.Printf("Key %v exists\n", key) | ||
return | ||
} | ||
|
||
// Store saves a value in corresponding key. | ||
func (c *Cache) Store(key, value interface{}) { | ||
c.c.values.Store(key, value) | ||
c.c.access <- key | ||
return | ||
} | ||
|
||
// MaxSize returns the max size of this cache | ||
func (c *Cache) MaxSize() int { | ||
return c.c.maxSize | ||
} | ||
|
||
func newCache(maxSize int) *cache { | ||
c := cache{ | ||
maxSize: maxSize, | ||
agingList: list.New(), | ||
agingMap: make(map[interface{}]*list.Element), | ||
access: make(chan interface{}), | ||
stop: make(chan bool), | ||
} | ||
return &c | ||
} | ||
|
||
func (c *Cache) dumpStatus() string { | ||
buff := bytes.Buffer{} | ||
buff.WriteString(fmt.Sprintf("\nMax size: %d", c.c.maxSize)) | ||
buff.WriteString(fmt.Sprintf("\naging list length: %d", c.c.agingList.Len())) | ||
buff.WriteString(fmt.Sprintf("\nremain in channel: %d", len(c.c.access))) | ||
return buff.String() | ||
} | ||
|
||
func (c *cache) run() { | ||
for { | ||
select { | ||
case k := <-c.access: | ||
c.updateHotspot(k) | ||
case <-c.stop: | ||
return | ||
} | ||
} | ||
} | ||
|
||
func (c *cache) updateHotspot(k interface{}) { | ||
li := c.agingList | ||
e, exist := c.agingMap[k] | ||
|
||
// this key already in hot spotqueue | ||
if exist { | ||
// just move it to the front | ||
// log.Printf("upd hotspot %v\n", k) | ||
li.MoveToFront(e) | ||
return | ||
} | ||
|
||
// this key not exists in hotspot queue | ||
// log.Printf("new hotspot %v\n", k) | ||
e = li.PushFront(k) | ||
c.agingMap[k] = e | ||
|
||
// check if the queue is full | ||
if li.Len() <= c.maxSize { | ||
return | ||
} | ||
|
||
// remove the coldest one | ||
e = li.Back() | ||
k = e.Value | ||
c.values.Delete(k) | ||
li.Remove(e) | ||
delete(c.agingMap, k) | ||
// log.Printf("del hotspot %v\n", k) | ||
return | ||
} | ||
|
||
func stopRunning(c *Cache) { | ||
c.c.stop <- true | ||
} |
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,83 @@ | ||
package hotspotcache | ||
|
||
import ( | ||
"log" | ||
"runtime" | ||
"sync" | ||
"testing" | ||
) | ||
|
||
func init() { | ||
log.Printf("runtime.GOMAXPROCS(0) = %d\n", runtime.GOMAXPROCS(0)) | ||
} | ||
|
||
func BenchmarkMultiRoutine(b *testing.B) { | ||
c := New(b.N / 4) | ||
mask := 1 | ||
for { | ||
mask <<= 1 | ||
if mask >= b.N/8 { | ||
break | ||
} | ||
} | ||
|
||
MAX := runtime.GOMAXPROCS(0) * 2 | ||
var wg sync.WaitGroup | ||
wg.Add(MAX) | ||
|
||
for i := 0; i < MAX; i++ { | ||
go func() { | ||
defer wg.Done() | ||
for i := 0; i < b.N; i++ { | ||
n := i & mask | ||
_, exist := c.Load(n) | ||
if false == exist { | ||
c.Store(n, i) | ||
} | ||
} | ||
}() | ||
} | ||
|
||
wg.Wait() | ||
b.Logf("b.N = %d", b.N) | ||
return | ||
} | ||
|
||
func Benchmark75PercentWrite(b *testing.B) { | ||
c := New(b.N / 4) | ||
|
||
for i := 0; i < b.N; i++ { | ||
n := i | ||
_, exist := c.Load(n) | ||
if false == exist { | ||
c.Store(n, i) | ||
} | ||
} | ||
|
||
b.Logf("b.N = %d", b.N) | ||
return | ||
} | ||
|
||
func BenchmarkNoHitNoWrite(b *testing.B) { | ||
c := New(b.N / 4) | ||
|
||
for i := 0; i < b.N; i++ { | ||
n := i | ||
_, _ = c.Load(n) | ||
} | ||
|
||
b.Logf("b.N = %d", b.N) | ||
return | ||
} | ||
|
||
func BenchmarkAllHit(b *testing.B) { | ||
c := New(b.N / 4) | ||
c.Store(1, 1) | ||
|
||
for i := 0; i < b.N; i++ { | ||
_, _ = c.Load(1) | ||
} | ||
|
||
b.Logf("b.N = %d", b.N) | ||
return | ||
} |
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,84 @@ | ||
package hotspotcache | ||
|
||
import ( | ||
"math/rand" | ||
"runtime" | ||
"sync" | ||
"testing" | ||
"time" | ||
) | ||
|
||
func init() { | ||
rand.Seed(time.Now().UnixNano()) | ||
} | ||
|
||
func TestBasic(t *testing.T) { | ||
const N = 20000 | ||
const SIZE = 0 // causes default as 10240 | ||
c := New(SIZE) | ||
|
||
for i := 0; i < SIZE; i++ { | ||
c.Store(i, i) | ||
} | ||
|
||
time.Sleep(time.Second) | ||
|
||
for i := 0; i < SIZE; i++ { | ||
res, exist := c.Load(i) | ||
if false == exist { | ||
t.Errorf("Key %v not exist", i) | ||
return | ||
} | ||
if res.(int) != i { | ||
t.Errorf("Key %v not equal to %v", res, i) | ||
} | ||
} | ||
|
||
nonexistCount := 0 | ||
|
||
for i := 0; i < N; i++ { | ||
n := int(rand.Int31n(N)) | ||
_, exist := c.Load(n) | ||
if false == exist { | ||
nonexistCount++ | ||
c.Store(n, n) | ||
} | ||
} | ||
|
||
t.Logf("max size: %d", c.MaxSize()) | ||
t.Logf("nonexist percentage: %.02f%%", float64(nonexistCount)/float64(N)*100) | ||
|
||
return | ||
} | ||
|
||
func TestConcurrency(t *testing.T) { | ||
const N = 1000000 | ||
const SIZE = 50000 | ||
c := New(SIZE) | ||
|
||
MAX := runtime.GOMAXPROCS(0) * 2 | ||
var wg sync.WaitGroup | ||
wg.Add(MAX) | ||
|
||
for i := 0; i < MAX; i++ { | ||
go func() { | ||
defer wg.Done() | ||
for i := 0; i < N; i++ { | ||
n := int(rand.Int31n(N)) | ||
_, exist := c.Load(n) | ||
if false == exist { | ||
c.Store(n, n) | ||
} | ||
} | ||
}() | ||
} | ||
|
||
time.Sleep(10 * time.Millisecond) | ||
t.Logf("inter status: %s", c.dumpStatus()) | ||
|
||
wg.Wait() | ||
t.Logf("status: %s", c.dumpStatus()) | ||
time.Sleep(time.Second) | ||
t.Logf("status: %s", c.dumpStatus()) | ||
return | ||
} |