This commit is contained in:
unknown
2026-02-04 20:27:13 +08:00
commit 3b042d1dad
9410 changed files with 1488147 additions and 0 deletions

View File

@@ -0,0 +1,204 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package counters
import (
"github.com/TeaOSLab/EdgeNode/internal/utils/fasttime"
"github.com/TeaOSLab/EdgeNode/internal/utils/goman"
memutils "github.com/TeaOSLab/EdgeNode/internal/utils/mem"
syncutils "github.com/TeaOSLab/EdgeNode/internal/utils/sync"
"github.com/cespare/xxhash/v2"
"sync"
"time"
)
const maxItemsPerGroup = 50_000
var SharedCounter = NewCounter[uint32]().WithGC()
type SupportedUIntType interface {
uint32 | uint64
}
type Counter[T SupportedUIntType] struct {
countMaps uint64
locker *syncutils.RWMutex
itemMaps []map[uint64]Item[T]
gcTicker *time.Ticker
gcIndex int
gcLocker sync.Mutex
}
// NewCounter create new counter
func NewCounter[T SupportedUIntType]() *Counter[T] {
var count = memutils.SystemMemoryGB() * 8
if count < 8 {
count = 8
}
var itemMaps = []map[uint64]Item[T]{}
for i := 0; i < count; i++ {
itemMaps = append(itemMaps, map[uint64]Item[T]{})
}
var counter = &Counter[T]{
countMaps: uint64(count),
locker: syncutils.NewRWMutex(count),
itemMaps: itemMaps,
}
return counter
}
// WithGC start the counter with gc automatically
func (this *Counter[T]) WithGC() *Counter[T] {
if this.gcTicker != nil {
return this
}
this.gcTicker = time.NewTicker(1 * time.Second)
goman.New(func() {
for range this.gcTicker.C {
this.GC()
}
})
return this
}
// Increase key
func (this *Counter[T]) Increase(key uint64, lifeSeconds int) T {
var index = int(key % this.countMaps)
this.locker.RLock(index)
var item = this.itemMaps[index][key] // item MUST NOT be pointer
this.locker.RUnlock(index)
if !item.IsOk() {
// no need to care about duplication
// always insert new item even when itemMap is full
item = NewItem[T](lifeSeconds)
var result = item.Increase()
this.locker.Lock(index)
this.itemMaps[index][key] = item
this.locker.Unlock(index)
return result
}
this.locker.Lock(index)
var result = item.Increase()
this.itemMaps[index][key] = item // overwrite
this.locker.Unlock(index)
return result
}
// IncreaseKey increase string key
func (this *Counter[T]) IncreaseKey(key string, lifeSeconds int) T {
return this.Increase(this.hash(key), lifeSeconds)
}
// Get value of key
func (this *Counter[T]) Get(key uint64) T {
var index = int(key % this.countMaps)
this.locker.RLock(index)
defer this.locker.RUnlock(index)
var item = this.itemMaps[index][key]
if item.IsOk() {
return item.Sum()
}
return 0
}
// GetKey get value of string key
func (this *Counter[T]) GetKey(key string) T {
return this.Get(this.hash(key))
}
// Reset key
func (this *Counter[T]) Reset(key uint64) {
var index = int(key % this.countMaps)
this.locker.RLock(index)
var item = this.itemMaps[index][key]
this.locker.RUnlock(index)
if item.IsOk() {
this.locker.Lock(index)
delete(this.itemMaps[index], key)
this.locker.Unlock(index)
}
}
// ResetKey string key
func (this *Counter[T]) ResetKey(key string) {
this.Reset(this.hash(key))
}
// TotalItems get items count
func (this *Counter[T]) TotalItems() int {
var total = 0
for i := 0; i < int(this.countMaps); i++ {
this.locker.RLock(i)
total += len(this.itemMaps[i])
this.locker.RUnlock(i)
}
return total
}
// GC garbage expired items
func (this *Counter[T]) GC() {
this.gcLocker.Lock()
var gcIndex = this.gcIndex
this.gcIndex++
if this.gcIndex >= int(this.countMaps) {
this.gcIndex = 0
}
this.gcLocker.Unlock()
var currentTime = fasttime.Now().Unix()
this.locker.RLock(gcIndex)
var itemMap = this.itemMaps[gcIndex]
var expiredKeys = []uint64{}
for key, item := range itemMap {
if item.IsExpired(currentTime) {
expiredKeys = append(expiredKeys, key)
}
}
var tooManyItems = len(itemMap) > maxItemsPerGroup // prevent too many items
this.locker.RUnlock(gcIndex)
if len(expiredKeys) > 0 {
this.locker.Lock(gcIndex)
for _, key := range expiredKeys {
delete(itemMap, key)
}
this.locker.Unlock(gcIndex)
}
if tooManyItems {
this.locker.Lock(gcIndex)
var count = len(itemMap) - maxItemsPerGroup
if count > 0 {
itemMap = this.itemMaps[gcIndex]
for key := range itemMap {
delete(itemMap, key)
count--
if count < 0 {
break
}
}
}
this.locker.Unlock(gcIndex)
}
}
func (this *Counter[T]) CountMaps() int {
return int(this.countMaps)
}
// calculate hash of the key
func (this *Counter[T]) hash(key string) uint64 {
return xxhash.Sum64String(key)
}

