New Upstream Snapshot - golang-github-rcrowley-go-metrics
Ready changes
Summary
Merged new upstream version: 0.0~git20201226.cf1acfc (was: 0.0~git20180125.8732c61).
Resulting package
Built on 2022-11-19T18:56 (took 5m13s)
The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:
apt install -t fresh-snapshots golang-github-rcrowley-go-metrics-dev
Lintian Result
- golang-github-rcrowley-go-metrics-dev_0.0~git20201226.cf1acfc-1~jan+nus1_all.deb
- golang-github-rcrowley-go-metrics_0.0~git20201226.cf1acfc-1~jan+nus1.dsc
- golang-github-rcrowley-go-metrics_0.0~git20201226.cf1acfc-1~jan+nus1_amd64.buildinfo
- golang-github-rcrowley-go-metrics_0.0~git20201226.cf1acfc-1~jan+nus1_amd64.changes
Diff
diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index 83c8f82..0000000
--- a/.gitignore
+++ /dev/null
@@ -1,9 +0,0 @@
-*.[68]
-*.a
-*.out
-*.swp
-_obj
-_testmain.go
-cmd/metrics-bench/metrics-bench
-cmd/metrics-example/metrics-example
-cmd/never-read/never-read
diff --git a/.travis.yml b/.travis.yml
index f8b3b2e..ce9afea 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,14 +1,19 @@
language: go
go:
- - 1.2
- - 1.3
- - 1.4
- - 1.5
- - 1.6
- - 1.7
- - 1.8
- - 1.9
+ - "1.3"
+ - "1.4"
+ - "1.5"
+ - "1.6"
+ - "1.7"
+ - "1.8"
+ - "1.9"
+ - "1.10"
+ - "1.11"
+ - "1.12"
+ - "1.13"
+ - "1.14"
+ - "1.15"
script:
- ./validate.sh
diff --git a/README.md b/README.md
index 17cea76..27ddfee 100644
--- a/README.md
+++ b/README.md
@@ -157,6 +157,7 @@ Publishing Metrics
Clients are available for the following destinations:
+* AppOptics - https://github.com/ysamlan/go-metrics-appoptics
* Librato - https://github.com/mihasya/go-metrics-librato
* Graphite - https://github.com/cyberdelia/go-metrics-graphite
* InfluxDB - https://github.com/vrischmann/go-metrics-influxdb
@@ -165,3 +166,6 @@ Clients are available for the following destinations:
* DataDog - https://github.com/syntaqx/go-metrics-datadog
* SignalFX - https://github.com/pascallouisperez/go-metrics-signalfx
* Honeycomb - https://github.com/getspine/go-metrics-honeycomb
+* Wavefront - https://github.com/wavefrontHQ/go-metrics-wavefront
+* Open-Falcon - https://github.com/g4zhuj/go-metrics-falcon
+* AWS CloudWatch - [https://github.com/savaki/cloudmetrics](https://github.com/savaki/cloudmetrics)
diff --git a/debian/changelog b/debian/changelog
index d07a7f0..40e263a 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+golang-github-rcrowley-go-metrics (0.0~git20201226.cf1acfc-1) UNRELEASED; urgency=low
+
+ * New upstream snapshot.
+
+ -- Debian Janitor <janitor@jelmer.uk> Sat, 19 Nov 2022 18:53:05 -0000
+
golang-github-rcrowley-go-metrics (0.0~git20180125.8732c61-3) unstable; urgency=medium
* Team upload.
diff --git a/debian/patches/precision.patch b/debian/patches/precision.patch
index b7e32f4..350b6e2 100644
--- a/debian/patches/precision.patch
+++ b/debian/patches/precision.patch
@@ -10,9 +10,11 @@ NB: I've stripped out the optimizations as they are not merged upstream. I did,
however, keep in the parts that fix the bitwise float comparison numerics. The
test-code now does a delta test instead which should work more reliably.
---- a/ewma_test.go
-+++ b/ewma_test.go
-@@ -15,67 +15,67 @@
+Index: golang-github-rcrowley-go-metrics.git/ewma_test.go
+===================================================================
+--- golang-github-rcrowley-go-metrics.git.orig/ewma_test.go
++++ golang-github-rcrowley-go-metrics.git/ewma_test.go
+@@ -48,67 +48,67 @@ func TestEWMA1(t *testing.T) {
a := NewEWMA1()
a.Update(3)
a.Tick()
@@ -96,7 +98,7 @@ test-code now does a delta test instead which should work more reliably.
t.Errorf("15 minute a.Rate(): 1.8354139230109722e-07 != %v\n", rate)
}
}
-@@ -84,67 +84,67 @@
+@@ -117,67 +117,67 @@ func TestEWMA5(t *testing.T) {
a := NewEWMA5()
a.Update(3)
a.Tick()
@@ -180,7 +182,7 @@ test-code now does a delta test instead which should work more reliably.
t.Errorf("15 minute a.Rate(): 0.0298722410207183831020718428 != %v\n", rate)
}
}
-@@ -153,67 +153,67 @@
+@@ -186,67 +186,67 @@ func TestEWMA15(t *testing.T) {
a := NewEWMA15()
a.Update(3)
a.Tick()
@@ -264,9 +266,11 @@ test-code now does a delta test instead which should work more reliably.
t.Errorf("15 minute a.Rate(): 0.2207276647028646247028654470286553 != %v\n", rate)
}
}
---- a/histogram_test.go
-+++ b/histogram_test.go
-@@ -76,20 +76,20 @@
+Index: golang-github-rcrowley-go-metrics.git/histogram_test.go
+===================================================================
+--- golang-github-rcrowley-go-metrics.git.orig/histogram_test.go
++++ golang-github-rcrowley-go-metrics.git/histogram_test.go
+@@ -76,20 +76,20 @@ func testHistogram10000(t *testing.T, h
if max := h.Max(); 10000 != max {
t.Errorf("h.Max(): 10000 != %v\n", max)
}
@@ -292,11 +296,13 @@ test-code now does a delta test instead which should work more reliably.
t.Errorf("99th percentile: 9900.99 != %v\n", ps[2])
}
}
---- a/sample_test.go
-+++ b/sample_test.go
+Index: golang-github-rcrowley-go-metrics.git/sample_test.go
+===================================================================
+--- golang-github-rcrowley-go-metrics.git.orig/sample_test.go
++++ golang-github-rcrowley-go-metrics.git/sample_test.go
@@ -1,12 +1,21 @@
package metrics
-
+
import (
+ "math"
"math/rand"
@@ -304,7 +310,7 @@ test-code now does a delta test instead which should work more reliably.
"testing"
"time"
)
-
+
+func float64NotEqual(a, b float64) bool {
+ v := math.Abs(a - b)
+ if b == 0.0 && v > 0.00001 {
@@ -316,7 +322,7 @@ test-code now does a delta test instead which should work more reliably.
// Benchmark{Compute,Copy}{1000,1000000} demonstrate that, even for relatively
// expensive computations like Variance, the cost of copying the Sample, as
// approximated by a make and copy, is much greater than the cost of the
-@@ -285,20 +336,20 @@
+@@ -285,20 +294,20 @@ func testExpDecaySampleStatistics(t *tes
if max := s.Max(); 10000 != max {
t.Errorf("s.Max(): 10000 != %v\n", max)
}
@@ -342,7 +348,7 @@ test-code now does a delta test instead which should work more reliably.
t.Errorf("99th percentile: 9998.99 != %v\n", ps[2])
}
}
-@@ -313,20 +364,20 @@
+@@ -313,20 +322,20 @@ func testUniformSampleStatistics(t *test
if max := s.Max(); 9989 != max {
t.Errorf("s.Max(): 9989 != %v\n", max)
}
@@ -368,9 +374,11 @@ test-code now does a delta test instead which should work more reliably.
t.Errorf("99th percentile: 9986.429999999998 != %v\n", ps[2])
}
}
---- a/timer_test.go
-+++ b/timer_test.go
-@@ -27,7 +27,7 @@
+Index: golang-github-rcrowley-go-metrics.git/timer_test.go
+===================================================================
+--- golang-github-rcrowley-go-metrics.git.orig/timer_test.go
++++ golang-github-rcrowley-go-metrics.git/timer_test.go
+@@ -27,7 +27,7 @@ func TestTimerExtremes(t *testing.T) {
tm := NewTimer()
tm.Update(math.MaxInt64)
tm.Update(0)
diff --git a/debug.go b/debug.go
index 043ccef..179e5aa 100644
--- a/debug.go
+++ b/debug.go
@@ -2,6 +2,7 @@ package metrics
import (
"runtime/debug"
+ "sync"
"time"
)
@@ -16,7 +17,8 @@ var (
}
ReadGCStats Timer
}
- gcStats debug.GCStats
+ gcStats debug.GCStats
+ registerDebugMetricsOnce = sync.Once{}
)
// Capture new values for the Go garbage collector statistics exported in
@@ -54,19 +56,21 @@ func CaptureDebugGCStatsOnce(r Registry) {
// debug.GCStats. The metrics are named by their fully-qualified Go symbols,
// i.e. debug.GCStats.PauseTotal.
func RegisterDebugGCStats(r Registry) {
- debugMetrics.GCStats.LastGC = NewGauge()
- debugMetrics.GCStats.NumGC = NewGauge()
- debugMetrics.GCStats.Pause = NewHistogram(NewExpDecaySample(1028, 0.015))
- //debugMetrics.GCStats.PauseQuantiles = NewHistogram(NewExpDecaySample(1028, 0.015))
- debugMetrics.GCStats.PauseTotal = NewGauge()
- debugMetrics.ReadGCStats = NewTimer()
+ registerDebugMetricsOnce.Do(func() {
+ debugMetrics.GCStats.LastGC = NewGauge()
+ debugMetrics.GCStats.NumGC = NewGauge()
+ debugMetrics.GCStats.Pause = NewHistogram(NewExpDecaySample(1028, 0.015))
+ //debugMetrics.GCStats.PauseQuantiles = NewHistogram(NewExpDecaySample(1028, 0.015))
+ debugMetrics.GCStats.PauseTotal = NewGauge()
+ debugMetrics.ReadGCStats = NewTimer()
- r.Register("debug.GCStats.LastGC", debugMetrics.GCStats.LastGC)
- r.Register("debug.GCStats.NumGC", debugMetrics.GCStats.NumGC)
- r.Register("debug.GCStats.Pause", debugMetrics.GCStats.Pause)
- //r.Register("debug.GCStats.PauseQuantiles", debugMetrics.GCStats.PauseQuantiles)
- r.Register("debug.GCStats.PauseTotal", debugMetrics.GCStats.PauseTotal)
- r.Register("debug.ReadGCStats", debugMetrics.ReadGCStats)
+ r.Register("debug.GCStats.LastGC", debugMetrics.GCStats.LastGC)
+ r.Register("debug.GCStats.NumGC", debugMetrics.GCStats.NumGC)
+ r.Register("debug.GCStats.Pause", debugMetrics.GCStats.Pause)
+ //r.Register("debug.GCStats.PauseQuantiles", debugMetrics.GCStats.PauseQuantiles)
+ r.Register("debug.GCStats.PauseTotal", debugMetrics.GCStats.PauseTotal)
+ r.Register("debug.ReadGCStats", debugMetrics.ReadGCStats)
+ })
}
// Allocate an initial slice for gcStats.Pause to avoid allocations during
diff --git a/debug_test.go b/debug_test.go
index 07eb867..1c4000b 100644
--- a/debug_test.go
+++ b/debug_test.go
@@ -46,3 +46,26 @@ func testDebugGCStatsBlocking(ch chan int) {
}
}
}
+
+func TestDebugGCStatsDoubleRegister(t *testing.T) {
+ r := NewRegistry()
+ RegisterDebugGCStats(r)
+ var storedGauge = (r.Get("debug.GCStats.LastGC")).(Gauge)
+
+ runtime.GC()
+ CaptureDebugGCStatsOnce(r)
+
+ firstGC := storedGauge.Value()
+ if 0 == firstGC {
+ t.Errorf("firstGC got %d, expected > 0", firstGC)
+ }
+
+ time.Sleep(time.Millisecond)
+
+ RegisterDebugGCStats(r)
+ runtime.GC()
+ CaptureDebugGCStatsOnce(r)
+ if lastGC := storedGauge.Value(); firstGC == lastGC {
+ t.Errorf("lastGC got %d, expected a higher timestamp value", lastGC)
+ }
+}
diff --git a/ewma.go b/ewma.go
index 694a1d0..a8183dd 100644
--- a/ewma.go
+++ b/ewma.go
@@ -79,16 +79,15 @@ func (NilEWMA) Update(n int64) {}
type StandardEWMA struct {
uncounted int64 // /!\ this should be the first member to ensure 64-bit alignment
alpha float64
- rate float64
- init bool
+ rate uint64
+ init uint32
mutex sync.Mutex
}
// Rate returns the moving average rate of events per second.
func (a *StandardEWMA) Rate() float64 {
- a.mutex.Lock()
- defer a.mutex.Unlock()
- return a.rate * float64(1e9)
+ currentRate := math.Float64frombits(atomic.LoadUint64(&a.rate)) * float64(1e9)
+ return currentRate
}
// Snapshot returns a read-only copy of the EWMA.
@@ -99,17 +98,38 @@ func (a *StandardEWMA) Snapshot() EWMA {
// Tick ticks the clock to update the moving average. It assumes it is called
// every five seconds.
func (a *StandardEWMA) Tick() {
+ // Optimization to avoid mutex locking in the hot-path.
+ if atomic.LoadUint32(&a.init) == 1 {
+ a.updateRate(a.fetchInstantRate())
+ } else {
+ // Slow-path: this is only needed on the first Tick() and preserves transactional updating
+ // of init and rate in the else block. The first conditional is needed below because
+ // a different thread could have set a.init = 1 between the time of the first atomic load and when
+ // the lock was acquired.
+ a.mutex.Lock()
+ if atomic.LoadUint32(&a.init) == 1 {
+ // The fetchInstantRate() uses atomic loading, which is unecessary in this critical section
+ // but again, this section is only invoked on the first successful Tick() operation.
+ a.updateRate(a.fetchInstantRate())
+ } else {
+ atomic.StoreUint32(&a.init, 1)
+ atomic.StoreUint64(&a.rate, math.Float64bits(a.fetchInstantRate()))
+ }
+ a.mutex.Unlock()
+ }
+}
+
+func (a *StandardEWMA) fetchInstantRate() float64 {
count := atomic.LoadInt64(&a.uncounted)
atomic.AddInt64(&a.uncounted, -count)
instantRate := float64(count) / float64(5e9)
- a.mutex.Lock()
- defer a.mutex.Unlock()
- if a.init {
- a.rate += a.alpha * (instantRate - a.rate)
- } else {
- a.init = true
- a.rate = instantRate
- }
+ return instantRate
+}
+
+func (a *StandardEWMA) updateRate(instantRate float64) {
+ currentRate := math.Float64frombits(atomic.LoadUint64(&a.rate))
+ currentRate += a.alpha * (instantRate - currentRate)
+ atomic.StoreUint64(&a.rate, math.Float64bits(currentRate))
}
// Update adds n uncounted events.
diff --git a/ewma_test.go b/ewma_test.go
index 0430fbd..058ae2d 100644
--- a/ewma_test.go
+++ b/ewma_test.go
@@ -1,6 +1,11 @@
package metrics
-import "testing"
+import (
+ "math/rand"
+ "sync"
+ "testing"
+ "time"
+)
func BenchmarkEWMA(b *testing.B) {
a := NewEWMA1()
@@ -11,6 +16,34 @@ func BenchmarkEWMA(b *testing.B) {
}
}
+func BenchmarkEWMAParallel(b *testing.B) {
+ a := NewEWMA1()
+ b.ResetTimer()
+
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ a.Update(1)
+ a.Tick()
+ }
+ })
+}
+
+// exercise race detector
+func TestEWMAConcurrency(t *testing.T) {
+ rand.Seed(time.Now().Unix())
+ a := NewEWMA1()
+ wg := &sync.WaitGroup{}
+ reps := 100
+ for i := 0; i < reps; i++ {
+ wg.Add(1)
+ go func(ewma EWMA, wg *sync.WaitGroup) {
+ a.Update(rand.Int63())
+ wg.Done()
+ }(a, wg)
+ }
+ wg.Wait()
+}
+
func TestEWMA1(t *testing.T) {
a := NewEWMA1()
a.Update(3)
diff --git a/gauge_float64.go b/gauge_float64.go
index 6f93920..3962e6d 100644
--- a/gauge_float64.go
+++ b/gauge_float64.go
@@ -1,6 +1,9 @@
package metrics
-import "sync"
+import (
+ "math"
+ "sync/atomic"
+)
// GaugeFloat64s hold a float64 value that can be set arbitrarily.
type GaugeFloat64 interface {
@@ -85,8 +88,7 @@ func (NilGaugeFloat64) Value() float64 { return 0.0 }
// StandardGaugeFloat64 is the standard implementation of a GaugeFloat64 and uses
// sync.Mutex to manage a single float64 value.
type StandardGaugeFloat64 struct {
- mutex sync.Mutex
- value float64
+ value uint64
}
// Snapshot returns a read-only copy of the gauge.
@@ -96,16 +98,12 @@ func (g *StandardGaugeFloat64) Snapshot() GaugeFloat64 {
// Update updates the gauge's value.
func (g *StandardGaugeFloat64) Update(v float64) {
- g.mutex.Lock()
- defer g.mutex.Unlock()
- g.value = v
+ atomic.StoreUint64(&g.value, math.Float64bits(v))
}
// Value returns the gauge's current value.
func (g *StandardGaugeFloat64) Value() float64 {
- g.mutex.Lock()
- defer g.mutex.Unlock()
- return g.value
+ return math.Float64frombits(atomic.LoadUint64(&g.value))
}
// FunctionalGaugeFloat64 returns value from given function
diff --git a/gauge_float64_test.go b/gauge_float64_test.go
index 99e62a4..6769b95 100644
--- a/gauge_float64_test.go
+++ b/gauge_float64_test.go
@@ -10,6 +10,16 @@ func BenchmarkGuageFloat64(b *testing.B) {
}
}
+func BenchmarkGuageFloat64Parallel(b *testing.B) {
+ g := NewGaugeFloat64()
+ b.ResetTimer()
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ g.Update(float64(1))
+ }
+ })
+}
+
func TestGaugeFloat64(t *testing.T) {
g := NewGaugeFloat64()
g.Update(float64(47.0))
diff --git a/gauge_test.go b/gauge_test.go
index 1f2603d..d54d6f1 100644
--- a/gauge_test.go
+++ b/gauge_test.go
@@ -2,7 +2,10 @@ package metrics
import (
"fmt"
+ "math/rand"
+ "sync"
"testing"
+ "time"
)
func BenchmarkGuage(b *testing.B) {
@@ -13,6 +16,22 @@ func BenchmarkGuage(b *testing.B) {
}
}
+// exercise race detector
+func TestGaugeConcurrency(t *testing.T) {
+ rand.Seed(time.Now().Unix())
+ g := NewGauge()
+ wg := &sync.WaitGroup{}
+ reps := 100
+ for i := 0; i < reps; i++ {
+ wg.Add(1)
+ go func(g Gauge, wg *sync.WaitGroup) {
+ g.Update(rand.Int63())
+ wg.Done()
+ }(g, wg)
+ }
+ wg.Wait()
+}
+
func TestGauge(t *testing.T) {
g := NewGauge()
g.Update(int64(47))
diff --git a/log.go b/log.go
index f8074c0..2614a0a 100644
--- a/log.go
+++ b/log.go
@@ -8,17 +8,37 @@ type Logger interface {
Printf(format string, v ...interface{})
}
+// Log outputs each metric in the given registry periodically using the given logger.
func Log(r Registry, freq time.Duration, l Logger) {
LogScaled(r, freq, time.Nanosecond, l)
}
-// Output each metric in the given registry periodically using the given
+// LogOnCue outputs each metric in the given registry on demand through the channel
+// using the given logger
+func LogOnCue(r Registry, ch chan interface{}, l Logger) {
+ LogScaledOnCue(r, ch, time.Nanosecond, l)
+}
+
+// LogScaled outputs each metric in the given registry periodically using the given
// logger. Print timings in `scale` units (eg time.Millisecond) rather than nanos.
func LogScaled(r Registry, freq time.Duration, scale time.Duration, l Logger) {
+ ch := make(chan interface{})
+ go func(channel chan interface{}) {
+ for _ = range time.Tick(freq) {
+ channel <- struct{}{}
+ }
+ }(ch)
+ LogScaledOnCue(r, ch, scale, l)
+}
+
+// LogScaledOnCue outputs each metric in the given registry on demand through the channel
+// using the given logger. Print timings in `scale` units (eg time.Millisecond) rather
+// than nanos.
+func LogScaledOnCue(r Registry, ch chan interface{}, scale time.Duration, l Logger) {
du := float64(scale)
duSuffix := scale.String()[1:]
- for _ = range time.Tick(freq) {
+ for _ = range ch {
r.Each(func(name string, i interface{}) {
switch metric := i.(type) {
case Counter:
diff --git a/meter.go b/meter.go
index 53ff329..223669b 100644
--- a/meter.go
+++ b/meter.go
@@ -1,7 +1,9 @@
package metrics
import (
+ "math"
"sync"
+ "sync/atomic"
"time"
)
@@ -62,7 +64,7 @@ func NewRegisteredMeter(name string, r Registry) Meter {
// MeterSnapshot is a read-only copy of another Meter.
type MeterSnapshot struct {
count int64
- rate1, rate5, rate15, rateMean float64
+ rate1, rate5, rate15, rateMean uint64
}
// Count returns the count of events at the time the snapshot was taken.
@@ -75,19 +77,19 @@ func (*MeterSnapshot) Mark(n int64) {
// Rate1 returns the one-minute moving average rate of events per second at the
// time the snapshot was taken.
-func (m *MeterSnapshot) Rate1() float64 { return m.rate1 }
+func (m *MeterSnapshot) Rate1() float64 { return math.Float64frombits(m.rate1) }
// Rate5 returns the five-minute moving average rate of events per second at
// the time the snapshot was taken.
-func (m *MeterSnapshot) Rate5() float64 { return m.rate5 }
+func (m *MeterSnapshot) Rate5() float64 { return math.Float64frombits(m.rate5) }
// Rate15 returns the fifteen-minute moving average rate of events per second
// at the time the snapshot was taken.
-func (m *MeterSnapshot) Rate15() float64 { return m.rate15 }
+func (m *MeterSnapshot) Rate15() float64 { return math.Float64frombits(m.rate15) }
// RateMean returns the meter's mean rate of events per second at the time the
// snapshot was taken.
-func (m *MeterSnapshot) RateMean() float64 { return m.rateMean }
+func (m *MeterSnapshot) RateMean() float64 { return math.Float64frombits(m.rateMean) }
// Snapshot returns the snapshot.
func (m *MeterSnapshot) Snapshot() Meter { return m }
@@ -124,11 +126,10 @@ func (NilMeter) Stop() {}
// StandardMeter is the standard implementation of a Meter.
type StandardMeter struct {
- lock sync.RWMutex
snapshot *MeterSnapshot
a1, a5, a15 EWMA
startTime time.Time
- stopped bool
+ stopped uint32
}
func newStandardMeter() *StandardMeter {
@@ -143,11 +144,7 @@ func newStandardMeter() *StandardMeter {
// Stop stops the meter, Mark() will be a no-op if you use it after being stopped.
func (m *StandardMeter) Stop() {
- m.lock.Lock()
- stopped := m.stopped
- m.stopped = true
- m.lock.Unlock()
- if !stopped {
+ if atomic.CompareAndSwapUint32(&m.stopped, 0, 1) {
arbiter.Lock()
delete(arbiter.meters, m)
arbiter.Unlock()
@@ -156,20 +153,17 @@ func (m *StandardMeter) Stop() {
// Count returns the number of events recorded.
func (m *StandardMeter) Count() int64 {
- m.lock.RLock()
- count := m.snapshot.count
- m.lock.RUnlock()
- return count
+ return atomic.LoadInt64(&m.snapshot.count)
}
// Mark records the occurance of n events.
func (m *StandardMeter) Mark(n int64) {
- m.lock.Lock()
- defer m.lock.Unlock()
- if m.stopped {
+ if atomic.LoadUint32(&m.stopped) == 1 {
return
}
- m.snapshot.count += n
+
+ atomic.AddInt64(&m.snapshot.count, n)
+
m.a1.Update(n)
m.a5.Update(n)
m.a15.Update(n)
@@ -178,56 +172,49 @@ func (m *StandardMeter) Mark(n int64) {
// Rate1 returns the one-minute moving average rate of events per second.
func (m *StandardMeter) Rate1() float64 {
- m.lock.RLock()
- rate1 := m.snapshot.rate1
- m.lock.RUnlock()
- return rate1
+ return math.Float64frombits(atomic.LoadUint64(&m.snapshot.rate1))
}
// Rate5 returns the five-minute moving average rate of events per second.
func (m *StandardMeter) Rate5() float64 {
- m.lock.RLock()
- rate5 := m.snapshot.rate5
- m.lock.RUnlock()
- return rate5
+ return math.Float64frombits(atomic.LoadUint64(&m.snapshot.rate5))
}
// Rate15 returns the fifteen-minute moving average rate of events per second.
func (m *StandardMeter) Rate15() float64 {
- m.lock.RLock()
- rate15 := m.snapshot.rate15
- m.lock.RUnlock()
- return rate15
+ return math.Float64frombits(atomic.LoadUint64(&m.snapshot.rate15))
}
// RateMean returns the meter's mean rate of events per second.
func (m *StandardMeter) RateMean() float64 {
- m.lock.RLock()
- rateMean := m.snapshot.rateMean
- m.lock.RUnlock()
- return rateMean
+ return math.Float64frombits(atomic.LoadUint64(&m.snapshot.rateMean))
}
// Snapshot returns a read-only copy of the meter.
func (m *StandardMeter) Snapshot() Meter {
- m.lock.RLock()
- snapshot := *m.snapshot
- m.lock.RUnlock()
- return &snapshot
+ copiedSnapshot := MeterSnapshot{
+ count: atomic.LoadInt64(&m.snapshot.count),
+ rate1: atomic.LoadUint64(&m.snapshot.rate1),
+ rate5: atomic.LoadUint64(&m.snapshot.rate5),
+ rate15: atomic.LoadUint64(&m.snapshot.rate15),
+ rateMean: atomic.LoadUint64(&m.snapshot.rateMean),
+ }
+ return &copiedSnapshot
}
func (m *StandardMeter) updateSnapshot() {
- // should run with write lock held on m.lock
- snapshot := m.snapshot
- snapshot.rate1 = m.a1.Rate()
- snapshot.rate5 = m.a5.Rate()
- snapshot.rate15 = m.a15.Rate()
- snapshot.rateMean = float64(snapshot.count) / time.Since(m.startTime).Seconds()
+ rate1 := math.Float64bits(m.a1.Rate())
+ rate5 := math.Float64bits(m.a5.Rate())
+ rate15 := math.Float64bits(m.a15.Rate())
+ rateMean := math.Float64bits(float64(m.Count()) / time.Since(m.startTime).Seconds())
+
+ atomic.StoreUint64(&m.snapshot.rate1, rate1)
+ atomic.StoreUint64(&m.snapshot.rate5, rate5)
+ atomic.StoreUint64(&m.snapshot.rate15, rate15)
+ atomic.StoreUint64(&m.snapshot.rateMean, rateMean)
}
func (m *StandardMeter) tick() {
- m.lock.Lock()
- defer m.lock.Unlock()
m.a1.Tick()
m.a5.Tick()
m.a15.Tick()
diff --git a/meter_test.go b/meter_test.go
index e889222..ecef37d 100644
--- a/meter_test.go
+++ b/meter_test.go
@@ -1,6 +1,8 @@
package metrics
import (
+ "math/rand"
+ "sync"
"testing"
"time"
)
@@ -13,6 +15,43 @@ func BenchmarkMeter(b *testing.B) {
}
}
+func BenchmarkMeterParallel(b *testing.B) {
+ m := NewMeter()
+ b.ResetTimer()
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ m.Mark(1)
+ }
+ })
+}
+
+// exercise race detector
+func TestMeterConcurrency(t *testing.T) {
+ rand.Seed(time.Now().Unix())
+ ma := meterArbiter{
+ ticker: time.NewTicker(time.Millisecond),
+ meters: make(map[*StandardMeter]struct{}),
+ }
+ m := newStandardMeter()
+ ma.meters[m] = struct{}{}
+ go ma.tick()
+ wg := &sync.WaitGroup{}
+ reps := 100
+ for i := 0; i < reps; i++ {
+ wg.Add(1)
+ go func(m Meter, wg *sync.WaitGroup) {
+ m.Mark(1)
+ wg.Done()
+ }(m, wg)
+ wg.Add(1)
+ go func(m Meter, wg *sync.WaitGroup) {
+ m.Stop()
+ wg.Done()
+ }(m, wg)
+ }
+ wg.Wait()
+}
+
func TestGetOrRegisterMeter(t *testing.T) {
r := NewRegistry()
NewRegisteredMeter("foo", r).Mark(47)
diff --git a/registry.go b/registry.go
index 6c0007b..a8e6722 100644
--- a/registry.go
+++ b/registry.go
@@ -54,7 +54,7 @@ type Registry interface {
// of names to metrics.
type StandardRegistry struct {
metrics map[string]interface{}
- mutex sync.Mutex
+ mutex sync.RWMutex
}
// Create a new registry.
@@ -64,15 +64,17 @@ func NewRegistry() Registry {
// Call the given function for each registered metric.
func (r *StandardRegistry) Each(f func(string, interface{})) {
- for name, i := range r.registered() {
- f(name, i)
+ metrics := r.registered()
+ for i := range metrics {
+ kv := &metrics[i]
+ f(kv.name, kv.value)
}
}
// Get the metric by the given name or nil if none is registered.
func (r *StandardRegistry) Get(name string) interface{} {
- r.mutex.Lock()
- defer r.mutex.Unlock()
+ r.mutex.RLock()
+ defer r.mutex.RUnlock()
return r.metrics[name]
}
@@ -81,6 +83,15 @@ func (r *StandardRegistry) Get(name string) interface{} {
// The interface can be the metric to register if not found in registry,
// or a function returning the metric for lazy instantiation.
func (r *StandardRegistry) GetOrRegister(name string, i interface{}) interface{} {
+ // access the read lock first which should be re-entrant
+ r.mutex.RLock()
+ metric, ok := r.metrics[name]
+ r.mutex.RUnlock()
+ if ok {
+ return metric
+ }
+
+ // only take the write lock if we'll be modifying the metrics map
r.mutex.Lock()
defer r.mutex.Unlock()
if metric, ok := r.metrics[name]; ok {
@@ -103,8 +114,8 @@ func (r *StandardRegistry) Register(name string, i interface{}) error {
// Run all registered healthchecks.
func (r *StandardRegistry) RunHealthchecks() {
- r.mutex.Lock()
- defer r.mutex.Unlock()
+ r.mutex.RLock()
+ defer r.mutex.RUnlock()
for _, i := range r.metrics {
if h, ok := i.(Healthcheck); ok {
h.Check()
@@ -202,12 +213,20 @@ func (r *StandardRegistry) register(name string, i interface{}) error {
return nil
}
-func (r *StandardRegistry) registered() map[string]interface{} {
- r.mutex.Lock()
- defer r.mutex.Unlock()
- metrics := make(map[string]interface{}, len(r.metrics))
+type metricKV struct {
+ name string
+ value interface{}
+}
+
+func (r *StandardRegistry) registered() []metricKV {
+ r.mutex.RLock()
+ defer r.mutex.RUnlock()
+ metrics := make([]metricKV, 0, len(r.metrics))
for name, i := range r.metrics {
- metrics[name] = i
+ metrics = append(metrics, metricKV{
+ name: name,
+ value: i,
+ })
}
return metrics
}
diff --git a/registry_test.go b/registry_test.go
index a63e485..c3b0855 100644
--- a/registry_test.go
+++ b/registry_test.go
@@ -1,6 +1,8 @@
package metrics
import (
+ "fmt"
+ "sync"
"testing"
)
@@ -13,6 +15,31 @@ func BenchmarkRegistry(b *testing.B) {
}
}
+func BenchmarkHugeRegistry(b *testing.B) {
+ r := NewRegistry()
+ for i := 0; i < 10000; i++ {
+ r.Register(fmt.Sprintf("foo%07d", i), NewCounter())
+ }
+ v := make([]string, 10000)
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ v := v[:0]
+ r.Each(func(k string, _ interface{}) {
+ v = append(v, k)
+ })
+ }
+}
+
+func BenchmarkRegistryParallel(b *testing.B) {
+ r := NewRegistry()
+ b.ResetTimer()
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ r.GetOrRegister("foo", NewCounter())
+ }
+ })
+}
+
func TestRegistry(t *testing.T) {
r := NewRegistry()
r.Register("foo", NewCounter())
@@ -301,5 +328,66 @@ func TestWalkRegistries(t *testing.T) {
if "prefix.prefix2." != prefix {
t.Fatal(prefix)
}
+}
+
+func TestConcurrentRegistryAccess(t *testing.T) {
+ r := NewRegistry()
+
+ counter := NewCounter()
+
+ signalChan := make(chan struct{})
+
+ var wg sync.WaitGroup
+ for i := 0; i < 10; i++ {
+ wg.Add(1)
+ go func(dowork chan struct{}) {
+ defer wg.Done()
+ iface := r.GetOrRegister("foo", counter)
+ retCounter, ok := iface.(Counter)
+ if !ok {
+ t.Fatal("Expected a Counter type")
+ }
+ if retCounter != counter {
+ t.Fatal("Counter references don't match")
+ }
+ }(signalChan)
+ }
+
+ close(signalChan) // Closing will cause all go routines to execute at the same time
+ wg.Wait() // Wait for all go routines to do their work
+
+ // At the end of the test we should still only have a single "foo" Counter
+ i := 0
+ r.Each(func(name string, iface interface{}) {
+ i++
+ if "foo" != name {
+ t.Fatal(name)
+ }
+ if _, ok := iface.(Counter); !ok {
+ t.Fatal(iface)
+ }
+ })
+ if 1 != i {
+ t.Fatal(i)
+ }
+ r.Unregister("foo")
+ i = 0
+ r.Each(func(string, interface{}) { i++ })
+ if 0 != i {
+ t.Fatal(i)
+ }
+}
+// exercise race detector
+func TestRegisterAndRegisteredConcurrency(t *testing.T) {
+ r := NewRegistry()
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
+ go func(r Registry, wg *sync.WaitGroup) {
+ defer wg.Done()
+ r.Each(func(name string, iface interface{}) {
+ })
+ }(r, wg)
+ r.Register("foo", NewCounter())
+ wg.Wait()
}
diff --git a/runtime.go b/runtime.go
index 11c6b78..4047ab3 100644
--- a/runtime.go
+++ b/runtime.go
@@ -3,6 +3,7 @@ package metrics
import (
"runtime"
"runtime/pprof"
+ "sync"
"time"
)
@@ -49,7 +50,8 @@ var (
numGC uint32
numCgoCalls int64
- threadCreateProfile = pprof.Lookup("threadcreate")
+ threadCreateProfile = pprof.Lookup("threadcreate")
+ registerRuntimeMetricsOnce = sync.Once{}
)
// Capture new values for the Go runtime statistics exported in
@@ -146,67 +148,69 @@ func CaptureRuntimeMemStatsOnce(r Registry) {
// specifically runtime.MemStats. The runtimeMetrics are named by their
// fully-qualified Go symbols, i.e. runtime.MemStats.Alloc.
func RegisterRuntimeMemStats(r Registry) {
- runtimeMetrics.MemStats.Alloc = NewGauge()
- runtimeMetrics.MemStats.BuckHashSys = NewGauge()
- runtimeMetrics.MemStats.DebugGC = NewGauge()
- runtimeMetrics.MemStats.EnableGC = NewGauge()
- runtimeMetrics.MemStats.Frees = NewGauge()
- runtimeMetrics.MemStats.HeapAlloc = NewGauge()
- runtimeMetrics.MemStats.HeapIdle = NewGauge()
- runtimeMetrics.MemStats.HeapInuse = NewGauge()
- runtimeMetrics.MemStats.HeapObjects = NewGauge()
- runtimeMetrics.MemStats.HeapReleased = NewGauge()
- runtimeMetrics.MemStats.HeapSys = NewGauge()
- runtimeMetrics.MemStats.LastGC = NewGauge()
- runtimeMetrics.MemStats.Lookups = NewGauge()
- runtimeMetrics.MemStats.Mallocs = NewGauge()
- runtimeMetrics.MemStats.MCacheInuse = NewGauge()
- runtimeMetrics.MemStats.MCacheSys = NewGauge()
- runtimeMetrics.MemStats.MSpanInuse = NewGauge()
- runtimeMetrics.MemStats.MSpanSys = NewGauge()
- runtimeMetrics.MemStats.NextGC = NewGauge()
- runtimeMetrics.MemStats.NumGC = NewGauge()
- runtimeMetrics.MemStats.GCCPUFraction = NewGaugeFloat64()
- runtimeMetrics.MemStats.PauseNs = NewHistogram(NewExpDecaySample(1028, 0.015))
- runtimeMetrics.MemStats.PauseTotalNs = NewGauge()
- runtimeMetrics.MemStats.StackInuse = NewGauge()
- runtimeMetrics.MemStats.StackSys = NewGauge()
- runtimeMetrics.MemStats.Sys = NewGauge()
- runtimeMetrics.MemStats.TotalAlloc = NewGauge()
- runtimeMetrics.NumCgoCall = NewGauge()
- runtimeMetrics.NumGoroutine = NewGauge()
- runtimeMetrics.NumThread = NewGauge()
- runtimeMetrics.ReadMemStats = NewTimer()
+ registerRuntimeMetricsOnce.Do(func() {
+ runtimeMetrics.MemStats.Alloc = NewGauge()
+ runtimeMetrics.MemStats.BuckHashSys = NewGauge()
+ runtimeMetrics.MemStats.DebugGC = NewGauge()
+ runtimeMetrics.MemStats.EnableGC = NewGauge()
+ runtimeMetrics.MemStats.Frees = NewGauge()
+ runtimeMetrics.MemStats.HeapAlloc = NewGauge()
+ runtimeMetrics.MemStats.HeapIdle = NewGauge()
+ runtimeMetrics.MemStats.HeapInuse = NewGauge()
+ runtimeMetrics.MemStats.HeapObjects = NewGauge()
+ runtimeMetrics.MemStats.HeapReleased = NewGauge()
+ runtimeMetrics.MemStats.HeapSys = NewGauge()
+ runtimeMetrics.MemStats.LastGC = NewGauge()
+ runtimeMetrics.MemStats.Lookups = NewGauge()
+ runtimeMetrics.MemStats.Mallocs = NewGauge()
+ runtimeMetrics.MemStats.MCacheInuse = NewGauge()
+ runtimeMetrics.MemStats.MCacheSys = NewGauge()
+ runtimeMetrics.MemStats.MSpanInuse = NewGauge()
+ runtimeMetrics.MemStats.MSpanSys = NewGauge()
+ runtimeMetrics.MemStats.NextGC = NewGauge()
+ runtimeMetrics.MemStats.NumGC = NewGauge()
+ runtimeMetrics.MemStats.GCCPUFraction = NewGaugeFloat64()
+ runtimeMetrics.MemStats.PauseNs = NewHistogram(NewExpDecaySample(1028, 0.015))
+ runtimeMetrics.MemStats.PauseTotalNs = NewGauge()
+ runtimeMetrics.MemStats.StackInuse = NewGauge()
+ runtimeMetrics.MemStats.StackSys = NewGauge()
+ runtimeMetrics.MemStats.Sys = NewGauge()
+ runtimeMetrics.MemStats.TotalAlloc = NewGauge()
+ runtimeMetrics.NumCgoCall = NewGauge()
+ runtimeMetrics.NumGoroutine = NewGauge()
+ runtimeMetrics.NumThread = NewGauge()
+ runtimeMetrics.ReadMemStats = NewTimer()
- r.Register("runtime.MemStats.Alloc", runtimeMetrics.MemStats.Alloc)
- r.Register("runtime.MemStats.BuckHashSys", runtimeMetrics.MemStats.BuckHashSys)
- r.Register("runtime.MemStats.DebugGC", runtimeMetrics.MemStats.DebugGC)
- r.Register("runtime.MemStats.EnableGC", runtimeMetrics.MemStats.EnableGC)
- r.Register("runtime.MemStats.Frees", runtimeMetrics.MemStats.Frees)
- r.Register("runtime.MemStats.HeapAlloc", runtimeMetrics.MemStats.HeapAlloc)
- r.Register("runtime.MemStats.HeapIdle", runtimeMetrics.MemStats.HeapIdle)
- r.Register("runtime.MemStats.HeapInuse", runtimeMetrics.MemStats.HeapInuse)
- r.Register("runtime.MemStats.HeapObjects", runtimeMetrics.MemStats.HeapObjects)
- r.Register("runtime.MemStats.HeapReleased", runtimeMetrics.MemStats.HeapReleased)
- r.Register("runtime.MemStats.HeapSys", runtimeMetrics.MemStats.HeapSys)
- r.Register("runtime.MemStats.LastGC", runtimeMetrics.MemStats.LastGC)
- r.Register("runtime.MemStats.Lookups", runtimeMetrics.MemStats.Lookups)
- r.Register("runtime.MemStats.Mallocs", runtimeMetrics.MemStats.Mallocs)
- r.Register("runtime.MemStats.MCacheInuse", runtimeMetrics.MemStats.MCacheInuse)
- r.Register("runtime.MemStats.MCacheSys", runtimeMetrics.MemStats.MCacheSys)
- r.Register("runtime.MemStats.MSpanInuse", runtimeMetrics.MemStats.MSpanInuse)
- r.Register("runtime.MemStats.MSpanSys", runtimeMetrics.MemStats.MSpanSys)
- r.Register("runtime.MemStats.NextGC", runtimeMetrics.MemStats.NextGC)
- r.Register("runtime.MemStats.NumGC", runtimeMetrics.MemStats.NumGC)
- r.Register("runtime.MemStats.GCCPUFraction", runtimeMetrics.MemStats.GCCPUFraction)
- r.Register("runtime.MemStats.PauseNs", runtimeMetrics.MemStats.PauseNs)
- r.Register("runtime.MemStats.PauseTotalNs", runtimeMetrics.MemStats.PauseTotalNs)
- r.Register("runtime.MemStats.StackInuse", runtimeMetrics.MemStats.StackInuse)
- r.Register("runtime.MemStats.StackSys", runtimeMetrics.MemStats.StackSys)
- r.Register("runtime.MemStats.Sys", runtimeMetrics.MemStats.Sys)
- r.Register("runtime.MemStats.TotalAlloc", runtimeMetrics.MemStats.TotalAlloc)
- r.Register("runtime.NumCgoCall", runtimeMetrics.NumCgoCall)
- r.Register("runtime.NumGoroutine", runtimeMetrics.NumGoroutine)
- r.Register("runtime.NumThread", runtimeMetrics.NumThread)
- r.Register("runtime.ReadMemStats", runtimeMetrics.ReadMemStats)
+ r.Register("runtime.MemStats.Alloc", runtimeMetrics.MemStats.Alloc)
+ r.Register("runtime.MemStats.BuckHashSys", runtimeMetrics.MemStats.BuckHashSys)
+ r.Register("runtime.MemStats.DebugGC", runtimeMetrics.MemStats.DebugGC)
+ r.Register("runtime.MemStats.EnableGC", runtimeMetrics.MemStats.EnableGC)
+ r.Register("runtime.MemStats.Frees", runtimeMetrics.MemStats.Frees)
+ r.Register("runtime.MemStats.HeapAlloc", runtimeMetrics.MemStats.HeapAlloc)
+ r.Register("runtime.MemStats.HeapIdle", runtimeMetrics.MemStats.HeapIdle)
+ r.Register("runtime.MemStats.HeapInuse", runtimeMetrics.MemStats.HeapInuse)
+ r.Register("runtime.MemStats.HeapObjects", runtimeMetrics.MemStats.HeapObjects)
+ r.Register("runtime.MemStats.HeapReleased", runtimeMetrics.MemStats.HeapReleased)
+ r.Register("runtime.MemStats.HeapSys", runtimeMetrics.MemStats.HeapSys)
+ r.Register("runtime.MemStats.LastGC", runtimeMetrics.MemStats.LastGC)
+ r.Register("runtime.MemStats.Lookups", runtimeMetrics.MemStats.Lookups)
+ r.Register("runtime.MemStats.Mallocs", runtimeMetrics.MemStats.Mallocs)
+ r.Register("runtime.MemStats.MCacheInuse", runtimeMetrics.MemStats.MCacheInuse)
+ r.Register("runtime.MemStats.MCacheSys", runtimeMetrics.MemStats.MCacheSys)
+ r.Register("runtime.MemStats.MSpanInuse", runtimeMetrics.MemStats.MSpanInuse)
+ r.Register("runtime.MemStats.MSpanSys", runtimeMetrics.MemStats.MSpanSys)
+ r.Register("runtime.MemStats.NextGC", runtimeMetrics.MemStats.NextGC)
+ r.Register("runtime.MemStats.NumGC", runtimeMetrics.MemStats.NumGC)
+ r.Register("runtime.MemStats.GCCPUFraction", runtimeMetrics.MemStats.GCCPUFraction)
+ r.Register("runtime.MemStats.PauseNs", runtimeMetrics.MemStats.PauseNs)
+ r.Register("runtime.MemStats.PauseTotalNs", runtimeMetrics.MemStats.PauseTotalNs)
+ r.Register("runtime.MemStats.StackInuse", runtimeMetrics.MemStats.StackInuse)
+ r.Register("runtime.MemStats.StackSys", runtimeMetrics.MemStats.StackSys)
+ r.Register("runtime.MemStats.Sys", runtimeMetrics.MemStats.Sys)
+ r.Register("runtime.MemStats.TotalAlloc", runtimeMetrics.MemStats.TotalAlloc)
+ r.Register("runtime.NumCgoCall", runtimeMetrics.NumCgoCall)
+ r.Register("runtime.NumGoroutine", runtimeMetrics.NumGoroutine)
+ r.Register("runtime.NumThread", runtimeMetrics.NumThread)
+ r.Register("runtime.ReadMemStats", runtimeMetrics.ReadMemStats)
+ })
}
diff --git a/runtime_test.go b/runtime_test.go
index ebbfd50..46a09e5 100644
--- a/runtime_test.go
+++ b/runtime_test.go
@@ -6,6 +6,29 @@ import (
"time"
)
+func TestRuntimeMemStatsDoubleRegister(t *testing.T) {
+ r := NewRegistry()
+ RegisterRuntimeMemStats(r)
+ storedGauge := r.Get("runtime.MemStats.LastGC").(Gauge)
+
+ runtime.GC()
+ CaptureRuntimeMemStatsOnce(r)
+
+ firstGC := storedGauge.Value()
+ if 0 == firstGC {
+ t.Errorf("firstGC got %d, expected timestamp > 0", firstGC)
+ }
+
+ time.Sleep(time.Millisecond)
+
+ RegisterRuntimeMemStats(r)
+ runtime.GC()
+ CaptureRuntimeMemStatsOnce(r)
+ if lastGC := storedGauge.Value(); firstGC == lastGC {
+ t.Errorf("lastGC got %d, expected a higher timestamp value", lastGC)
+ }
+}
+
func BenchmarkRuntimeMemStats(b *testing.B) {
r := NewRegistry()
RegisterRuntimeMemStats(r)
Debdiff
File lists identical (after any substitutions)
No differences were encountered in the control files