-
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
Open
maciuszek
wants to merge
16
commits into
master
Choose a base branch
from
mattkuzminski/add-timer-reservoir
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+485
−17
Open
add reservoir timers #171
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
72bc838
prototype a simple reservoir for timers
maciuszek f67aca4
clarify comment
maciuszek 5cc34d9
Move s.timerCount inside timer.Range
bshramin f7fc55e
fix test messages
maciuszek 4a0662a
fix spelling
maciuszek f35471d
Merge remote-tracking branch 'origin/master' into mattkuzminski/add-t…
maciuszek 57ef42b
re-design with timer reservoirs correctly independent per mertic
maciuszek 4610f55
add some more todos
maciuszek cc908b5
clean up redundant code
maciuszek b1a2def
some more clean up
maciuszek 8eb942d
address todos
maciuszek 5dd8757
fix comment
maciuszek 0d3fb45
ensure memory and flush management for timers
maciuszek ea5ae6a
optimize reservoirTimer by utilizing a ring buffer
maciuszek e81d603
correct how we flush reusable timer entries
maciuszek 74a26a1
add test for reused timer map after flushing
maciuszek File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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). |
||
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 | ||
} | ||
|
||
|
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.