View File

@@ -0,0 +1,197 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package counters_test
import (
"github.com/TeaOSLab/EdgeNode/internal/utils/counters"
"github.com/TeaOSLab/EdgeNode/internal/utils/testutils"
"github.com/iwind/TeaGo/assert"
"github.com/iwind/TeaGo/rands"
"github.com/iwind/TeaGo/types"
timeutil "github.com/iwind/TeaGo/utils/time"
"runtime"
"runtime/debug"
"sync/atomic"
"testing"
"time"
)
func TestCounter_Increase(t *testing.T) {
var a = assert.NewAssertion(t)
var counter = counters.NewCounter[uint32]()
a.IsTrue(counter.Increase(1, 10) == 1)
a.IsTrue(counter.Increase(1, 10) == 2)
a.IsTrue(counter.Increase(2, 10) == 1)
counter.Reset(1)
a.IsTrue(counter.Get(1) == 0) // changed
a.IsTrue(counter.Get(2) == 1) // not changed
}
func TestCounter_IncreaseKey(t *testing.T) {
var a = assert.NewAssertion(t)
var counter = counters.NewCounter[uint32]()
a.IsTrue(counter.IncreaseKey("1", 10) == 1)
a.IsTrue(counter.IncreaseKey("1", 10) == 2)
a.IsTrue(counter.IncreaseKey("2", 10) == 1)
counter.ResetKey("1")
a.IsTrue(counter.GetKey("1") == 0) // changed
a.IsTrue(counter.GetKey("2") == 1) // not changed
}
func TestCounter_GC(t *testing.T) {
if !testutils.IsSingleTesting() {
return
}
var counter = counters.NewCounter[uint32]()
counter.Increase(1, 20)
time.Sleep(1 * time.Second)
counter.Increase(1, 20)
time.Sleep(1 * time.Second)
counter.Increase(1, 20)
counter.GC()
t.Log(counter.Get(1))
}
func TestCounter_GC2(t *testing.T) {
if !testutils.IsSingleTesting() {
return
}
var counter = counters.NewCounter[uint32]().WithGC()
for i := 0; i < 100_000; i++ {
counter.Increase(uint64(i), rands.Int(10, 300))
}
var ticker = time.NewTicker(1 * time.Second)
for range ticker.C {
t.Log(timeutil.Format("H:i:s"), counter.TotalItems())
if counter.TotalItems() == 0 {
break
}
}
}
func TestCounterMemory(t *testing.T) {
var stat = &runtime.MemStats{}
runtime.ReadMemStats(stat)
var counter = counters.NewCounter[uint32]()
for i := 0; i < 1_000_000; i++ {
counter.Increase(uint64(i), rands.Int(10, 300))
}
runtime.GC()
runtime.GC()
debug.FreeOSMemory()
var stat1 = &runtime.MemStats{}
runtime.ReadMemStats(stat1)
t.Log((stat1.HeapInuse-stat.HeapInuse)/(1<<20), "MB")
t.Log(counter.TotalItems())
var gcPause = func() {
var before = time.Now()
runtime.GC()
var costSeconds = time.Since(before).Seconds()
var stats = &debug.GCStats{}
debug.ReadGCStats(stats)
t.Log("GC pause:", stats.Pause[0].Seconds()*1000, "ms", "cost:", costSeconds*1000, "ms")
}
gcPause()
_ = counter.TotalItems()
}
func BenchmarkCounter_Increase(b *testing.B) {
runtime.GOMAXPROCS(4)
var counter = counters.NewCounter[uint32]()
b.ReportAllocs()
b.ResetTimer()
var i uint64
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
counter.Increase(atomic.AddUint64(&i, 1)%1_000_000, 20)
}
})
//b.Log(counter.TotalItems())
}
func BenchmarkCounter_IncreaseKey(b *testing.B) {
runtime.GOMAXPROCS(4)
var counter = counters.NewCounter[uint32]()
go func() {
var ticker = time.NewTicker(100 * time.Millisecond)
for range ticker.C {
counter.GC()
}
}()
b.ResetTimer()
b.ReportAllocs()
var i uint64
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
counter.IncreaseKey(types.String(atomic.AddUint64(&i, 1)%1_000_000), 20)
}
})
//b.Log(counter.TotalItems())
}
func BenchmarkCounter_IncreaseKey2(b *testing.B) {
runtime.GOMAXPROCS(4)
var counter = counters.NewCounter[uint32]()
go func() {
var ticker = time.NewTicker(1 * time.Millisecond)
for range ticker.C {
counter.GC()
}
}()
b.ResetTimer()
var i uint64
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
counter.IncreaseKey(types.String(atomic.AddUint64(&i, 1)%1e5), 20)
}
})
//b.Log(counter.TotalItems())
}
func BenchmarkCounter_GC(b *testing.B) {
runtime.GOMAXPROCS(4)
var counter = counters.NewCounter[uint32]()
for i := uint64(0); i < 1e5; i++ {
counter.IncreaseKey(types.String(i), 20)
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
counter.GC()
}
})
//b.Log(counter.TotalItems())
}

