Skip to content
This repository was archived by the owner on Apr 1, 2025. It is now read-only.

Commit f1af7f7

Browse files
authored
Merge pull request #232 from deckarep/optimizations-lock-contention
Reduces lock-contention in high-performance usage of go-metrics.
2 parents 8732c61 + 3a868c8 commit f1af7f7

File tree

8 files changed

+175
-69
lines changed

8 files changed

+175
-69
lines changed

.travis.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
language: go
22

33
go:
4-
- 1.2
54
- 1.3
65
- 1.4
76
- 1.5

ewma.go

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -79,16 +79,15 @@ func (NilEWMA) Update(n int64) {}
7979
type StandardEWMA struct {
8080
uncounted int64 // /!\ this should be the first member to ensure 64-bit alignment
8181
alpha float64
82-
rate float64
83-
init bool
82+
rate uint64
83+
init uint32
8484
mutex sync.Mutex
8585
}
8686

8787
// Rate returns the moving average rate of events per second.
8888
func (a *StandardEWMA) Rate() float64 {
89-
a.mutex.Lock()
90-
defer a.mutex.Unlock()
91-
return a.rate * float64(1e9)
89+
currentRate := math.Float64frombits(atomic.LoadUint64(&a.rate)) * float64(1e9)
90+
return currentRate
9291
}
9392

