Add SlidingTimeWindowArray

The current go-ethereum histogram metrics object is set to reset whenever
the metrics endpoint is scraped.  The reset is done to prevent the problem of
histograms not getting updated for commands that are rarely called.  However,
this breaks many Prometheus assumptions and makes computing correct metrics
difficult and unreliable.  The go-ethereum metrics appears to be a fork of
rcrowley/go-metrics which in turn is a port of the Java library
https://github.com/dropwizard/metrics.  Looking at
https://metrics.dropwizard.io/4.2.0/manual/core.html#exponentially-decaying-reservoirs
there is an implementation of `SlidingTimeWindowArrayReservoir`:
> SlidingTimeWindowArrayReservoir is comparable with
> ExponentiallyDecayingReservoir in terms GC overhead and performance. As for
> required memory, SlidingTimeWindowArrayReservoir takes ~128 bits per stored
> measurement and you can simply calculate required amount of heap.
> Example: 10K measurements / sec with reservoir storing time of 1 minute will
> take 10000 * 60 * 128 / 8 = 9600000 bytes ~ 9 megabytes

Here is more information on the sampling error introduced by Exponential Decay
sampling:
https://medium.com/expedia-group-tech/your-latency-metrics-could-be-misleading-you-how-hdrhistogram-can-help-9d545b598374
This commit is contained in:
Joshua Colvin 2022-12-11 19:29:16 -07:00
parent 1d98eeb9d7
commit 852baee5d0
6 changed files with 533 additions and 5 deletions

3
go.mod
View file

