From c850eaff51a06df7a0c3eccefc4e05efb753edbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Werner?= Date: Wed, 19 Oct 2022 11:42:09 +0200 Subject: [PATCH] increase performance for the memory storage --- memory/internal/time.go | 32 ++++++++++++++++++++++++ memory/internal/time_test.go | 47 ++++++++++++++++++++++++++++++++++++ memory/memory.go | 32 +++++++++++++++--------- memory/memory_test.go | 30 ++++++++++++++++++++++- 4 files changed, 129 insertions(+), 12 deletions(-) create mode 100644 memory/internal/time.go create mode 100644 memory/internal/time_test.go diff --git a/memory/internal/time.go b/memory/internal/time.go new file mode 100644 index 00000000..9bb9f1ec --- /dev/null +++ b/memory/internal/time.go @@ -0,0 +1,32 @@ +package internal + +import ( + "sync" + "sync/atomic" + "time" +) + +var ( + timestampTimer sync.Once + // Timestamp please start the timer function before you use this value + // please load the value with atomic `atomic.LoadUint32(&utils.Timestamp)` + Timestamp uint32 +) + +// StartTimeStampUpdater starts a concurrent function which stores the timestamp to an atomic value per second, +// which is much better for performance than determining it at runtime each time +func StartTimeStampUpdater() { + timestampTimer.Do(func() { + // set initial value + atomic.StoreUint32(&Timestamp, uint32(time.Now().Unix())) + go func(sleep time.Duration) { + ticker := time.NewTicker(sleep) + defer ticker.Stop() + + for t := range ticker.C { + // update timestamp + atomic.StoreUint32(&Timestamp, uint32(t.Unix())) + } + }(1 * time.Second) // duration + }) +} diff --git a/memory/internal/time_test.go b/memory/internal/time_test.go new file mode 100644 index 00000000..729376b0 --- /dev/null +++ b/memory/internal/time_test.go @@ -0,0 +1,47 @@ +package internal + +import ( + "sync/atomic" + "testing" + "time" + + "github.com/gofiber/utils" +) + +func checkTimeStamp(t testing.TB, expectedCurrent, actualCurrent uint32) { + // test with some buffer in front and back of the expectedCurrent time -> because of the timing on the work machine + utils.AssertEqual(t, true, actualCurrent >= expectedCurrent-1 || actualCurrent <= expectedCurrent+1) +} + +func Test_TimeStampUpdater(t *testing.T) { + t.Parallel() + + StartTimeStampUpdater() + + now := uint32(time.Now().Unix()) + checkTimeStamp(t, now, atomic.LoadUint32(&Timestamp)) + // one second later + time.Sleep(1 * time.Second) + checkTimeStamp(t, now+1, atomic.LoadUint32(&Timestamp)) + // two seconds later + time.Sleep(1 * time.Second) + checkTimeStamp(t, now+2, atomic.LoadUint32(&Timestamp)) +} + +func Benchmark_CalculateTimestamp(b *testing.B) { + StartTimeStampUpdater() + + var res uint32 + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = atomic.LoadUint32(&Timestamp) + } + checkTimeStamp(b, uint32(time.Now().Unix()), res) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = uint32(time.Now().Unix()) + } + checkTimeStamp(b, uint32(time.Now().Unix()), res) + }) +} diff --git a/memory/memory.go b/memory/memory.go index 05d6b391..b1bd2f4e 100644 --- a/memory/memory.go +++ b/memory/memory.go @@ -2,7 +2,10 @@ package memory import ( "sync" + "sync/atomic" "time" + + "github.com/gofiber/storage/memory/internal" ) // Storage interface that is implemented by storage providers @@ -14,8 +17,9 @@ type Storage struct { } type entry struct { - data []byte - expiry int64 + data []byte + // max value is 4294967295 -> Sun Feb 07 2106 06:28:15 GMT+0000 + expiry uint32 } // New creates a new memory storage @@ -31,6 +35,7 @@ func New(config ...Config) *Storage { } // Start garbage collector + internal.StartTimeStampUpdater() go store.gc() return store @@ -44,14 +49,13 @@ func (s *Storage) Get(key string) ([]byte, error) { s.mux.RLock() v, ok := s.db[key] s.mux.RUnlock() - if !ok || v.expiry != 0 && v.expiry <= time.Now().Unix() { + if !ok || v.expiry != 0 && v.expiry <= atomic.LoadUint32(&internal.Timestamp) { return nil, nil } return v.data, nil } -// Set key with value // Set key with value func (s *Storage) Set(key string, val []byte, exp time.Duration) error { // Ain't Nobody Got Time For That @@ -59,9 +63,9 @@ func (s *Storage) Set(key string, val []byte, exp time.Duration) error { return nil } - var expire int64 + var expire uint32 if exp != 0 { - expire = time.Now().Add(exp).Unix() + expire = uint32(exp.Seconds()) + atomic.LoadUint32(&internal.Timestamp) } s.mux.Lock() @@ -99,19 +103,25 @@ func (s *Storage) Close() error { func (s *Storage) gc() { ticker := time.NewTicker(s.gcInterval) defer ticker.Stop() + var expired []string for { select { case <-s.done: return - case t := <-ticker.C: - now := t.Unix() - s.mux.Lock() + case <-ticker.C: + expired = expired[:0] + s.mux.RLock() for id, v := range s.db { - if v.expiry != 0 && v.expiry < now { - delete(s.db, id) + if v.expiry != 0 && v.expiry < atomic.LoadUint32(&internal.Timestamp) { + expired = append(expired, id) } } + s.mux.RUnlock() + s.mux.Lock() + for i := range expired { + delete(s.db, expired[i]) + } s.mux.Unlock() } } diff --git a/memory/memory_test.go b/memory/memory_test.go index 2e9a08ef..73918a55 100644 --- a/memory/memory_test.go +++ b/memory/memory_test.go @@ -122,4 +122,32 @@ func Test_Memory_Close(t *testing.T) { func Test_Memory_Conn(t *testing.T) { utils.AssertEqual(t, true, testStore.Conn() != nil) -} \ No newline at end of file +} + +// go test -v -run=^$ -bench=Benchmark_Memory -benchmem -count=4 +func Benchmark_Memory(b *testing.B) { + keyLength := 1000 + keys := make([]string, keyLength) + for i := 0; i < keyLength; i++ { + keys[i] = utils.UUID() + } + value := []byte("joe") + + ttl := 2 * time.Second + b.Run("fiber_memory", func(b *testing.B) { + d := New() + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + for _, key := range keys { + d.Set(key, value, ttl) + } + for _, key := range keys { + _, _ = d.Get(key) + } + for _, key := range keys { + d.Delete(key) + } + } + }) +}