View File

@@ -0,0 +1,132 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package counters
import (
"github.com/TeaOSLab/EdgeNode/internal/utils/fasttime"
)
const spanMaxValue = 10_000_000
const maxSpans = 10
type Item[T SupportedUIntType] struct {
spans [maxSpans + 1]T
lastUpdateTime int64
lifeSeconds int64
spanSeconds int64
}
func NewItem[T SupportedUIntType](lifeSeconds int) Item[T] {
if lifeSeconds <= 0 {
lifeSeconds = 60
}
var spanSeconds = lifeSeconds / maxSpans
if spanSeconds < 1 {
spanSeconds = 1
} else if lifeSeconds > maxSpans && lifeSeconds%maxSpans != 0 {
spanSeconds++
}
return Item[T]{
lifeSeconds: int64(lifeSeconds),
spanSeconds: int64(spanSeconds),
lastUpdateTime: fasttime.Now().Unix(),
}
}
func (this *Item[T]) Increase() (result T) {
var currentTime = fasttime.Now().Unix()
var currentSpanIndex = this.calculateSpanIndex(currentTime)
// return quickly
if this.lastUpdateTime == currentTime {
if this.spans[currentSpanIndex] < spanMaxValue {
this.spans[currentSpanIndex]++
}
for _, count := range this.spans {
result += count
}
return
}
if this.lastUpdateTime > 0 {
if currentTime-this.lastUpdateTime > this.lifeSeconds {
for index := range this.spans {
this.spans[index] = 0
}
} else {
var lastSpanIndex = this.calculateSpanIndex(this.lastUpdateTime)
if lastSpanIndex != currentSpanIndex {
var countSpans = len(this.spans)
// reset values between LAST and CURRENT
for index := lastSpanIndex + 1; ; index++ {
var realIndex = index % countSpans
this.spans[realIndex] = 0
if realIndex == currentSpanIndex {
break
}
}
}
}
}
if this.spans[currentSpanIndex] < spanMaxValue {
this.spans[currentSpanIndex]++
}
this.lastUpdateTime = currentTime
for _, count := range this.spans {
result += count
}
return
}
func (this *Item[T]) Sum() (result T) {
if this.lastUpdateTime == 0 {
return 0
}
var currentTime = fasttime.Now().Unix()
var currentSpanIndex = this.calculateSpanIndex(currentTime)
if currentTime-this.lastUpdateTime > this.lifeSeconds {
return 0
} else {
var lastSpanIndex = this.calculateSpanIndex(this.lastUpdateTime)
var countSpans = len(this.spans)
for index := currentSpanIndex + 1; ; index++ {
var realIndex = index % countSpans
result += this.spans[realIndex]
if realIndex == lastSpanIndex {
break
}
}
}
return result
}
func (this *Item[T]) Reset() {
for index := range this.spans {
this.spans[index] = 0
}
}
func (this *Item[T]) IsExpired(currentTime int64) bool {
return this.lastUpdateTime < currentTime-this.lifeSeconds-this.spanSeconds
}
func (this *Item[T]) calculateSpanIndex(timestamp int64) int {
var index = int(timestamp % this.lifeSeconds / this.spanSeconds)
if index > maxSpans-1 {
return maxSpans - 1
}
return index
}
func (this *Item[T]) IsOk() bool {
return this.lifeSeconds > 0
}