@ -25,6 +25,7 @@ require (
github.com/fjl/gencodec v0.0.0-20230517082657-f9840df7b83e
github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5
github.com/fsnotify/fsnotify v1.6.0
github.com/gammazero/deque v0.2.1
github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff
github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46
github.com/gofrs/flock v0.8.1
@ -98,7 +99,7 @@ require (
github.com/consensys/bavard v0.1.13 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/deepmap/oapi-codegen v1.6.0 // indirect
github.com/deepmap/oapi-codegen v1.8.2 // indirect
github.com/dlclark/regexp2 v1.7.0 // indirect
github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect

9
go.sum
View file

@ -160,8 +160,9 @@ github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/deepmap/oapi-codegen v1.6.0 h1:w/d1ntwh91XI0b/8ja7+u5SvA4IFfM0UNNLmiDR1gg0=
github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M=
github.com/deepmap/oapi-codegen v1.8.2 h1:SegyeYGcdi0jLLrpbCMoJxnUUn8GBXHsvr4rbzjuhfU=
github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw=
github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
@ -196,6 +197,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/gammazero/deque v0.2.1 h1:qSdsbG6pgp6nL7A0+K/B7s12mcCY/5l5SIUpMOl+dC0=
github.com/gammazero/deque v0.2.1/go.mod h1:LFroj8x4cMYCukHJDbxFCkT+r9AndaJnFMuZDV34tuU=
github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61 h1:IZqZOB2fydHte3kUgxrzK5E1fW7RQGeDwE8F/ZZnUYc=
github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61/go.mod h1:Q0X6pkwTILDlzrGEckF6HKjXe48EgsY/l7K7vhY4MW8=
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
@ -204,6 +207,7 @@ github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x
github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46 h1:BAIP2GihuqhwdILrV+7GJel5lyPV3u1+PgzrWLc0TkE=
github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46/go.mod h1:QNpY22eby74jVhqH4WhDLDwxc/vqsern6pW+u2kbkpc=
github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
@ -221,7 +225,6 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY=
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
@ -777,8 +780,6 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=

View file

@ -0,0 +1,221 @@
package metrics
// Ported from
// https://github.com/dropwizard/metrics/blob/release/4.2.x/metrics-core/src/main/java/com/codahale/metrics/ChunkedAssociativeLongArray.java
import (
"github.com/gammazero/deque"
"sort"
"strconv"
"strings"
)
const (
ChunkedAssociativeArrayDefaultChunkSize = 512
ChunkedAssociativeArrayMaxCacheSize = 128
)
type ChunkedAssociativeArray struct {
defaultChunkSize int
/*
* We use this ArrayDeque as cache to store chunks that are expired and removed from main data structure.
* Then instead of allocating new AssociativeArrayChunk immediately we are trying to poll one from this deque.
* So if you have constant or slowly changing load ChunkedAssociativeLongArray will never
* throw away old chunks or allocate new ones which makes this data structure almost garbage free.
*/
chunksCache *deque.Deque[*AssociativeArrayChunk]
chunks *deque.Deque[*AssociativeArrayChunk]
}
func NewChunkedAssociativeArray(chunkSize int) *ChunkedAssociativeArray {
return &ChunkedAssociativeArray{
defaultChunkSize: chunkSize,
chunksCache: deque.New[*AssociativeArrayChunk](ChunkedAssociativeArrayMaxCacheSize, ChunkedAssociativeArrayMaxCacheSize),
chunks: deque.New[*AssociativeArrayChunk](),
}
}
func (caa *ChunkedAssociativeArray) Clear() {
for i := 0; i < caa.chunks.Len(); i++ {
chunk := caa.chunks.PopBack()
caa.freeChunk(chunk)
}
}
func (caa *ChunkedAssociativeArray) AllocateChunk() *AssociativeArrayChunk {
if caa.chunksCache.Len() == 0 {
return NewAssociativeArrayChunk(caa.defaultChunkSize)
}
chunk := caa.chunksCache.PopBack()
chunk.cursor = 0
chunk.startIndex = 0
chunk.chunkSize = len(chunk.keys)
return chunk
}
func (caa *ChunkedAssociativeArray) freeChunk(chunk *AssociativeArrayChunk) {
if caa.chunksCache.Len() < ChunkedAssociativeArrayMaxCacheSize {
caa.chunksCache.PushBack(chunk)
}
}
func (caa *ChunkedAssociativeArray) Put(key int64, value int64) {
var activeChunk *AssociativeArrayChunk
if caa.chunks.Len() > 0 {
activeChunk = caa.chunks.Back()
}
if activeChunk != nil && activeChunk.cursor != 0 && activeChunk.keys[activeChunk.cursor-1] > key {
// Key must be the same as last inserted or bigger
key = activeChunk.keys[activeChunk.cursor-1] + 1
}
if activeChunk == nil || activeChunk.cursor-activeChunk.startIndex == activeChunk.chunkSize {
// The last chunk doesn't exist or full
activeChunk = caa.AllocateChunk()
caa.chunks.PushBack(activeChunk)
}
activeChunk.Append(key, value)
}
func (caa *ChunkedAssociativeArray) Values() []int64 {
valuesSize := caa.Size()
if valuesSize == 0 {
// Empty
return []int64{0}
}
values := make([]int64, 0, valuesSize)
caa.chunks.Index(func(chunk *AssociativeArrayChunk) bool {
values = append(values, chunk.values[chunk.startIndex:chunk.cursor]...)
return false
})
return values
}
func (caa *ChunkedAssociativeArray) Size() int {
var result int
caa.chunks.Index(func(chunk *AssociativeArrayChunk) bool {
result += chunk.cursor - chunk.startIndex
return false
})
return result
}
func (caa *ChunkedAssociativeArray) String() string {
var builder strings.Builder
first := true
caa.chunks.Index(func(chunk *AssociativeArrayChunk) bool {
if first {
first = false
} else {
builder.WriteString("->")
}
builder.WriteString("[")
for i := chunk.startIndex; i < chunk.cursor; i++ {
builder.WriteString("(")
builder.WriteString(strconv.FormatInt(chunk.keys[i], 10))
builder.WriteString(": ")
builder.WriteString(strconv.FormatInt(chunk.values[i], 10))
builder.WriteString(") ")
}
builder.WriteString("]")
return false
})
return builder.String()
}
// Trim tries to trim all beyond specified boundaries
// startKey: the start value for which all elements less than it should be removed.
// endKey: the end value for which all elements greater/equals than it should be removed
func (caa *ChunkedAssociativeArray) Trim(startKey int64, endKey int64) {
/*
* [3, 4, 5, 9] -> [10, 13, 14, 15] -> [21, 24, 29, 30] -> [31] :: start layout
* |5______________________________23| :: trim(5, 23)
* [5, 9] -> [10, 13, 14, 15] -> [21] :: result layout
*/
// Remove elements that are too large
indexBeforeEndKey := caa.chunks.RIndex(func(chunk *AssociativeArrayChunk) bool {
if chunk.IsFirstElementEmptyOrGreaterEqualThanKey(endKey) {
return false
}
chunk.cursor = chunk.FindFirstIndexOfGreaterEqualElements(endKey)
return true
})
// Remove chunks that only contain elements that are too large
if indexBeforeEndKey >= 0 {
for i := caa.chunks.Len() - 1; i > indexBeforeEndKey; i-- {
chunk := caa.chunks.PopBack()
caa.freeChunk(chunk)
}
}
// Remove elements that are too small
indexAfterStartKey := caa.chunks.Index(func(chunk *AssociativeArrayChunk) bool {
if chunk.IsLastElementEmptyOrLessThanKey(startKey) {
return false
}
newStartIndex := chunk.FindFirstIndexOfGreaterEqualElements(startKey)
if chunk.startIndex != newStartIndex {
chunk.startIndex = newStartIndex
chunk.chunkSize = chunk.cursor - chunk.startIndex
}
return true
})
// Remove chunks that only contain elements that are too small
for i := 0; i < indexAfterStartKey; i++ {
chunk := caa.chunks.PopFront()
caa.freeChunk(chunk)
}
}
type AssociativeArrayChunk struct {
keys []int64
values []int64
chunkSize int
startIndex int
cursor int
}
func NewAssociativeArrayChunk(chunkSize int) *AssociativeArrayChunk {
return &AssociativeArrayChunk{
keys: make([]int64, chunkSize),
values: make([]int64, chunkSize),
chunkSize: chunkSize,
startIndex: 0,
cursor: 0,
}
}
func (c *AssociativeArrayChunk) Append(key int64, value int64) {
c.keys[c.cursor] = key
c.values[c.cursor] = value
c.cursor++
}
func (c *AssociativeArrayChunk) IsFirstElementEmptyOrGreaterEqualThanKey(key int64) bool {
return c.cursor == c.startIndex || c.keys[c.startIndex] >= key
}
func (c *AssociativeArrayChunk) IsLastElementEmptyOrLessThanKey(key int64) bool {
return c.cursor == c.startIndex || c.keys[c.cursor-1] < key
}
func (c *AssociativeArrayChunk) FindFirstIndexOfGreaterEqualElements(minKey int64) int {
if c.cursor == c.startIndex || c.keys[c.startIndex] >= minKey {
return c.startIndex
}
elements := c.keys[c.startIndex:c.cursor]
keyIndex := sort.Search(len(elements), func(i int) bool { return elements[i] >= minKey })
return c.startIndex + keyIndex
}

View file

@ -0,0 +1,170 @@
package metrics
import (
"testing"
)
// Ported from
// https://github.com/dropwizard/metrics/blob/release/4.2.x/metrics-core/src/main/java/com/codahale/metrics/ChunkedAssociativeLongArray.java
func TestChunkedAssociativeArray_Put(t *testing.T) {
array := NewChunkedAssociativeArray(3)
// Test that time cannot go backwards
array.Put(7, 7)
expectedStringBefore := "[(7: 7) ]"
if array.String() != expectedStringBefore {
t.Errorf("initial array string incorrect: %s", array.String())
}
}
func TestChunkedAssociativeArray_ValuesEmpty(t *testing.T) {
array := NewChunkedAssociativeArray(3)
values := array.Values()
if len(values) != 1 {
t.Fatalf("unexpected length of values: %d", len(values))
}
if values[0] != 0 {
t.Errorf("unexpected value in empty values: %d", values[0])
}
}
func TestChunkedAssociativeArray_Trim(t *testing.T) {
array := NewChunkedAssociativeArray(3)
array.Put(-7, 7)
array.Put(-5, 7)
array.Put(-4, 7)
array.Put(-3, 3)
array.Put(-2, 1)
array.Put(0, 5)
array.Put(3, 0)
array.Put(9, 8)
array.Put(15, 0)
array.Put(19, 5)
array.Put(21, 5)
array.Put(34, -9)
array.Put(109, 5)
expectedStringBefore := "[(-7: 7) (-5: 7) (-4: 7) ]->[(-3: 3) (-2: 1) (0: 5) ]->[(3: 0) (9: 8) (15: 0) ]->[(19: 5) (21: 5) (34: -9) ]->[(109: 5) ]"
if array.String() != expectedStringBefore {
t.Errorf("initial array string incorrect: %s", array.String())
}
valuesBefore := array.Values()
expectedValuesBefore := []int64{7, 7, 7, 3, 1, 5, 0, 8, 0, 5, 5, -9, 5}
if len(valuesBefore) != len(expectedValuesBefore) {
t.Errorf("initial values returned incorrect length: %d", len(valuesBefore))
} else {
for i, value := range valuesBefore {
if value != expectedValuesBefore[i] {
t.Errorf("unexpected value %d at index %d", value, i)
}
}
}
if array.Size() != 13 {
t.Errorf("initial array size incorrect: %d", array.Size())
}
array.Trim(-2, 20)
expectedStringAfter := "[(-2: 1) (0: 5) ]->[(3: 0) (9: 8) (15: 0) ]->[(19: 5) ]"
if array.String() != expectedStringAfter {
t.Errorf("array string incorrect: %s", array.String())
}
valuesAfter := array.Values()
expectedValuesAfter := []int64{1, 5, 0, 8, 0, 5}
if len(valuesAfter) != len(expectedValuesAfter) {
t.Errorf("values returned incorrect length: %d", len(valuesAfter))
} else {
for i, value := range valuesAfter {
if value != expectedValuesAfter[i] {
t.Errorf("unexpected value %d at index %d", value, i)
}
}
}
if array.Size() != 6 {
t.Errorf("array size incorrect: %d", array.Size())
}
array.Trim(-2, 16)
expectedStringAfter2 := "[(-2: 1) (0: 5) ]->[(3: 0) (9: 8) (15: 0) ]"
if array.String() != expectedStringAfter2 {
t.Errorf("array string incorrect: %s", array.String())
}
initialCacheCount := array.chunksCache.Len()
// Have AllocateChunk take from cache
array.Put(200, 555)
expectedStringAfter3 := "[(-2: 1) (0: 5) ]->[(3: 0) (9: 8) (15: 0) ]->[(200: 555) ]"
if array.String() != expectedStringAfter3 {
t.Errorf("array string incorrect: %s", array.String())
}
if array.chunksCache.Len() >= initialCacheCount {
t.Error("cache not used when allocating chunk")
}
}
func TestAssociativeArrayChunk_IsFirstElementEmptyOrGreaterEqualThanKey(t *testing.T) {
chunk := NewAssociativeArrayChunk(3)
if !chunk.IsFirstElementEmptyOrGreaterEqualThanKey(5) {
t.Error("empty test failed")
}
chunk.keys = []int64{41, 42, 43}
chunk.startIndex = 1
if chunk.IsFirstElementEmptyOrGreaterEqualThanKey(43) {
t.Error("element less than key test failed")
}
if !chunk.IsFirstElementEmptyOrGreaterEqualThanKey(42) {
t.Error("element greater than or equal to key test failed")
}
}
func TestAssociativeArrayChunk_IsLastElementIsLessThanKey(t *testing.T) {
chunk := NewAssociativeArrayChunk(3)
if !chunk.IsLastElementEmptyOrLessThanKey(5) {
t.Error("empty test failed")
}
chunk.keys = []int64{41, 42, 43}
chunk.cursor = 2
if !chunk.IsLastElementEmptyOrLessThanKey(43) {
t.Error("element less than key test failed")
}
if chunk.IsLastElementEmptyOrLessThanKey(42) {
t.Error("element greater than or equal to key test failed")
}
}
func TestAssociativeArrayChunk_FindFirstIndexOfGreaterEqualElements(t *testing.T) {
chunk := NewAssociativeArrayChunk(7)
if chunk.FindFirstIndexOfGreaterEqualElements(5) != 0 {
t.Error("empty test failed")
}
chunk.keys = []int64{41, 42, 43, 44, 45, 46, 48}
chunk.startIndex = 1
chunk.cursor = 6
if chunk.FindFirstIndexOfGreaterEqualElements(41) != chunk.startIndex {
t.Error("minKey less than first element test failed")
}
if chunk.FindFirstIndexOfGreaterEqualElements(42) != chunk.startIndex {
t.Error("minKey greater than or equal to first element test failed")
}
if chunk.FindFirstIndexOfGreaterEqualElements(43) != 2 {
t.Error("2nd element test failed")
}
if chunk.FindFirstIndexOfGreaterEqualElements(46) != 5 {
t.Error("last element test failed")
}
if chunk.FindFirstIndexOfGreaterEqualElements(49) != 6 {
t.Error("past last element test failed")
}
}

View file

@ -87,6 +87,18 @@ func BenchmarkUniformSample1028(b *testing.B) {
benchmarkSample(b, NewUniformSample(1028))
}
func BenchmarkSlidingWindowArraySample257(b *testing.B) {
benchmarkSample(b, NewSlidingTimeWindowArraySample(257))
}
func BenchmarkSlidingWindowArraySample514(b *testing.B) {
benchmarkSample(b, NewSlidingTimeWindowArraySample(514))
}
func BenchmarkSlidingWindowArraySample1028(b *testing.B) {
benchmarkSample(b, NewSlidingTimeWindowArraySample(1028))
}
func min(a, b int) int {
if a < b {
return a

View file

@ -0,0 +1,123 @@
package metrics
import (
"math"
"sync"
"time"
)
// SlidingTimeWindowArraySample is ported from Coda Hale's dropwizard library
// <https://github.com/dropwizard/metrics/pull/1139>
// A reservoir implementation backed by a sliding window that stores only the
// measurements made in the last given window of time
type SlidingTimeWindowArraySample struct {
startTick int64
measurements *ChunkedAssociativeArray
window int64
count int64
lastTick int64
mutex sync.Mutex
}
const (
// SlidingTimeWindowCollisionBuffer allow this many duplicate ticks
// before overwriting measurements
SlidingTimeWindowCollisionBuffer = 256
// SlidingTimeWindowTrimThreshold is number of updates between trimming data
SlidingTimeWindowTrimThreshold = 256
// SlidingTimeWindowClearBufferTicks is the number of ticks to keep past the
// requested trim
SlidingTimeWindowClearBufferTicks = int64(time.Hour/time.Nanosecond) *
SlidingTimeWindowCollisionBuffer
)
// NewSlidingTimeWindowArraySample creates new object with given window of time
func NewSlidingTimeWindowArraySample(window time.Duration) Sample {
if !Enabled {
return NilSample{}
}
return &SlidingTimeWindowArraySample{
startTick: time.Now().UnixNano(),
measurements: NewChunkedAssociativeArray(ChunkedAssociativeArrayDefaultChunkSize),
window: window.Nanoseconds() * SlidingTimeWindowCollisionBuffer,
}
}
// Clear clears all samples.
func (s *SlidingTimeWindowArraySample) Clear() {
s.mutex.Lock()
defer s.mutex.Unlock()
s.count = 0
s.measurements.Clear()
}
// trim requires s.mutex to already be acquired
func (s *SlidingTimeWindowArraySample) trim() {
now := s.getTick()
windowStart := now - s.window
windowEnd := now + SlidingTimeWindowClearBufferTicks
if windowStart < windowEnd {
s.measurements.Trim(windowStart, windowEnd)
} else {
// long overflow handling that can only happen 1 year after class loading
s.measurements.Clear()
}
}
// getTick requires s.mutex to already be acquired
func (s *SlidingTimeWindowArraySample) getTick() int64 {
oldTick := s.lastTick
tick := (time.Now().UnixNano() - s.startTick) * SlidingTimeWindowCollisionBuffer
var newTick int64
if tick-oldTick > 0 {
newTick = tick
} else {
newTick = oldTick + 1
}
s.lastTick = newTick
return newTick
}
// Snapshot returns a read-only copy of the sample.
func (s *SlidingTimeWindowArraySample) Snapshot() SampleSnapshot {
s.mutex.Lock()
defer s.mutex.Unlock()
s.trim()
var (
samples = s.measurements.Values()
values = make([]int64, len(samples))
max int64 = math.MinInt64
min int64 = math.MaxInt64
sum int64
)
for i, v := range samples {
values[i] = v
sum += v
if v > max {
max = v
}
if v < min {
min = v
}
}
return newSampleSnapshotPrecalculated(s.count, values, min, max, sum)
}
// Update samples a new value.
func (s *SlidingTimeWindowArraySample) Update(v int64) {
s.mutex.Lock()
defer s.mutex.Unlock()
var newTick int64
s.count += 1
if s.count%SlidingTimeWindowTrimThreshold == 0 {
s.trim()
}
newTick = s.getTick()
longOverflow := newTick < s.lastTick
if longOverflow {
s.measurements.Clear()
}
s.measurements.Put(newTick, v)
}