/
statstimer.go
154 lines (121 loc) · 3.16 KB
/
statstimer.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
// Copyright (c) 2014 Square, Inc
package metrics
import (
"encoding/json"
"errors"
"fmt"
"math"
"sort"
"sync"
"time"
)
/* StatsTimer
A StatTimer can be used to compute statistics for a timed operation
Arguments:
timeUnit time.Duration - time unit to report statistics on
nsamples int - number of samples to keep in-memory for stats computation
Example use:
m := metrics.NewMetricContext("webapp")
s := m.NewStatsTimer("latency", time.Millisecond, 100)
func (wa *WebApp) HandleQuery(w http.ResponseWriter, r *http.Request) {
stopWatch := s.Start()
.....
s.Stop(stopWatch)
}
pctile_95th, err := s.Percentile(95)
if err == nil {
fmt.Printf("95th percentile latency: ", pctile_95th)
}
*/
type StatsTimer struct {
history []int64
idx int
mu sync.RWMutex
timeUnit time.Duration
}
const NOT_INITIALIZED = -1
// default percentiles to compute when serializing statstimer type
// to stdout/json
var PERCENTILES = []float64{50, 75, 95, 99, 99.9, 99.99, 99.999}
func NewStatsTimer(timeUnit time.Duration, nsamples int) *StatsTimer {
s := new(StatsTimer)
s.timeUnit = timeUnit
s.history = make([]int64, nsamples)
s.Reset()
return s
}
func (s *StatsTimer) Reset() {
for i := range s.history {
s.history[i] = NOT_INITIALIZED
}
}
func (s *StatsTimer) Start() *Timer {
t := NewTimer()
t.Start()
return t
}
func (s *StatsTimer) Stop(t *Timer) float64 {
delta := t.Stop()
// Store current value in history
s.mu.Lock()
defer s.mu.Unlock()
s.history[s.idx] = delta
s.idx++
if s.idx == len(s.history) {
s.idx = 0
}
return float64(delta) / float64(s.timeUnit.Nanoseconds())
}
// TODO: move stats implementation to a dedicated package
type Int64Slice []int64
func (a Int64Slice) Len() int { return len(a) }
func (a Int64Slice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a Int64Slice) Less(i, j int) bool { return a[i] < a[j] }
func (s *StatsTimer) Percentile(percentile float64) (float64, error) {
// Nearest rank implementation
// http://en.wikipedia.org/wiki/Percentile
histLen := len(s.history)
if percentile > 100 {
return math.NaN(), errors.New("Invalid argument")
}
in := make([]int64, 0, histLen)
for i := range s.history {
if s.history[i] != NOT_INITIALIZED {
in = append(in, s.history[i])
}
}
filtLen := len(in)
if filtLen < 1 {
return math.NaN(), errors.New("No values")
}
// Since slices are zero-indexed, we are naturally rounded up
nearest_rank := int((percentile / 100) * float64(filtLen))
if nearest_rank == filtLen {
nearest_rank = filtLen - 1
}
sort.Sort(Int64Slice(in))
ret := float64(in[nearest_rank]) / float64(s.timeUnit.Nanoseconds())
return ret, nil
}
// MarshalJSON returns a byte slice containing representation of
// StatsTimer
func (s *StatsTimer) MarshalJSON() ([]byte, error) {
type percentileData struct {
percentile string
value float64
}
var pctiles []percentileData
for _, p := range PERCENTILES {
percentile, err := s.Percentile(p)
stuff := fmt.Sprintf("%.6f", p)
if err == nil {
pctiles = append(pctiles, percentileData{stuff, percentile})
}
}
data := struct {
Percentiles []percentileData
}{
pctiles,
}
return json.Marshal(data)
}