1package vegeta
2
3import (
4	"encoding/json"
5	"fmt"
6	"io"
7	"sort"
8	"strings"
9	"text/tabwriter"
10	"time"
11)
12
13// A Report represents the state a Reporter uses to write out its reports.
14type Report interface {
15	// Add adds a given *Result to a Report.
16	Add(*Result)
17}
18
19// Closer wraps the optional Report Close method.
20type Closer interface {
21	// Close permantently closes a Report, running any necessary book keeping.
22	Close()
23}
24
25// A Reporter function writes out reports to the given io.Writer or returns an
26// error in case of failure.
27type Reporter func(io.Writer) error
28
29// Report is a convenience method wrapping the Reporter function type.
30func (rep Reporter) Report(w io.Writer) error { return rep(w) }
31
32// NewHistogramReporter returns a Reporter that writes out a Histogram as
33// aligned, formatted text.
34func NewHistogramReporter(h *Histogram) Reporter {
35	return func(w io.Writer) (err error) {
36		tw := tabwriter.NewWriter(w, 0, 8, 2, ' ', tabwriter.StripEscape)
37		if _, err = fmt.Fprintf(tw, "Bucket\t\t#\t%%\tHistogram\n"); err != nil {
38			return err
39		}
40
41		for i, count := range h.Counts {
42			ratio := float64(count) / float64(h.Total)
43			lo, hi := h.Buckets.Nth(i)
44			pad := strings.Repeat("#", int(ratio*75))
45			_, err = fmt.Fprintf(tw, "[%s,\t%s]\t%d\t%.2f%%\t%s\n", lo, hi, count, ratio*100, pad)
46			if err != nil {
47				return nil
48			}
49		}
50
51		return tw.Flush()
52	}
53}
54
55// NewTextReporter returns a Reporter that writes out Metrics as aligned,
56// formatted text.
57func NewTextReporter(m *Metrics) Reporter {
58	const fmtstr = "Requests\t[total, rate, throughput]\t%d, %.2f, %.2f\n" +
59		"Duration\t[total, attack, wait]\t%s, %s, %s\n" +
60		"Latencies\t[min, mean, 50, 90, 95, 99, max]\t%s, %s, %s, %s, %s, %s, %s\n" +
61		"Bytes In\t[total, mean]\t%d, %.2f\n" +
62		"Bytes Out\t[total, mean]\t%d, %.2f\n" +
63		"Success\t[ratio]\t%.2f%%\n" +
64		"Status Codes\t[code:count]\t"
65
66	return func(w io.Writer) (err error) {
67		tw := tabwriter.NewWriter(w, 0, 8, 2, ' ', tabwriter.StripEscape)
68		if _, err = fmt.Fprintf(tw, fmtstr,
69			m.Requests, m.Rate, m.Throughput,
70			round(m.Duration+m.Wait),
71			round(m.Duration),
72			round(m.Wait),
73			round(m.Latencies.Min),
74			round(m.Latencies.Mean),
75			round(m.Latencies.P50),
76			round(m.Latencies.P90),
77			round(m.Latencies.P95),
78			round(m.Latencies.P99),
79			round(m.Latencies.Max),
80			m.BytesIn.Total, m.BytesIn.Mean,
81			m.BytesOut.Total, m.BytesOut.Mean,
82			m.Success*100,
83		); err != nil {
84			return err
85		}
86
87		codes := make([]string, 0, len(m.StatusCodes))
88		for code := range m.StatusCodes {
89			codes = append(codes, code)
90		}
91
92		sort.Strings(codes)
93
94		for _, code := range codes {
95			count := m.StatusCodes[code]
96			if _, err = fmt.Fprintf(tw, "%s:%d  ", code, count); err != nil {
97				return err
98			}
99		}
100
101		if _, err = fmt.Fprintln(tw, "\nError Set:"); err != nil {
102			return err
103		}
104
105		for _, e := range m.Errors {
106			if _, err = fmt.Fprintln(tw, e); err != nil {
107				return err
108			}
109		}
110
111		return tw.Flush()
112	}
113}
114
115var durations = [...]time.Duration{
116	time.Hour,
117	time.Minute,
118	time.Second,
119	time.Millisecond,
120	time.Microsecond,
121	time.Nanosecond,
122}
123
124// round to the next most precise unit
125func round(d time.Duration) time.Duration {
126	for i, unit := range durations {
127		if d >= unit && i < len(durations)-1 {
128			return d.Round(durations[i+1])
129		}
130	}
131	return d
132}
133
134// NewJSONReporter returns a Reporter that writes out Metrics as JSON.
135func NewJSONReporter(m *Metrics) Reporter {
136	return func(w io.Writer) error {
137		return json.NewEncoder(w).Encode(m)
138	}
139}
140
141var logarithmic = []float64{
142	0.00,
143	0.100,
144	0.200,
145	0.300,
146	0.400,
147	0.500,
148	0.550,
149	0.600,
150	0.650,
151	0.700,
152	0.750,
153	0.775,
154	0.800,
155	0.825,
156	0.850,
157	0.875,
158	0.8875,
159	0.900,
160	0.9125,
161	0.925,
162	0.9375,
163	0.94375,
164	0.950,
165	0.95625,
166	0.9625,
167	0.96875,
168	0.971875,
169	0.975,
170	0.978125,
171	0.98125,
172	0.984375,
173	0.985938,
174	0.9875,
175	0.989062,
176	0.990625,
177	0.992188,
178	0.992969,
179	0.99375,
180	0.994531,
181	0.995313,
182	0.996094,
183	0.996484,
184	0.996875,
185	0.997266,
186	0.997656,
187	0.998047,
188	0.998242,
189	0.998437,
190	0.998633,
191	0.998828,
192	0.999023,
193	0.999121,
194	0.999219,
195	0.999316,
196	0.999414,
197	0.999512,
198	0.999561,
199	0.999609,
200	0.999658,
201	0.999707,
202	0.999756,
203	0.99978,
204	0.999805,
205	0.999829,
206	0.999854,
207	0.999878,
208	0.99989,
209	0.999902,
210	0.999915,
211	0.999927,
212	0.999939,
213	0.999945,
214	0.999951,
215	0.999957,
216	0.999963,
217	0.999969,
218	0.999973,
219	0.999976,
220	0.999979,
221	0.999982,
222	0.999985,
223	0.999986,
224	0.999988,
225	0.999989,
226	0.999991,
227	0.999992,
228	0.999993,
229	0.999994,
230	0.999995,
231	0.999996,
232	0.999997,
233	0.999998,
234	0.999999,
235	1.0,
236}
237
238// NewHDRHistogramPlotReporter returns a Reporter that writes out latency metrics
239// in a format plottable by http://hdrhistogram.github.io/HdrHistogram/plotFiles.html.
240func NewHDRHistogramPlotReporter(m *Metrics) Reporter {
241	return func(w io.Writer) error {
242		tw := tabwriter.NewWriter(w, 0, 8, 2, ' ', tabwriter.StripEscape)
243		_, err := fmt.Fprintf(tw, "Value(ms)\tPercentile\tTotalCount\t1/(1-Percentile)\n")
244		if err != nil {
245			return err
246		}
247
248		total := float64(m.Requests)
249		for _, q := range logarithmic {
250			value := milliseconds(m.Latencies.Quantile(q))
251			oneBy := oneByQuantile(q)
252			count := int64((q * total) + 0.5) // Count at quantile
253			_, err = fmt.Fprintf(tw, "%f\t%f\t%d\t%f\n", value, q, count, oneBy)
254			if err != nil {
255				return err
256			}
257		}
258
259		return tw.Flush()
260	}
261}
262
263// milliseconds converts the given duration to a number of
264// fractional milliseconds. Splitting the integer and fraction
265// ourselves guarantees that converting the returned float64 to an
266// integer rounds the same way that a pure integer conversion would have,
267// even in cases where, say, float64(d.Nanoseconds())/1e9 would have rounded
268// differently.
269func milliseconds(d time.Duration) float64 {
270	msec, nsec := d/time.Millisecond, d%time.Millisecond
271	return float64(msec) + float64(nsec)/1e6
272}
273
274func oneByQuantile(q float64) float64 {
275	if q < 1.0 {
276		return 1 / (1 - q)
277	}
278	return float64(10000000)
279}
280