forked from bojand/ghz
-
Notifications
You must be signed in to change notification settings - Fork 0
/
reporter.go
215 lines (187 loc) · 5.28 KB
/
reporter.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
package ghz
import (
"encoding/json"
"sort"
"time"
)
// Reporter gethers all the results
type Reporter struct {
options *Options
results chan *callResult
done chan bool
avgTotal float64
lats []float64
errors []string
statuses []string
errorDist map[string]int
statusCodeDist map[string]int
totalCount uint64
}
// Report holds the data for the full test
type Report struct {
Options *Options `json:"options,omitempty"`
Date time.Time `json:"date"`
Count uint64 `json:"count"`
Total time.Duration `json:"total"`
Average time.Duration `json:"average"`
Fastest time.Duration `json:"fastest"`
Slowest time.Duration `json:"slowest"`
Rps float64 `json:"rps"`
ErrorDist map[string]int `json:"errorDistribution"`
StatusCodeDist map[string]int `json:"statusCodeDistribution"`
LatencyDistribution []LatencyDistribution `json:"latencyDistribution"`
Histogram []Bucket `json:"histogram"`
Details []ResultDetail `json:"details"`
}
// MarshalJSON is custom marshal for report to properly format the date
func (r Report) MarshalJSON() ([]byte, error) {
type Alias Report
return json.Marshal(&struct {
Date string `json:"date"`
*Alias
}{
Date: r.Date.Format(time.RFC3339),
Alias: (*Alias)(&r),
})
}
// LatencyDistribution holds latency distribution data
type LatencyDistribution struct {
Percentage int `json:"percentage"`
Latency time.Duration `json:"latency"`
}
// Bucket holds histogram data
type Bucket struct {
// The Mark for histogram bucket in seconds
Mark float64 `json:"mark"`
// The count in the bucket
Count int `json:"count"`
// The frequency of results in the bucket as a decimal percentage
Frequency float64 `json:"frequency"`
}
// ResultDetail data for each result
type ResultDetail struct {
Latency time.Duration `json:"latency"`
Error string `json:"error"`
Status string `json:"status"`
}
func newReporter(results chan *callResult, options *Options) *Reporter {
cap := min(options.N, maxResult)
return &Reporter{
options: options,
results: results,
done: make(chan bool, 1),
statusCodeDist: make(map[string]int),
errorDist: make(map[string]int),
lats: make([]float64, 0, cap),
}
}
// Run runs the reporter
func (r *Reporter) Run() {
for res := range r.results {
r.totalCount++
if res.err != nil {
errStr := res.err.Error()
r.errorDist[errStr]++
if len(r.errors) < maxResult {
r.errors = append(r.errors, errStr)
r.statuses = append(r.statuses, res.status)
}
} else {
r.avgTotal += res.duration.Seconds()
r.statusCodeDist[res.status]++
if len(r.lats) < maxResult {
r.lats = append(r.lats, res.duration.Seconds())
r.errors = append(r.errors, "")
r.statuses = append(r.statuses, res.status)
}
}
}
r.done <- true
}
// Finalize all the gathered data into a final report
func (r *Reporter) Finalize(total time.Duration) *Report {
average := r.avgTotal / float64(r.totalCount)
avgDuration := time.Duration(average * float64(time.Second))
rps := float64(r.totalCount) / total.Seconds()
rep := &Report{
Options: r.options,
Date: time.Now(),
Count: r.totalCount,
Total: total,
Average: avgDuration,
Rps: rps,
ErrorDist: r.errorDist,
StatusCodeDist: r.statusCodeDist}
if len(r.lats) > 0 {
lats := make([]float64, len(r.lats))
copy(lats, r.lats)
sort.Float64s(lats)
var fastestNum, slowestNum float64
fastestNum = lats[0]
slowestNum = lats[len(lats)-1]
rep.Fastest = time.Duration(fastestNum * float64(time.Second))
rep.Slowest = time.Duration(slowestNum * float64(time.Second))
rep.Histogram = histogram(&lats, slowestNum, fastestNum)
rep.LatencyDistribution = latencies(&lats)
rep.Details = make([]ResultDetail, len(r.lats))
for i, num := range r.lats {
lat := time.Duration(num * float64(time.Second))
rep.Details[i] = ResultDetail{Latency: lat, Error: r.errors[i], Status: r.statuses[i]}
}
}
return rep
}
func latencies(latencies *[]float64) []LatencyDistribution {
lats := *latencies
pctls := []int{10, 25, 50, 75, 90, 95, 99}
data := make([]float64, len(pctls))
j := 0
for i := 0; i < len(lats) && j < len(pctls); i++ {
current := i * 100 / len(lats)
if current >= pctls[j] {
data[j] = lats[i]
j++
}
}
res := make([]LatencyDistribution, len(pctls))
for i := 0; i < len(pctls); i++ {
if data[i] > 0 {
lat := time.Duration(data[i] * float64(time.Second))
res[i] = LatencyDistribution{Percentage: pctls[i], Latency: lat}
}
}
return res
}
func histogram(latencies *[]float64, slowest, fastest float64) []Bucket {
lats := *latencies
bc := 10
buckets := make([]float64, bc+1)
counts := make([]int, bc+1)
bs := (slowest - fastest) / float64(bc)
for i := 0; i < bc; i++ {
buckets[i] = fastest + bs*float64(i)
}
buckets[bc] = slowest
var bi int
var max int
for i := 0; i < len(lats); {
if lats[i] <= buckets[bi] {
i++
counts[bi]++
if max < counts[bi] {
max = counts[bi]
}
} else if bi < len(buckets)-1 {
bi++
}
}
res := make([]Bucket, len(buckets))
for i := 0; i < len(buckets); i++ {
res[i] = Bucket{
Mark: buckets[i],
Count: counts[i],
Frequency: float64(counts[i]) / float64(len(lats)),
}
}
return res
}