9493
// Snapshot returns a read-only copy of the EWMA.
@@ -99,17 +98,38 @@ func (a *StandardEWMA) Snapshot() EWMA {
9998
// Tick ticks the clock to update the moving average. It assumes it is called
10099
// every five seconds.
101100
func (a *StandardEWMA) Tick() {
101+
// Optimization to avoid mutex locking in the hot-path.
102+
if atomic.LoadUint32(&a.init) == 1 {
103+
a.updateRate(a.fetchInstantRate())
104+
} else {
105+
// Slow-path: this is only needed on the first Tick() and preserves transactional updating
106+
// of init and rate in the else block. The first conditional is needed below because
107+
// a different thread could have set a.init = 1 between the time of the first atomic load and when
108+
// the lock was acquired.
109+
a.mutex.Lock()
110+
if atomic.LoadUint32(&a.init) == 1 {
111+
// The fetchInstantRate() uses atomic loading, which is unecessary in this critical section
112+
// but again, this section is only invoked on the first successful Tick() operation.
113+
a.updateRate(a.fetchInstantRate())
114+
} else {
115+
atomic.StoreUint32(&a.init, 1)
116+
atomic.StoreUint64(&a.rate, math.Float64bits(a.fetchInstantRate()))
117+
}
118+
a.mutex.Unlock()
119+
}
120+
}
121+
122+
func (a *StandardEWMA) fetchInstantRate() float64 {
102123
count := atomic.LoadInt64(&a.uncounted)
103124
atomic.AddInt64(&a.uncounted, -count)
104125
instantRate := float64(count) / float64(5e9)
105-
a.mutex.Lock()
106-
defer a.mutex.Unlock()
107-
if a.init {
108-
a.rate += a.alpha * (instantRate - a.rate)
109-
} else {
110-
a.init = true
111-
a.rate = instantRate
112-
}
126+
return instantRate
127+
}
128+
129+
func (a *StandardEWMA) updateRate(instantRate float64) {
130+
currentRate := math.Float64frombits(atomic.LoadUint64(&a.rate))
131+
currentRate += a.alpha * (instantRate - currentRate)
132+
atomic.StoreUint64(&a.rate, math.Float64bits(currentRate))
113133
}
114134

115135
// Update adds n uncounted events.

ewma_test.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package metrics
22

3-
import "testing"
3+
import (
4+
"math/rand"
5+
"sync"
6+
"testing"
7+
"time"
8+
)
49

510
func BenchmarkEWMA(b *testing.B) {
611
a := NewEWMA1()
@@ -11,6 +16,34 @@ func BenchmarkEWMA(b *testing.B) {
1116
}
1217
}
1318

19+
func BenchmarkEWMAParallel(b *testing.B) {
20+
a := NewEWMA1()
21+
b.ResetTimer()
22+
23+
b.RunParallel(func(pb *testing.PB) {
24+
for pb.Next() {
25+
a.Update(1)
26+
a.Tick()
27+
}
28+
})
29+
}
30+
31+
// exercise race detector
32+
func TestEWMAConcurrency(t *testing.T) {
33+
rand.Seed(time.Now().Unix())
34+
a := NewEWMA1()
35+
wg := &sync.WaitGroup{}
36+
reps := 100
37+
for i := 0; i < reps; i++ {
38+
wg.Add(1)
39+
go func(ewma EWMA, wg *sync.WaitGroup) {
40+
a.Update(rand.Int63())
41+
wg.Done()
42+
}(a, wg)
43+
}
44+
wg.Wait()
45+
}
46+
1447
func TestEWMA1(t *testing.T) {
1548
a := NewEWMA1()
1649
a.Update(3)

gauge_float64.go

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package metrics
22

3-
import "sync"
3+
import (
4+
"math"
5+
"sync/atomic"
6+
)
47

58
// GaugeFloat64s hold a float64 value that can be set arbitrarily.
69
type GaugeFloat64 interface {
@@ -85,8 +88,7 @@ func (NilGaugeFloat64) Value() float64 { return 0.0 }
8588
// StandardGaugeFloat64 is the standard implementation of a GaugeFloat64 and uses
8689
// sync.Mutex to manage a single float64 value.
8790
type StandardGaugeFloat64 struct {
88-
mutex sync.Mutex
89-
value float64
91+
value uint64
9092
}
9193

9294
// Snapshot returns a read-only copy of the gauge.
@@ -96,16 +98,12 @@ func (g *StandardGaugeFloat64) Snapshot() GaugeFloat64 {
9698

9799
// Update updates the gauge's value.
98100
func (g *StandardGaugeFloat64) Update(v float64) {
99-
g.mutex.Lock()
100-
defer g.mutex.Unlock()
101-
g.value = v
101+
atomic.StoreUint64(&g.value, math.Float64bits(v))
102102
}
103103

104104
// Value returns the gauge's current value.
105105
func (g *StandardGaugeFloat64) Value() float64 {
106-
g.mutex.Lock()
107-
defer g.mutex.Unlock()
108-
return g.value
106+
return math.Float64frombits(atomic.LoadUint64(&g.value))
109107
}
110108

111109
// FunctionalGaugeFloat64 returns value from given function

gauge_float64_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@ func BenchmarkGuageFloat64(b *testing.B) {
1010
}
1111
}
1212

13+
func BenchmarkGuageFloat64Parallel(b *testing.B) {
14+
g := NewGaugeFloat64()
15+
b.ResetTimer()
16+
b.RunParallel(func(pb *testing.PB) {
17+
for pb.Next() {
18+
g.Update(float64(1))
19+
}
20+
})
21+
}
22+
1323
func TestGaugeFloat64(t *testing.T) {
1424
g := NewGaugeFloat64()
1525
g.Update(float64(47.0))

gauge_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package metrics
22

33
import (
44
"fmt"
5+
"math/rand"
6+
"sync"
57
"testing"
8+
"time"
69
)
710

811
func BenchmarkGuage(b *testing.B) {
@@ -13,6 +16,22 @@ func BenchmarkGuage(b *testing.B) {
1316
}
1417
}
1518

19+
// exercise race detector
20+
func TestGaugeConcurrency(t *testing.T) {
21+
rand.Seed(time.Now().Unix())
22+
g := NewGauge()
23+
wg := &sync.WaitGroup{}
24+
reps := 100
25+
for i := 0; i < reps; i++ {
26+
wg.Add(1)
27+
go func(g Gauge, wg *sync.WaitGroup) {
28+
g.Update(rand.Int63())
29+
wg.Done()
30+
}(g, wg)
31+
}
32+
wg.Wait()
33+
}
34+
1635
func TestGauge(t *testing.T) {
1736
g := NewGauge()
1837
g.Update(int64(47))

meter.go

Lines changed: 38 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package metrics
22

33
import (
4+
"math"
45
"sync"
6+
"sync/atomic"
57
"time"
68
)
79

@@ -62,7 +64,7 @@ func NewRegisteredMeter(name string, r Registry) Meter {
6264
// MeterSnapshot is a read-only copy of another Meter.
6365
type MeterSnapshot struct {
6466
count int64
65-
rate1, rate5, rate15, rateMean float64
67+
rate1, rate5, rate15, rateMean uint64
6668
}
6769

6870
// Count returns the count of events at the time the snapshot was taken.
@@ -75,19 +77,19 @@ func (*MeterSnapshot) Mark(n int64) {
7577

7678
// Rate1 returns the one-minute moving average rate of events per second at the
7779
// time the snapshot was taken.
78-
func (m *MeterSnapshot) Rate1() float64 { return m.rate1 }
80+
func (m *MeterSnapshot) Rate1() float64 { return math.Float64frombits(m.rate1) }
7981

8082
// Rate5 returns the five-minute moving average rate of events per second at
8183
// the time the snapshot was taken.
82-
func (m *MeterSnapshot) Rate5() float64 { return m.rate5 }
84+
func (m *MeterSnapshot) Rate5() float64 { return math.Float64frombits(m.rate5) }
8385

8486
// Rate15 returns the fifteen-minute moving average rate of events per second
8587
// at the time the snapshot was taken.
86-
func (m *MeterSnapshot) Rate15() float64 { return m.rate15 }
88+
func (m *MeterSnapshot) Rate15() float64 { return math.Float64frombits(m.rate15) }
8789

8890
// RateMean returns the meter's mean rate of events per second at the time the
8991
// snapshot was taken.
90-
func (m *MeterSnapshot) RateMean() float64 { return m.rateMean }
92+
func (m *MeterSnapshot) RateMean() float64 { return math.Float64frombits(m.rateMean) }
9193

9294
// Snapshot returns the snapshot.
9395
func (m *MeterSnapshot) Snapshot() Meter { return m }
@@ -124,11 +126,12 @@ func (NilMeter) Stop() {}
124126

125127
// StandardMeter is the standard implementation of a Meter.
126128
type StandardMeter struct {
127-
lock sync.RWMutex
129+
// Only used on stop.
130+
lock sync.Mutex
128131
snapshot *MeterSnapshot
129132
a1, a5, a15 EWMA
130133
startTime time.Time
131-
stopped bool
134+
stopped uint32
132135
}
133136

134137
func newStandardMeter() *StandardMeter {
@@ -145,9 +148,9 @@ func newStandardMeter() *StandardMeter {
145148
func (m *StandardMeter) Stop() {
146149
m.lock.Lock()
147150
stopped := m.stopped
148-
m.stopped = true
151+
m.stopped = 1
149152
m.lock.Unlock()
150-
if !stopped {
153+
if stopped != 1 {
151154
arbiter.Lock()
152155
delete(arbiter.meters, m)
153156
arbiter.Unlock()
@@ -156,20 +159,17 @@ func (m *StandardMeter) Stop() {
156159

157160
// Count returns the number of events recorded.
158161
func (m *StandardMeter) Count() int64 {
159-
m.lock.RLock()
160-
count := m.snapshot.count
161-
m.lock.RUnlock()
162-
return count
162+
return atomic.LoadInt64(&m.snapshot.count)
163163
}
164164

165165
// Mark records the occurance of n events.
166166
func (m *StandardMeter) Mark(n int64) {
167-
m.lock.Lock()
168-
defer m.lock.Unlock()
169-
if m.stopped {
167+
if atomic.LoadUint32(&m.stopped) == 1 {
170168
return
171169
}
172-
m.snapshot.count += n
170+
171+
atomic.AddInt64(&m.snapshot.count, n)
172+
173173
m.a1.Update(n)
174174
m.a5.Update(n)
175175
m.a15.Update(n)
@@ -178,56 +178,49 @@ func (m *StandardMeter) Mark(n int64) {
178178

179179
// Rate1 returns the one-minute moving average rate of events per second.
180180
func (m *StandardMeter) Rate1() float64 {
181-
m.lock.RLock()
182-
rate1 := m.snapshot.rate1
183-
m.lock.RUnlock()
184-
return rate1
181+
return math.Float64frombits(atomic.LoadUint64(&m.snapshot.rate1))
185182
}
186183

187184
// Rate5 returns the five-minute moving average rate of events per second.
188185
func (m *StandardMeter) Rate5() float64 {
189-
m.lock.RLock()
190-
rate5 := m.snapshot.rate5
191-
m.lock.RUnlock()
192-
return rate5
186+
return math.Float64frombits(atomic.LoadUint64(&m.snapshot.rate5))
193187
}
194188

195189
// Rate15 returns the fifteen-minute moving average rate of events per second.
196190
func (m *StandardMeter) Rate15() float64 {
197-
m.lock.RLock()
198-
rate15 := m.snapshot.rate15
199-
m.lock.RUnlock()
200-
return rate15
191+
return math.Float64frombits(atomic.LoadUint64(&m.snapshot.rate15))
201192
}
202193

203194
// RateMean returns the meter's mean rate of events per second.
204195
func (m *StandardMeter) RateMean() float64 {
205-
m.lock.RLock()
206-
rateMean := m.snapshot.rateMean
207-
m.lock.RUnlock()
208-
return rateMean
196+
return math.Float64frombits(atomic.LoadUint64(&m.snapshot.rateMean))
209197
}
210198

211199
// Snapshot returns a read-only copy of the meter.
212200
func (m *StandardMeter) Snapshot() Meter {
213-
m.lock.RLock()
214-
snapshot := *m.snapshot
215-
m.lock.RUnlock()
216-
return &snapshot
201+
copiedSnapshot := MeterSnapshot{
202+
count: atomic.LoadInt64(&m.snapshot.count),
203+
rate1: atomic.LoadUint64(&m.snapshot.rate1),
204+
rate5: atomic.LoadUint64(&m.snapshot.rate5),
205+
rate15: atomic.LoadUint64(&m.snapshot.rate15),
206+
rateMean: atomic.LoadUint64(&m.snapshot.rateMean),
207+
}
208+
return &copiedSnapshot
217209
}
218210

219211
func (m *StandardMeter) updateSnapshot() {
220-
// should run with write lock held on m.lock
221-
snapshot := m.snapshot
222-
snapshot.rate1 = m.a1.Rate()
223-
snapshot.rate5 = m.a5.Rate()
224-
snapshot.rate15 = m.a15.Rate()
225-
snapshot.rateMean = float64(snapshot.count) / time.Since(m.startTime).Seconds()
212+
rate1 := math.Float64bits(m.a1.Rate())
213+
rate5 := math.Float64bits(m.a5.Rate())
214+
rate15 := math.Float64bits(m.a15.Rate())
215+
rateMean := math.Float64bits(float64(m.Count()) / time.Since(m.startTime).Seconds())
216+
217+
atomic.StoreUint64(&m.snapshot.rate1, rate1)
218+
atomic.StoreUint64(&m.snapshot.rate5, rate5)
219+
atomic.StoreUint64(&m.snapshot.rate15, rate15)
220+
atomic.StoreUint64(&m.snapshot.rateMean, rateMean)
226221
}
227222

228223
func (m *StandardMeter) tick() {
229-
m.lock.Lock()
230-
defer m.lock.Unlock()
231224
m.a1.Tick()
232225
m.a5.Tick()
233226
m.a15.Tick()

0 commit comments

Comments
 (0)