-
Notifications
You must be signed in to change notification settings - Fork 18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add reservoir timers #171
base: master
Are you sure you want to change the base?
add reservoir timers #171
Changes from 16 commits
72bc838
f67aca4
5cc34d9
f7fc55e
4a0662a
f35471d
57ef42b
4610f55
cc908b5
b1a2def
8eb942d
5dd8757
0d3fb45
ea5ae6a
e81d603
74a26a1
6d2687c
d067744
7e5a451
a54db1a
18c0e57
bf0ef63
8dad5ed
9762152
2641924
858a3fd
4e9611d
70cc61c
b352a4f
ef8cf0b
8cceead
4cae9ac
7fe2893
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ package stats | |
|
||
import ( | ||
"context" | ||
"math/bits" | ||
"strconv" | ||
"sync" | ||
"sync/atomic" | ||
|
@@ -214,7 +215,10 @@ type StatGenerator interface { | |
// NewStore returns an Empty store that flushes to Sink passed as an argument. | ||
// Note: the export argument is unused. | ||
func NewStore(sink Sink, _ bool) Store { | ||
return &statStore{sink: sink} | ||
return &statStore{ | ||
sink: sink, | ||
conf: GetSettings(), // todo: right now the environment is being loaded in multiple places and can be made more efficient by computing it once and storing for subsequent gets | ||
} | ||
} | ||
|
||
// NewDefaultStore returns a Store with a TCP statsd sink, and a running flush timer. | ||
|
@@ -298,30 +302,130 @@ func (c *gauge) Value() uint64 { | |
return atomic.LoadUint64(&c.value) | ||
} | ||
|
||
type timer struct { | ||
type timer interface { | ||
time(time.Duration) | ||
AddDuration(time.Duration) | ||
AddValue(float64) | ||
AllocateSpan() Timespan | ||
GetValue(int) float64 | ||
ValueCount() int | ||
SampleRate() float64 | ||
Reset() | ||
} | ||
|
||
type standardTimer struct { | ||
base time.Duration | ||
name string | ||
sink Sink | ||
} | ||
|
||
func (t *timer) time(dur time.Duration) { | ||
func (t *standardTimer) time(dur time.Duration) { | ||
t.AddDuration(dur) | ||
} | ||
|
||
func (t *timer) AddDuration(dur time.Duration) { | ||
func (t *standardTimer) AddDuration(dur time.Duration) { | ||
t.AddValue(float64(dur / t.base)) | ||
} | ||
|
||
func (t *timer) AddValue(value float64) { | ||
func (t *standardTimer) AddValue(value float64) { | ||
t.sink.FlushTimer(t.name, value) | ||
} | ||
|
||
func (t *timer) AllocateSpan() Timespan { | ||
func (t *standardTimer) AllocateSpan() Timespan { | ||
return ×pan{timer: t, start: time.Now()} | ||
} | ||
|
||
func (t *standardTimer) GetValue(_ int) float64 { | ||
return 0.0 // since we flush right away nothing will be collected | ||
} | ||
|
||
func (t *standardTimer) ValueCount() int { | ||
return 0 // since we flush right away nothing will be collected | ||
} | ||
|
||
func (t *standardTimer) SampleRate() float64 { | ||
return 1.0 // metrics which are not sampled have an implicit sample rate 1.0 | ||
} | ||
|
||
// nothing to persisted in memroy for this timer | ||
func (t *standardTimer) Reset() {} | ||
|
||
type reservoirTimer struct { | ||
mu sync.Mutex | ||
base time.Duration | ||
name string | ||
ringSize int | ||
ringMask int | ||
values []float64 | ||
count int | ||
overflow int | ||
} | ||
|
||
func (t *reservoirTimer) time(dur time.Duration) { | ||
t.AddDuration(dur) | ||
} | ||
|
||
func (t *reservoirTimer) AddDuration(dur time.Duration) { | ||
t.AddValue(float64(dur / t.base)) | ||
} | ||
|
||
func (t *reservoirTimer) AddValue(value float64) { | ||
t.mu.Lock() | ||
defer t.mu.Unlock() | ||
|
||
t.values[t.overflow&t.ringMask] = value | ||
t.overflow++ | ||
|
||
// todo: can i optimize this with xor? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think you need to reset the overflow since you're masking (taking only the lower bits is equivalent to a modulo operation). Also using an unsigned int here would be a bit safer / clearer (makes the value always positive and unsigned overflow is well defined). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah this implementation here is wrong |
||
if t.overflow == t.ringSize { | ||
t.overflow = 0 | ||
} | ||
|
||
t.count++ | ||
} | ||
|
||
func (t *reservoirTimer) AllocateSpan() Timespan { | ||
return ×pan{timer: t, start: time.Now()} | ||
} | ||
|
||
func (t *reservoirTimer) GetValue(index int) float64 { | ||
t.mu.Lock() | ||
defer t.mu.Unlock() | ||
|
||
return t.values[index] | ||
} | ||
|
||
func (t *reservoirTimer) ValueCount() int { | ||
t.mu.Lock() // todo: could probably convert locks like this to atomic.LoadUint64 | ||
defer t.mu.Unlock() | ||
|
||
if t.count > t.ringSize { | ||
return t.ringSize | ||
} | ||
return t.count | ||
} | ||
|
||
func (t *reservoirTimer) SampleRate() float64 { | ||
t.mu.Lock() | ||
defer t.mu.Unlock() | ||
|
||
// todo: a 0 count should probably not be a 1.0 sample rate | ||
if t.count <= t.ringSize { | ||
return 1.0 | ||
} | ||
return float64(t.ringSize) / float64(t.count) // todo: is it worth it to use t.ringSize instead of computing len of values worth it? | ||
} | ||
|
||
func (t *reservoirTimer) Reset() { | ||
t.mu.Lock() | ||
defer t.mu.Unlock() | ||
|
||
t.count = 0 // this will imply a 0.0 sample rate until it's increased | ||
t.overflow = 0 | ||
} | ||
|
||
type timespan struct { | ||
timer *timer | ||
timer timer | ||
start time.Time | ||
} | ||
|
||
|
@@ -336,6 +440,7 @@ func (ts *timespan) CompleteWithDuration(value time.Duration) { | |
} | ||
|
||
type statStore struct { | ||
// these maps may grow indefinitely however slots in this maps are reused as stats names are stable over the lifetime of the process | ||
counters sync.Map | ||
gauges sync.Map | ||
timers sync.Map | ||
|
@@ -344,6 +449,8 @@ type statStore struct { | |
statGenerators []StatGenerator | ||
|
||
sink Sink | ||
|
||
conf Settings | ||
} | ||
|
||
var ReservedTagWords = map[string]bool{"asg": true, "az": true, "backend": true, "canary": true, "host": true, "period": true, "region": true, "shard": true, "window": true, "source": true, "project": true, "facet": true, "envoyservice": true} | ||
|
@@ -393,6 +500,21 @@ func (s *statStore) Flush() { | |
return true | ||
}) | ||
|
||
s.timers.Range(func(key, v interface{}) bool { | ||
if timer, ok := v.(*reservoirTimer); ok { | ||
sampleRate := timer.SampleRate() | ||
|
||
// since the map memory is reused only process what we accumulated in the current processing itteration | ||
for i := 0; i < timer.ValueCount(); i++ { | ||
s.sink.FlushAggregatedTimer(key.(string), timer.GetValue(i), sampleRate) | ||
} | ||
|
||
timer.Reset() | ||
} | ||
|
||
return true | ||
}) | ||
|
||
flushableSink, ok := s.sink.(FlushableSink) | ||
if ok { | ||
flushableSink.Flush() | ||
|
@@ -490,14 +612,34 @@ func (s *statStore) NewPerInstanceGauge(name string, tags map[string]string) Gau | |
return s.newGaugeWithTagSet(name, tagspkg.TagSet(nil).MergePerInstanceTags(tags)) | ||
} | ||
|
||
func (s *statStore) newTimer(serializedName string, base time.Duration) *timer { | ||
func (s *statStore) newTimer(serializedName string, base time.Duration) timer { | ||
if v, ok := s.timers.Load(serializedName); ok { | ||
return v.(*timer) | ||
return v.(timer) | ||
} | ||
t := &timer{name: serializedName, sink: s.sink, base: base} | ||
|
||
var t timer | ||
if s.conf.isTimerReservoirEnabled() { | ||
capacity := s.conf.TimerReservoirSize | ||
capacityRoundedToTheNextPowerOfTwo := 1 << bits.Len(uint(capacity)) | ||
t = &reservoirTimer{ | ||
name: serializedName, | ||
base: base, | ||
ringSize: capacity, | ||
ringMask: capacityRoundedToTheNextPowerOfTwo - 1, | ||
values: make([]float64, capacity), | ||
} | ||
} else { | ||
t = &standardTimer{ | ||
name: serializedName, | ||
sink: s.sink, | ||
base: base, | ||
} | ||
} | ||
|
||
if v, loaded := s.timers.LoadOrStore(serializedName, t); loaded { | ||
return v.(*timer) | ||
return v.(timer) | ||
} | ||
|
||
return t | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because of this we'll need a new major release.
The library usage should be backwards compatible, and additionally there is a feature flag to control the new behaviour, however if anything is implementing this interface it'll break.