View File

@@ -0,0 +1,82 @@
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
package counters_test
import (
"github.com/TeaOSLab/EdgeNode/internal/utils/counters"
"github.com/TeaOSLab/EdgeNode/internal/utils/testutils"
"github.com/iwind/TeaGo/assert"
"github.com/iwind/TeaGo/types"
timeutil "github.com/iwind/TeaGo/utils/time"
"runtime"
"testing"
"time"
)
func TestItem_Increase(t *testing.T) {
if !testutils.IsSingleTesting() {
return
}
var item = counters.NewItem[uint32](10)
t.Log(item.Increase(), item.Sum())
time.Sleep(1 * time.Second)
t.Log(item.Increase(), item.Sum())
time.Sleep(2 * time.Second)
t.Log(item.Increase(), item.Sum())
time.Sleep(5 * time.Second)
t.Log(item.Increase(), item.Sum())
time.Sleep(6 * time.Second)
t.Log(item.Increase(), item.Sum())
time.Sleep(5 * time.Second)
t.Log(item.Increase(), item.Sum())
time.Sleep(11 * time.Second)
t.Log(item.Increase(), item.Sum())
}
func TestItem_Increase2(t *testing.T) {
// run only under single testing
if !testutils.IsSingleTesting() {
return
}
var a = assert.NewAssertion(t)
var item = counters.NewItem[uint32](23)
for i := 0; i < 100; i++ {
t.Log("round "+types.String(i)+":", item.Increase(), item.Sum(), timeutil.Format("H:i:s"))
time.Sleep(2 * time.Second)
}
item.Reset()
a.IsTrue(item.Sum() == 0)
}
func TestItem_IsExpired(t *testing.T) {
if !testutils.IsSingleTesting() {
return
}
var item = counters.NewItem[uint32](10)
t.Log(item.IsExpired(time.Now().Unix()))
time.Sleep(10 * time.Second)
t.Log(item.IsExpired(time.Now().Unix()))
time.Sleep(2 * time.Second)
t.Log(item.IsExpired(time.Now().Unix()))
time.Sleep(2 * time.Second)
t.Log(item.IsExpired(time.Now().Unix()))
}
func BenchmarkItem_Increase(b *testing.B) {
runtime.GOMAXPROCS(1)
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
var item = counters.NewItem[uint32](60)
item.Increase()
item.Sum()
}
})
}