forked from jamiealquiza/tachymeter
-
Notifications
You must be signed in to change notification settings - Fork 0
/
tachymeter.go
330 lines (302 loc) · 7.9 KB
/
tachymeter.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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
// Package tachymeter yields summarized data
// describing a series of timed events.
package tachymeter
import (
"bytes"
"encoding/json"
"fmt"
"math"
"strings"
"sync"
"time"
)
// Order represents the order in which time durations should be sorted before
// computing percentiles and other values.
type Order bool
const (
// Ascending sorts time durations in ascending order.
Ascending Order = false
// Descending sort time durations in descending order.
Descending Order = true
)
// Config holds tachymeter initialization
// parameters. Size defines the sample capacity.
// Tachymeter is thread safe.
type Config struct {
Size int
Safe bool // Deprecated. Flag held on to as to not break existing users.
HBins int // Histogram bins.
Name string // Name is the tachymeter of the metric represented by a tachymeter.
Order Order // Order is the sorting order (default is Ascending).
}
// Tachymeter holds event durations
// and counts.
type Tachymeter struct {
sync.RWMutex
Size uint64
Times timeSlice
Count uint64
WallTime time.Duration
HBins int
Name string
order Order
}
// timeslice holds time.Duration values.
type timeSlice []time.Duration
// Satisfy sort for timeSlice.
func (p timeSlice) Len() int { return len(p) }
func (p timeSlice) Less(i, j int) bool { return int64(p[i]) < int64(p[j]) }
func (p timeSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
// Histogram is a map["low-high duration"]count of events that
// fall within the low-high time duration range.
type Histogram []map[string]uint64
// Metrics holds the calculated outputs
// produced from a Tachymeter sample set.
type Metrics struct {
Name string // Name is the metric name.
Time struct { // All values under Time are selected entirely from events within the sample window.
Cumulative time.Duration // Cumulative time of all sampled events.
HMean time.Duration // Event duration harmonic mean.
Avg time.Duration // Event duration average.
P50 time.Duration // Event duration nth percentiles ..
P75 time.Duration
P95 time.Duration
P99 time.Duration
P999 time.Duration
Long5p time.Duration // Average of the longest 5% event durations.
Short5p time.Duration // Average of the shortest 5% event durations.
Max time.Duration // Highest event duration.
Min time.Duration // Lowest event duration.
StdDev time.Duration // Standard deviation.
Range time.Duration // Event duration range (Max-Min).
}
Rate struct {
// Per-second rate based on event duration avg. via Metrics.Cumulative / Metrics.Samples.
// If SetWallTime was called, event duration avg = wall time / Metrics.Count
Second float64
}
Histogram *Histogram // Frequency distribution of event durations in len(Histogram) bins of HistogramBinSize.
HistogramBinSize time.Duration // The width of a histogram bin in time.
Samples int // Number of events included in the sample set.
Count int // Total number of events observed.
}
// New initializes a new Tachymeter.
func New(c *Config) *Tachymeter {
var hSize int
if c.HBins != 0 {
hSize = c.HBins
} else {
hSize = 10
}
return &Tachymeter{
Size: uint64(c.Size),
Times: make([]time.Duration, c.Size),
HBins: hSize,
Name: c.Name,
order: c.Order,
}
}
// Reset resets a Tachymeter instance for reuse.
// This function is safe for concurrent use by multiple goroutines.
func (m *Tachymeter) Reset() {
// This lock is needed for:
// - m.Count update in AddTime
// - Calc
m.Lock()
m.Count = 0
m.Unlock()
}
// AddTime adds a time.Duration to Tachymeter. This function is safe for
// concurrent use by multiple goroutines.
func (m *Tachymeter) AddTime(t time.Duration) {
m.Lock()
m.Times[m.Count%m.Size] = t
m.Count++
m.Unlock()
}
// SetWallTime optionally sets an elapsed wall time duration.
// This affects rate output by using total events counted over time.
// This is useful for concurrent/parallelized events that overlap
// in wall time and are writing to a shared Tachymeter instance.
//
// This function is NOT SAFE for concurrent use by multiple goroutines.
func (m *Tachymeter) SetWallTime(t time.Duration) {
m.WallTime = t
}
// WriteHTML writes a histograph
// html file to the cwd.
func (m *Metrics) WriteHTML(p string) error {
w := Timeline{}
w.AddEvent(m)
return w.WriteHTML(p)
}
// Dump prints a formatted Metrics output to console.
func (m *Metrics) Dump() {
fmt.Println(m.String())
}
// String returns a formatted Metrics string.
func (m *Metrics) String() string {
return fmt.Sprintf(`%d samples of %d events
Cumulative: %s
HMean: %s
Avg.: %s
p50: %s
p75: %s
p95: %s
p99: %s
p999: %s
Long 5%%: %s
Short 5%%: %s
Max: %s
Min: %s
Range: %s
StdDev: %s
Rate/sec.: %.2f`,
m.Samples,
m.Count,
m.Time.Cumulative,
m.Time.HMean,
m.Time.Avg,
m.Time.P50,
m.Time.P75,
m.Time.P95,
m.Time.P99,
m.Time.P999,
m.Time.Long5p,
m.Time.Short5p,
m.Time.Max,
m.Time.Min,
m.Time.Range,
m.Time.StdDev,
m.Rate.Second)
}
// JSON returns a *Metrics as
// a JSON string.
func (m *Metrics) JSON() string {
j, _ := json.Marshal(m)
return string(j)
}
// MarshalJSON defines the output formatting
// for the JSON() method. This is exported as a
// requirement but not intended for end users.
func (m *Metrics) MarshalJSON() ([]byte, error) {
return json.Marshal(&struct {
Name string
Time struct {
Cumulative string
HMean string
Avg string
P50 string
P75 string
P95 string
P99 string
P999 string
Long5p string
Short5p string
Max string
Min string
Range string
StdDev string
}
Rate struct {
Second float64
}
Samples int
Count int
Histogram *Histogram
}{
Name: m.Name,
Time: struct {
Cumulative string
HMean string
Avg string
P50 string
P75 string
P95 string
P99 string
P999 string
Long5p string
Short5p string
Max string
Min string
Range string
StdDev string
}{
Cumulative: m.Time.Cumulative.String(),
HMean: m.Time.HMean.String(),
Avg: m.Time.Avg.String(),
P50: m.Time.P50.String(),
P75: m.Time.P75.String(),
P95: m.Time.P95.String(),
P99: m.Time.P99.String(),
P999: m.Time.P999.String(),
Long5p: m.Time.Long5p.String(),
Short5p: m.Time.Short5p.String(),
Max: m.Time.Max.String(),
Min: m.Time.Min.String(),
Range: m.Time.Range.String(),
StdDev: m.Time.StdDev.String(),
},
Rate: struct{ Second float64 }{
Second: m.Rate.Second,
},
Histogram: m.Histogram,
Samples: m.Samples,
Count: m.Count,
})
}
// Dump prints a formatted histogram output to console
// scaled to a width of s.
func (h *Histogram) Dump(s int) {
fmt.Println(h.String(s))
}
// String returns a formatted Metrics string scaled
// to a width of s.
func (h *Histogram) String(s int) string {
if h == nil {
return ""
}
var min, max uint64 = math.MaxUint64, 0
// Get the histogram min/max counts.
for _, bin := range *h {
for _, v := range bin {
if v > max {
max = v
}
if v < min {
min = v
}
}
}
// Handle cases of no or
// a single bin.
switch len(*h) {
case 0:
return ""
case 1:
min = 0
}
var b bytes.Buffer
// Build histogram string.
for _, bin := range *h {
for k, v := range bin {
// Get the bar length.
blen := scale(float64(v), float64(min), float64(max), 1, float64(s))
line := fmt.Sprintf("%22s %s\n", k, strings.Repeat("-", int(blen)))
b.WriteString(line)
}
}
return b.String()
}
// Scale scales the input x with the input-min a0,
// input-max a1, output-min b0, and output-max b1.
func scale(x, a0, a1, b0, b1 float64) float64 {
a, b := x-a0, a1-a0
var c float64
if a == 0 {
c = 0
} else {
c = a / b
}
return c*(b1-b0) + b0
}