...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package report
18
19 import (
20 "fmt"
21 "math"
22 "sort"
23 "strings"
24 "time"
25 )
26
27 const (
28 barChar = "∎"
29 )
30
31
32 type Result struct {
33 Start time.Time
34 End time.Time
35 Err error
36 Weight float64
37 }
38
39 func (res *Result) Duration() time.Duration { return res.End.Sub(res.Start) }
40
41 type report struct {
42 results chan Result
43 precision string
44
45 stats Stats
46 sps *secondPoints
47 }
48
49
50 type Stats struct {
51 AvgTotal float64
52 Fastest float64
53 Slowest float64
54 Average float64
55 Stddev float64
56 RPS float64
57 Total time.Duration
58 ErrorDist map[string]int
59 Lats []float64
60 TimeSeries TimeSeries
61 }
62
63 func (s *Stats) copy() Stats {
64 ss := *s
65 ss.ErrorDist = copyMap(ss.ErrorDist)
66 ss.Lats = copyFloats(ss.Lats)
67 return ss
68 }
69
70
71
72 type Report interface {
73 Results() chan<- Result
74
75
76 Run() <-chan string
77
78
79 Stats() <-chan Stats
80 }
81
82 func NewReport(precision string) Report { return newReport(precision) }
83
84 func newReport(precision string) *report {
85 r := &report{
86 results: make(chan Result, 16),
87 precision: precision,
88 }
89 r.stats.ErrorDist = make(map[string]int)
90 return r
91 }
92
93 func NewReportSample(precision string) Report {
94 r := NewReport(precision).(*report)
95 r.sps = newSecondPoints()
96 return r
97 }
98
99 func (r *report) Results() chan<- Result { return r.results }
100
101 func (r *report) Run() <-chan string {
102 donec := make(chan string, 1)
103 go func() {
104 defer close(donec)
105 r.processResults()
106 donec <- r.String()
107 }()
108 return donec
109 }
110
111 func (r *report) Stats() <-chan Stats {
112 donec := make(chan Stats, 1)
113 go func() {
114 defer close(donec)
115 r.processResults()
116 s := r.stats.copy()
117 if r.sps != nil {
118 s.TimeSeries = r.sps.getTimeSeries()
119 }
120 donec <- s
121 }()
122 return donec
123 }
124
125 func copyMap(m map[string]int) (c map[string]int) {
126 c = make(map[string]int, len(m))
127 for k, v := range m {
128 c[k] = v
129 }
130 return c
131 }
132
133 func copyFloats(s []float64) (c []float64) {
134 c = make([]float64, len(s))
135 copy(c, s)
136 return c
137 }
138
139 func (r *report) String() (s string) {
140 if len(r.stats.Lats) > 0 {
141 s += fmt.Sprintf("\nSummary:\n")
142 s += fmt.Sprintf(" Total:\t%s.\n", r.sec2str(r.stats.Total.Seconds()))
143 s += fmt.Sprintf(" Slowest:\t%s.\n", r.sec2str(r.stats.Slowest))
144 s += fmt.Sprintf(" Fastest:\t%s.\n", r.sec2str(r.stats.Fastest))
145 s += fmt.Sprintf(" Average:\t%s.\n", r.sec2str(r.stats.Average))
146 s += fmt.Sprintf(" Stddev:\t%s.\n", r.sec2str(r.stats.Stddev))
147 s += fmt.Sprintf(" Requests/sec:\t"+r.precision+"\n", r.stats.RPS)
148 s += r.histogram()
149 s += r.sprintLatencies()
150 if r.sps != nil {
151 s += fmt.Sprintf("%v\n", r.sps.getTimeSeries())
152 }
153 }
154 if len(r.stats.ErrorDist) > 0 {
155 s += r.errors()
156 }
157 return s
158 }
159
160 func (r *report) sec2str(sec float64) string { return fmt.Sprintf(r.precision+" secs", sec) }
161
162 type reportRate struct{ *report }
163
164 func NewReportRate(precision string) Report {
165 return &reportRate{NewReport(precision).(*report)}
166 }
167
168 func (r *reportRate) String() string {
169 return fmt.Sprintf(" Requests/sec:\t"+r.precision+"\n", r.stats.RPS)
170 }
171
172 func (r *report) processResult(res *Result) {
173 if res.Err != nil {
174 r.stats.ErrorDist[res.Err.Error()]++
175 return
176 }
177 dur := res.Duration()
178 r.stats.Lats = append(r.stats.Lats, dur.Seconds())
179 r.stats.AvgTotal += dur.Seconds()
180 if r.sps != nil {
181 r.sps.Add(res.Start, dur)
182 }
183 }
184
185 func (r *report) processResults() {
186 st := time.Now()
187 for res := range r.results {
188 r.processResult(&res)
189 }
190 r.stats.Total = time.Since(st)
191
192 r.stats.RPS = float64(len(r.stats.Lats)) / r.stats.Total.Seconds()
193 r.stats.Average = r.stats.AvgTotal / float64(len(r.stats.Lats))
194 for i := range r.stats.Lats {
195 dev := r.stats.Lats[i] - r.stats.Average
196 r.stats.Stddev += dev * dev
197 }
198 r.stats.Stddev = math.Sqrt(r.stats.Stddev / float64(len(r.stats.Lats)))
199 sort.Float64s(r.stats.Lats)
200 if len(r.stats.Lats) > 0 {
201 r.stats.Fastest = r.stats.Lats[0]
202 r.stats.Slowest = r.stats.Lats[len(r.stats.Lats)-1]
203 }
204 }
205
206 var pctls = []float64{10, 25, 50, 75, 90, 95, 99, 99.9}
207
208
209 func Percentiles(nums []float64) (pcs []float64, data []float64) {
210 return pctls, percentiles(nums)
211 }
212
213 func percentiles(nums []float64) (data []float64) {
214 data = make([]float64, len(pctls))
215 j := 0
216 n := len(nums)
217 for i := 0; i < n && j < len(pctls); i++ {
218 current := float64(i) * 100.0 / float64(n)
219 if current >= pctls[j] {
220 data[j] = nums[i]
221 j++
222 }
223 }
224 return data
225 }
226
227 func (r *report) sprintLatencies() string {
228 data := percentiles(r.stats.Lats)
229 s := fmt.Sprintf("\nLatency distribution:\n")
230 for i := 0; i < len(pctls); i++ {
231 if data[i] > 0 {
232 s += fmt.Sprintf(" %v%% in %s.\n", pctls[i], r.sec2str(data[i]))
233 }
234 }
235 return s
236 }
237
238 func (r *report) histogram() string {
239 bc := 10
240 buckets := make([]float64, bc+1)
241 counts := make([]int, bc+1)
242 bs := (r.stats.Slowest - r.stats.Fastest) / float64(bc)
243 for i := 0; i < bc; i++ {
244 buckets[i] = r.stats.Fastest + bs*float64(i)
245 }
246 buckets[bc] = r.stats.Slowest
247 var bi int
248 var max int
249 for i := 0; i < len(r.stats.Lats); {
250 if r.stats.Lats[i] <= buckets[bi] {
251 i++
252 counts[bi]++
253 if max < counts[bi] {
254 max = counts[bi]
255 }
256 } else if bi < len(buckets)-1 {
257 bi++
258 }
259 }
260 s := fmt.Sprintf("\nResponse time histogram:\n")
261 for i := 0; i < len(buckets); i++ {
262
263 var barLen int
264 if max > 0 {
265 barLen = counts[i] * 40 / max
266 }
267 s += fmt.Sprintf(" "+r.precision+" [%v]\t|%v\n", buckets[i], counts[i], strings.Repeat(barChar, barLen))
268 }
269 return s
270 }
271
272 func (r *report) errors() string {
273 s := fmt.Sprintf("\nError distribution:\n")
274 for err, num := range r.stats.ErrorDist {
275 s += fmt.Sprintf(" [%d]\t%s\n", num, err)
276 }
277 return s
278 }
279
View as plain text