├── .circleci └── config.yml ├── .gitignore ├── CONTRIBUTORS.md ├── LICENSE ├── README.md ├── avgratecounter.go ├── avgratecounter_test.go ├── counter.go ├── counter_test.go ├── doc.go ├── go.mod ├── go.sum ├── ratecounter.go └── ratecounter_test.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | test: 4 | docker: 5 | - image: circleci/golang:1.15 6 | steps: 7 | - checkout 8 | 9 | - run: 10 | name: Run unit tests 11 | command: | 12 | go test -v -race -coverprofile=coverage.txt -covermode=atomic ./... 13 | 14 | - run: 15 | name: Upload codecov report 16 | command: | 17 | bash <(curl -s https://codecov.io/bash) 18 | 19 | lint: 20 | docker: 21 | - image: circleci/golang:1.15 22 | steps: 23 | - checkout 24 | 25 | - run: 26 | name: Install golangci-lint 27 | command: | 28 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.32.2 29 | 30 | - run: 31 | name: Run golangci-lint 32 | command: | 33 | golangci-lint run -v ./... 34 | 35 | workflows: 36 | version: 2 37 | test-workflow: 38 | jobs: 39 | - test 40 | - lint 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | 24 | *.sw? 25 | coverage.txt 26 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | RateCounter Contributors (sorted alphabetically) 2 | ============================================ 3 | 4 | - **[cheshir](https://github.com/cheshir)** 5 | 6 | - Added averate rate counter 7 | 8 | - **[paulbellamy](https://github.com/paulbellamy)** 9 | 10 | - Original implementation and general housekeeping 11 | 12 | - **[sheerun](https://github.com/sheerun)** 13 | 14 | - Improved memory efficiency 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 by Paul Bellamy 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ratecounter 2 | 3 | [![CircleCI](https://circleci.com/gh/paulbellamy/ratecounter.svg?style=svg)](https://circleci.com/gh/paulbellamy/ratecounter) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/paulbellamy/ratecounter)](https://goreportcard.com/report/github.com/paulbellamy/ratecounter) 5 | [![GoDoc](https://godoc.org/github.com/paulbellamy/ratecounter?status.svg)](https://godoc.org/github.com/paulbellamy/ratecounter) 6 | [![codecov](https://codecov.io/gh/paulbellamy/ratecounter/branch/master/graph/badge.svg)](https://codecov.io/gh/paulbellamy/ratecounter) 7 | 8 | A Thread-Safe RateCounter implementation in Golang 9 | 10 | ## Usage 11 | 12 | ``` 13 | import "github.com/paulbellamy/ratecounter" 14 | ``` 15 | 16 | Package ratecounter provides a thread-safe rate-counter, for tracking 17 | counts in an interval 18 | 19 | Useful for implementing counters and stats of 'requests-per-second' (for 20 | example): 21 | 22 | ```go 23 | // We're recording marks-per-1second 24 | counter := ratecounter.NewRateCounter(1 * time.Second) 25 | // Record an event happening 26 | counter.Incr(1) 27 | // get the current requests-per-second 28 | counter.Rate() 29 | ``` 30 | 31 | To record an average over a longer period, you can: 32 | 33 | ```go 34 | // Record requests-per-minute 35 | counter := ratecounter.NewRateCounter(60 * time.Second) 36 | // Calculate the average requests-per-second for the last minute 37 | counter.Rate() / 60 38 | ``` 39 | 40 | Also you can track average value of some metric in an interval. 41 | 42 | Useful for implementing counters and stats of 'average-execution-time' (for 43 | example): 44 | 45 | ```go 46 | // We're recording average execution time of some heavy operation in the last minute. 47 | counter := ratecounter.NewAvgRateCounter(60 * time.Second) 48 | // Start timer. 49 | startTime := time.Now() 50 | // Execute heavy operation. 51 | heavyOperation() 52 | // Record elapsed time. 53 | counter.Incr(time.Since(startTime).Nanoseconds()) 54 | // Get the current average execution time. 55 | counter.Rate() 56 | ``` 57 | 58 | ## Documentation 59 | 60 | Check latest documentation on [go doc](https://godoc.org/github.com/paulbellamy/ratecounter). 61 | 62 | -------------------------------------------------------------------------------- /avgratecounter.go: -------------------------------------------------------------------------------- 1 | package ratecounter 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | ) 7 | 8 | // An AvgRateCounter is a thread-safe counter which returns 9 | // the ratio between the number of calls 'Incr' and the counter value in the last interval 10 | type AvgRateCounter struct { 11 | hits *RateCounter 12 | counter *RateCounter 13 | interval time.Duration 14 | } 15 | 16 | // NewAvgRateCounter constructs a new AvgRateCounter, for the interval provided 17 | func NewAvgRateCounter(intrvl time.Duration) *AvgRateCounter { 18 | return &AvgRateCounter{ 19 | hits: NewRateCounter(intrvl), 20 | counter: NewRateCounter(intrvl), 21 | interval: intrvl, 22 | } 23 | } 24 | 25 | // WithResolution determines the minimum resolution of this counter 26 | func (a *AvgRateCounter) WithResolution(resolution int) *AvgRateCounter { 27 | if resolution < 1 { 28 | panic("AvgRateCounter resolution cannot be less than 1") 29 | } 30 | 31 | a.hits = a.hits.WithResolution(resolution) 32 | a.counter = a.counter.WithResolution(resolution) 33 | 34 | return a 35 | } 36 | 37 | // Incr Adds an event into the AvgRateCounter 38 | func (a *AvgRateCounter) Incr(val int64) { 39 | a.hits.Incr(1) 40 | a.counter.Incr(val) 41 | } 42 | 43 | // Rate Returns the current ratio between the events count and its values during the last interval 44 | func (a *AvgRateCounter) Rate() float64 { 45 | hits, value := a.hits.Rate(), a.counter.Rate() 46 | 47 | if hits == 0 { 48 | return 0 // Avoid division by zero 49 | } 50 | 51 | return float64(value) / float64(hits) 52 | } 53 | 54 | // Hits returns the number of calling method Incr during specified interval 55 | func (a *AvgRateCounter) Hits() int64 { 56 | return a.hits.Rate() 57 | } 58 | 59 | // String returns counter's rate formatted to string 60 | func (a *AvgRateCounter) String() string { 61 | return strconv.FormatFloat(a.Rate(), 'e', 5, 64) 62 | } 63 | -------------------------------------------------------------------------------- /avgratecounter_test.go: -------------------------------------------------------------------------------- 1 | package ratecounter 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestAvgRateCounter(t *testing.T) { 11 | interval := 50 * time.Millisecond 12 | r := NewAvgRateCounter(interval) 13 | 14 | assert.Equal(t, float64(0), r.Rate()) 15 | assert.Equal(t, int64(0), r.Hits()) 16 | r.Incr(1) // counter = 1, hits = 1 17 | assert.Equal(t, float64(1.0), r.Rate()) 18 | assert.Equal(t, int64(1), r.Hits()) 19 | r.Incr(3) // counter = 4, hits = 2 20 | assert.Equal(t, float64(2.0), r.Rate()) 21 | assert.Equal(t, int64(2), r.Hits()) 22 | time.Sleep(2 * interval) 23 | assert.Equal(t, float64(0), r.Rate()) 24 | assert.Equal(t, int64(0), r.Hits()) 25 | } 26 | 27 | func TestAvgRateCounterAdvanced(t *testing.T) { 28 | interval := 50 * time.Millisecond 29 | almost := 45 * time.Millisecond 30 | gap := 1 * time.Millisecond 31 | r := NewAvgRateCounter(interval) 32 | 33 | assert.Equal(t, float64(0), r.Rate()) 34 | assert.Equal(t, int64(0), r.Hits()) 35 | r.Incr(1) // counter = 1, hits = 1 36 | assert.Equal(t, float64(1.0), r.Rate()) 37 | assert.Equal(t, int64(1), r.Hits()) 38 | time.Sleep(interval - almost) 39 | r.Incr(3) // counter = 4, hits = 2 40 | assert.Equal(t, float64(2.0), r.Rate()) 41 | assert.Equal(t, int64(2), r.Hits()) 42 | time.Sleep(almost + gap) 43 | assert.Equal(t, float64(3.0), r.Rate()) 44 | assert.Equal(t, int64(1), r.Hits()) // counter = 3, hits = 1 45 | time.Sleep(2 * interval) 46 | assert.Equal(t, float64(0), r.Rate()) 47 | assert.Equal(t, int64(0), r.Hits()) 48 | } 49 | 50 | func TestAvgRateCounterMinResolution(t *testing.T) { 51 | defer func() { 52 | if r := recover(); r == nil { 53 | t.Errorf("Resolution < 1 did not panic") 54 | } 55 | }() 56 | 57 | NewAvgRateCounter(500 * time.Millisecond).WithResolution(0) 58 | } 59 | 60 | func TestAvgRateCounterNoResolution(t *testing.T) { 61 | interval := 50 * time.Millisecond 62 | almost := 45 * time.Millisecond 63 | gap := 1 * time.Millisecond 64 | r := NewAvgRateCounter(interval).WithResolution(1) 65 | 66 | assert.Equal(t, float64(0), r.Rate()) 67 | assert.Equal(t, int64(0), r.Hits()) 68 | r.Incr(1) // counter = 1, hits = 1 69 | assert.Equal(t, float64(1.0), r.Rate()) 70 | assert.Equal(t, int64(1), r.Hits()) 71 | time.Sleep(interval - almost) 72 | r.Incr(3) // counter = 4, hits = 2 73 | assert.Equal(t, float64(2.0), r.Rate()) 74 | assert.Equal(t, int64(2), r.Hits()) 75 | time.Sleep(almost + gap) 76 | assert.Equal(t, float64(0), r.Rate()) 77 | assert.Equal(t, int64(0), r.Hits()) // counter = 0, hits = 0, r.Hits()) 78 | time.Sleep(2 * interval) 79 | assert.Equal(t, float64(0), r.Rate()) 80 | assert.Equal(t, int64(0), r.Hits()) 81 | } 82 | 83 | func TestAvgRateCounter_String(t *testing.T) { 84 | r := NewAvgRateCounter(1 * time.Second) 85 | if r.String() != "0.00000e+00" { 86 | t.Error("Expected ", r.String(), " to equal ", "0.00000e+00") 87 | } 88 | 89 | r.Incr(1) 90 | if r.String() != "1.00000e+00" { 91 | t.Error("Expected ", r.String(), " to equal ", "1.00000e+00") 92 | } 93 | } 94 | 95 | func TestAvgRateCounter_Incr_ReturnsImmediately(t *testing.T) { 96 | interval := 1 * time.Second 97 | r := NewAvgRateCounter(interval) 98 | 99 | start := time.Now() 100 | r.Incr(-1) 101 | duration := time.Since(start) 102 | 103 | if duration >= 1*time.Second { 104 | t.Error("incr took", duration, "to return") 105 | } 106 | } 107 | 108 | func BenchmarkAvgRateCounter(b *testing.B) { 109 | interval := 1 * time.Millisecond 110 | r := NewAvgRateCounter(interval) 111 | 112 | for i := 0; i < b.N; i++ { 113 | r.Incr(1) 114 | r.Rate() 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /counter.go: -------------------------------------------------------------------------------- 1 | package ratecounter 2 | 3 | import "sync/atomic" 4 | 5 | // A Counter is a thread-safe counter implementation 6 | type Counter int64 7 | 8 | // Incr method increments the counter by some value 9 | func (c *Counter) Incr(val int64) { 10 | atomic.AddInt64((*int64)(c), val) 11 | } 12 | 13 | // Reset method resets the counter's value to zero 14 | func (c *Counter) Reset() { 15 | atomic.StoreInt64((*int64)(c), 0) 16 | } 17 | 18 | // Value method returns the counter's current value 19 | func (c *Counter) Value() int64 { 20 | return atomic.LoadInt64((*int64)(c)) 21 | } 22 | -------------------------------------------------------------------------------- /counter_test.go: -------------------------------------------------------------------------------- 1 | package ratecounter 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestCounter(t *testing.T) { 11 | var c Counter 12 | 13 | assert.Equal(t, int64(0), c.Value()) 14 | c.Incr(1) 15 | assert.Equal(t, int64(1), c.Value()) 16 | c.Incr(9) 17 | assert.Equal(t, int64(10), c.Value()) 18 | c.Reset() 19 | assert.Equal(t, int64(0), c.Value()) 20 | 21 | // Concurrent usage 22 | wg := &sync.WaitGroup{} 23 | wg.Add(3) 24 | for i := 1; i <= 3; i++ { 25 | go func(val int64) { 26 | c.Incr(val) 27 | wg.Done() 28 | }(int64(i)) 29 | } 30 | wg.Wait() 31 | assert.Equal(t, int64(6), c.Value()) 32 | } 33 | 34 | func BenchmarkCounter(b *testing.B) { 35 | var c Counter 36 | 37 | for i := 0; i < b.N; i++ { 38 | c.Incr(1) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package ratecounter provides a thread-safe rate-counter, for tracking counts 3 | in an interval 4 | 5 | Useful for implementing counters and stats of 'requests-per-second' (for example). 6 | 7 | // We're recording events-per-1second 8 | counter := ratecounter.NewRateCounter(1 * time.Second) 9 | 10 | // Record an event happening 11 | counter.Incr(1) 12 | 13 | // get the current requests-per-second 14 | counter.Rate() 15 | 16 | To record an average over a longer period, you can: 17 | 18 | // Record requests-per-minute 19 | counter := ratecounter.NewRateCounter(60 * time.Second) 20 | 21 | // Calculate the average requests-per-second for the last minute 22 | counter.Rate() / 60 23 | */ 24 | package ratecounter 25 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/paulbellamy/ratecounter 2 | 3 | go 1.22 4 | 5 | require github.com/stretchr/testify v1.9.0 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.1 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 6 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 10 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | -------------------------------------------------------------------------------- /ratecounter.go: -------------------------------------------------------------------------------- 1 | package ratecounter 2 | 3 | import ( 4 | "strconv" 5 | "sync" 6 | "sync/atomic" 7 | "time" 8 | ) 9 | 10 | // A RateCounter is a thread-safe counter which returns the number of times 11 | // 'Incr' has been called in the last interval 12 | type RateCounter struct { 13 | counter Counter 14 | interval time.Duration 15 | resolution int 16 | partials []Counter 17 | current int32 18 | running int32 19 | onStop func(r *RateCounter) 20 | onStopLock sync.RWMutex 21 | } 22 | 23 | // NewRateCounter Constructs a new RateCounter, for the interval provided 24 | func NewRateCounter(intrvl time.Duration) *RateCounter { 25 | ratecounter := &RateCounter{ 26 | interval: intrvl, 27 | running: 0, 28 | } 29 | 30 | return ratecounter.WithResolution(20) 31 | } 32 | 33 | // NewRateCounterWithResolution Constructs a new RateCounter, for the provided interval and resolution 34 | func NewRateCounterWithResolution(intrvl time.Duration, resolution int) *RateCounter { 35 | ratecounter := &RateCounter{ 36 | interval: intrvl, 37 | running: 0, 38 | } 39 | 40 | return ratecounter.WithResolution(resolution) 41 | } 42 | 43 | // WithResolution determines the minimum resolution of this counter, default is 20 44 | func (r *RateCounter) WithResolution(resolution int) *RateCounter { 45 | if resolution < 1 { 46 | panic("RateCounter resolution cannot be less than 1") 47 | } 48 | 49 | r.resolution = resolution 50 | r.partials = make([]Counter, resolution) 51 | r.current = 0 52 | 53 | return r 54 | } 55 | 56 | // OnStop allow to specify a function that will be called each time the counter 57 | // reaches 0. Useful for removing it. 58 | func (r *RateCounter) OnStop(f func(*RateCounter)) { 59 | r.onStopLock.Lock() 60 | r.onStop = f 61 | r.onStopLock.Unlock() 62 | } 63 | 64 | func (r *RateCounter) run() { 65 | if ok := atomic.CompareAndSwapInt32(&r.running, 0, 1); !ok { 66 | return 67 | } 68 | 69 | go func() { 70 | ticker := time.NewTicker(time.Duration(float64(r.interval) / float64(r.resolution))) 71 | 72 | for range ticker.C { 73 | current := atomic.LoadInt32(&r.current) 74 | next := (int(current) + 1) % r.resolution 75 | r.counter.Incr(-1 * r.partials[next].Value()) 76 | r.partials[next].Reset() 77 | atomic.CompareAndSwapInt32(&r.current, current, int32(next)) 78 | if r.counter.Value() == 0 { 79 | atomic.StoreInt32(&r.running, 0) 80 | ticker.Stop() 81 | 82 | r.onStopLock.RLock() 83 | if r.onStop != nil { 84 | r.onStop(r) 85 | } 86 | r.onStopLock.RUnlock() 87 | 88 | return 89 | } 90 | } 91 | }() 92 | } 93 | 94 | // Incr Add an event into the RateCounter 95 | func (r *RateCounter) Incr(val int64) { 96 | r.counter.Incr(val) 97 | r.partials[atomic.LoadInt32(&r.current)].Incr(val) 98 | r.run() 99 | } 100 | 101 | // Rate Return the current number of events in the last interval 102 | func (r *RateCounter) Rate() int64 { 103 | return r.counter.Value() 104 | } 105 | 106 | // MaxRate counts the maximum instantaneous change in rate. 107 | // 108 | // This is useful to calculate number of events in last period without 109 | // "averaging" effect. i.e. currently if counter is set for 30 seconds 110 | // duration, and events fire 10 times per second, it'll take 30 seconds for 111 | // "Rate" to show 300 (or 10 per second). The "MaxRate" will show 10 112 | // immediately, and it'll stay this way for the next 30 seconds, even if rate 113 | // drops below it. 114 | func (r *RateCounter) MaxRate() int64 { 115 | max := int64(0) 116 | for i := 0; i < r.resolution; i++ { 117 | if value := r.partials[i].Value(); max < value { 118 | max = value 119 | } 120 | } 121 | return max 122 | } 123 | 124 | func (r *RateCounter) String() string { 125 | return strconv.FormatInt(r.counter.Value(), 10) 126 | } 127 | -------------------------------------------------------------------------------- /ratecounter_test.go: -------------------------------------------------------------------------------- 1 | package ratecounter 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestRateCounter(t *testing.T) { 13 | interval := 50 * time.Millisecond 14 | r := NewRateCounter(interval) 15 | 16 | assert.Equal(t, int64(0), r.Rate()) 17 | r.Incr(1) 18 | assert.Equal(t, int64(1), r.Rate()) 19 | r.Incr(2) 20 | assert.Equal(t, int64(3), r.Rate()) 21 | time.Sleep(2 * interval) 22 | assert.Equal(t, int64(0), r.Rate()) 23 | } 24 | 25 | func TestRateCounterExpireAndRestart(t *testing.T) { 26 | interval := 50 * time.Millisecond 27 | 28 | r := NewRateCounter(interval) 29 | 30 | assert.Equal(t, int64(0), r.Rate()) 31 | r.Incr(1) 32 | assert.Equal(t, int64(1), r.Rate()) 33 | 34 | // Let it expire down to zero, then restart 35 | time.Sleep(2 * interval) 36 | assert.Equal(t, int64(0), r.Rate()) 37 | time.Sleep(2 * interval) 38 | r.Incr(2) 39 | assert.Equal(t, int64(2), r.Rate()) 40 | 41 | // Let it expire down to zero 42 | time.Sleep(2 * interval) 43 | assert.Equal(t, int64(0), r.Rate()) 44 | 45 | // Restart it 46 | r.Incr(2) 47 | assert.Equal(t, int64(2), r.Rate()) 48 | } 49 | 50 | func TestRateCounterPartial(t *testing.T) { 51 | interval := 50 * time.Millisecond 52 | almost := 40 * time.Millisecond 53 | 54 | r := NewRateCounter(interval) 55 | 56 | assert.Equal(t, int64(0), r.Rate()) 57 | r.Incr(1) 58 | assert.Equal(t, int64(1), r.Rate()) 59 | time.Sleep(almost) 60 | r.Incr(2) 61 | assert.Equal(t, int64(3), r.Rate()) 62 | time.Sleep(almost) 63 | assert.Equal(t, int64(2), r.Rate()) 64 | time.Sleep(2 * interval) 65 | assert.Equal(t, int64(0), r.Rate()) 66 | } 67 | 68 | func TestRateCounterHighResolution(t *testing.T) { 69 | interval := 50 * time.Millisecond 70 | tenth := 5 * time.Millisecond 71 | 72 | r := NewRateCounter(interval).WithResolution(100) 73 | 74 | assert.Equal(t, int64(0), r.Rate()) 75 | r.Incr(1) 76 | assert.Equal(t, int64(1), r.Rate()) 77 | time.Sleep(2 * tenth) 78 | r.Incr(1) 79 | assert.Equal(t, int64(2), r.Rate()) 80 | time.Sleep(2 * tenth) 81 | r.Incr(1) 82 | assert.Equal(t, int64(3), r.Rate()) 83 | time.Sleep(interval - 5*tenth) 84 | assert.Equal(t, int64(3), r.Rate()) 85 | time.Sleep(2 * tenth) 86 | assert.Equal(t, int64(2), r.Rate()) 87 | time.Sleep(2 * tenth) 88 | assert.Equal(t, int64(1), r.Rate()) 89 | time.Sleep(2 * tenth) 90 | assert.Equal(t, int64(0), r.Rate()) 91 | } 92 | 93 | func TestRateCounterLowResolution(t *testing.T) { 94 | interval := 50 * time.Millisecond 95 | tenth := 5 * time.Millisecond 96 | 97 | r := NewRateCounter(interval).WithResolution(4) 98 | 99 | assert.Equal(t, int64(0), r.Rate()) 100 | r.Incr(1) 101 | assert.Equal(t, int64(1), r.Rate()) 102 | time.Sleep(2 * tenth) 103 | r.Incr(1) 104 | assert.Equal(t, int64(2), r.Rate()) 105 | time.Sleep(2 * tenth) 106 | r.Incr(1) 107 | assert.Equal(t, int64(3), r.Rate()) 108 | time.Sleep(interval - 5*tenth) 109 | assert.Equal(t, int64(3), r.Rate()) 110 | time.Sleep(2 * tenth) 111 | assert.Equal(t, int64(1), r.Rate()) 112 | time.Sleep(2 * tenth) 113 | assert.Equal(t, int64(0), r.Rate()) 114 | time.Sleep(2 * tenth) 115 | assert.Equal(t, int64(0), r.Rate()) 116 | } 117 | 118 | func TestNewRateCounterWithResolution(t *testing.T) { 119 | interval := 50 * time.Millisecond 120 | tenth := 5 * time.Millisecond 121 | 122 | r := NewRateCounterWithResolution(interval, 4) 123 | 124 | // Same as previous test with low resolution 125 | assert.Equal(t, int64(0), r.Rate()) 126 | r.Incr(1) 127 | assert.Equal(t, int64(1), r.Rate()) 128 | time.Sleep(2 * tenth) 129 | r.Incr(1) 130 | assert.Equal(t, int64(2), r.Rate()) 131 | time.Sleep(2 * tenth) 132 | r.Incr(1) 133 | assert.Equal(t, int64(3), r.Rate()) 134 | time.Sleep(interval - 5*tenth) 135 | assert.Equal(t, int64(3), r.Rate()) 136 | time.Sleep(2 * tenth) 137 | assert.Equal(t, int64(1), r.Rate()) 138 | time.Sleep(2 * tenth) 139 | assert.Equal(t, int64(0), r.Rate()) 140 | time.Sleep(2 * tenth) 141 | assert.Equal(t, int64(0), r.Rate()) 142 | } 143 | 144 | func TestRateCounterMinResolution(t *testing.T) { 145 | defer func() { 146 | if r := recover(); r == nil { 147 | t.Errorf("Resolution < 1 did not panic") 148 | } 149 | }() 150 | 151 | NewRateCounter(50 * time.Millisecond).WithResolution(0) 152 | } 153 | 154 | func TestRateCounterNoResolution(t *testing.T) { 155 | interval := 50 * time.Millisecond 156 | tenth := 5 * time.Millisecond 157 | 158 | r := NewRateCounter(interval).WithResolution(1) 159 | 160 | assert.Equal(t, int64(0), r.Rate()) 161 | r.Incr(1) 162 | assert.Equal(t, int64(1), r.Rate()) 163 | time.Sleep(2 * tenth) 164 | r.Incr(1) 165 | assert.Equal(t, int64(2), r.Rate()) 166 | time.Sleep(2 * tenth) 167 | r.Incr(1) 168 | assert.Equal(t, int64(3), r.Rate()) 169 | time.Sleep(interval - 5*tenth) 170 | assert.Equal(t, int64(3), r.Rate()) 171 | time.Sleep(2 * tenth) 172 | assert.Equal(t, int64(0), r.Rate()) 173 | time.Sleep(2 * tenth) 174 | assert.Equal(t, int64(0), r.Rate()) 175 | time.Sleep(2 * tenth) 176 | assert.Equal(t, int64(0), r.Rate()) 177 | } 178 | 179 | func TestRateCounter_String(t *testing.T) { 180 | r := NewRateCounter(1 * time.Second) 181 | if r.String() != "0" { 182 | t.Error("Expected ", r.String(), " to equal ", "0") 183 | } 184 | 185 | r.Incr(1) 186 | if r.String() != "1" { 187 | t.Error("Expected ", r.String(), " to equal ", "1") 188 | } 189 | } 190 | 191 | func TestRateCounterHighResolutionMaxRate(t *testing.T) { 192 | interval := 500 * time.Millisecond 193 | tenth := 50 * time.Millisecond 194 | 195 | r := NewRateCounter(interval).WithResolution(100) 196 | 197 | assert.Equal(t, int64(0), r.MaxRate()) 198 | r.Incr(3) 199 | assert.Equal(t, int64(3), r.MaxRate()) 200 | time.Sleep(2 * tenth) 201 | r.Incr(2) 202 | assert.Equal(t, int64(3), r.MaxRate()) 203 | time.Sleep(2 * tenth) 204 | r.Incr(4) 205 | assert.Equal(t, int64(4), r.MaxRate()) 206 | time.Sleep(interval - 5*tenth) 207 | assert.Equal(t, int64(4), r.MaxRate()) 208 | time.Sleep(2 * tenth) 209 | assert.Equal(t, int64(4), r.MaxRate()) 210 | time.Sleep(2 * tenth) 211 | assert.Equal(t, int64(4), r.MaxRate()) 212 | time.Sleep(2 * tenth) 213 | assert.Equal(t, int64(0), r.MaxRate()) 214 | } 215 | 216 | func TestRateCounter_Incr_ReturnsImmediately(t *testing.T) { 217 | interval := 1 * time.Second 218 | r := NewRateCounter(interval) 219 | 220 | start := time.Now() 221 | r.Incr(-1) 222 | duration := time.Since(start) 223 | 224 | if duration >= 1*time.Second { 225 | t.Error("incr took", duration, "to return") 226 | } 227 | } 228 | 229 | func TestRateCounter_OnStop(t *testing.T) { 230 | var called Counter 231 | interval := 50 * time.Millisecond 232 | r := NewRateCounter(interval) 233 | r.OnStop(func(r *RateCounter) { 234 | called.Incr(1) 235 | }) 236 | r.Incr(1) 237 | 238 | current := called.Value() 239 | if current != 0 { 240 | t.Error("Expected called to equal 0, got ", current) 241 | } 242 | 243 | time.Sleep(2 * interval) 244 | current = called.Value() 245 | if current != 1 { 246 | t.Error("Expected called to equal 1, got ", current) 247 | } 248 | } 249 | 250 | func BenchmarkRateCounter(b *testing.B) { 251 | interval := 1 * time.Millisecond 252 | r := NewRateCounter(interval) 253 | 254 | for i := 0; i < b.N; i++ { 255 | r.Incr(1) 256 | r.Rate() 257 | } 258 | } 259 | 260 | func BenchmarkRateCounter_Parallel(b *testing.B) { 261 | interval := 1 * time.Millisecond 262 | r := NewRateCounter(interval) 263 | 264 | b.RunParallel(func(pb *testing.PB) { 265 | for pb.Next() { 266 | r.Incr(1) 267 | r.Rate() 268 | } 269 | }) 270 | } 271 | 272 | func BenchmarkRateCounter_With5MillionExisting(b *testing.B) { 273 | interval := 1 * time.Hour 274 | r := NewRateCounter(interval) 275 | 276 | for i := 0; i < 5000000; i++ { 277 | r.Incr(1) 278 | } 279 | 280 | b.ResetTimer() 281 | 282 | for i := 0; i < b.N; i++ { 283 | r.Incr(1) 284 | r.Rate() 285 | } 286 | } 287 | 288 | func Benchmark_TimeNowAndAdd(b *testing.B) { 289 | var a time.Time 290 | for i := 0; i < b.N; i++ { 291 | a = time.Now().Add(1 * time.Second) 292 | } 293 | fmt.Fprintln(ioutil.Discard, a) 294 | } 295 | --------------------------------------------------------------------------------