Skip to content

Commit

Permalink
Custom output formatter (#97)
Browse files Browse the repository at this point in the history
  • Loading branch information
kpacha authored and rakyll committed Mar 12, 2018
1 parent 3565079 commit 61cf992
Show file tree
Hide file tree
Showing 3 changed files with 375 additions and 214 deletions.
4 changes: 0 additions & 4 deletions hey.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,10 +180,6 @@ func main() {
bodyAll = slurp
}

if *output != "csv" && *output != "" {
usageAndExit("Invalid output type; only csv is supported.")
}

var proxyURL *gourl.URL
if *proxyAddr != "" {
var err error
Expand Down
275 changes: 65 additions & 210 deletions requester/print.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,236 +15,91 @@
package requester

import (
"bytes"
"encoding/json"
"fmt"
"io"
"sort"
"strings"
"time"
"text/template"
)

const (
barChar = "∎"
)

// We report for max 1M results.
const maxRes = 1000000

type report struct {
avgTotal float64
fastest float64
slowest float64
average float64
rps float64

avgConn float64
avgDNS float64
avgReq float64
avgRes float64
avgDelay float64
connLats []float64
dnsLats []float64
reqLats []float64
resLats []float64
delayLats []float64

results chan *result
done chan bool
total time.Duration

errorDist map[string]int
statusCodeDist map[int]int
lats []float64
sizeTotal int64
numRes int64
output string

w io.Writer
}

func newReport(w io.Writer, results chan *result, output string, n int) *report {
cap := min(n, maxRes)
return &report{
output: output,
results: results,
done: make(chan bool, 1),
statusCodeDist: make(map[int]int),
errorDist: make(map[string]int),
w: w,
connLats: make([]float64, 0, cap),
dnsLats: make([]float64, 0, cap),
reqLats: make([]float64, 0, cap),
resLats: make([]float64, 0, cap),
delayLats: make([]float64, 0, cap),
lats: make([]float64, 0, cap),
func newTemplate(output string) *template.Template {
outputTmpl := output
switch outputTmpl {
case "":
outputTmpl = defaultTmpl
case "csv":
outputTmpl = csvTmpl
}
return template.Must(template.New("tmpl").Funcs(tmplFuncMap).Parse(outputTmpl))
}

func runReporter(r *report) {
// Loop will continue until channel is closed
for res := range r.results {
r.numRes++
if res.err != nil {
r.errorDist[res.err.Error()]++
} else {
r.avgTotal += res.duration.Seconds()
r.avgConn += res.connDuration.Seconds()
r.avgDelay += res.delayDuration.Seconds()
r.avgDNS += res.dnsDuration.Seconds()
r.avgReq += res.reqDuration.Seconds()
r.avgRes += res.resDuration.Seconds()
if len(r.resLats) < maxRes {
r.lats = append(r.lats, res.duration.Seconds())
r.connLats = append(r.connLats, res.connDuration.Seconds())
r.dnsLats = append(r.dnsLats, res.dnsDuration.Seconds())
r.reqLats = append(r.reqLats, res.reqDuration.Seconds())
r.delayLats = append(r.delayLats, res.delayDuration.Seconds())
r.resLats = append(r.resLats, res.resDuration.Seconds())
}
r.statusCodeDist[res.statusCode]++
if res.contentLength > 0 {
r.sizeTotal += res.contentLength
}
}
}
// Signal reporter is done.
r.done <- true
var tmplFuncMap = template.FuncMap{
"formatNumber": formatNumber,
"histogram": histogram,
"jsonify": jsonify,
}

func (r *report) finalize(total time.Duration) {
r.total = total
r.rps = float64(r.numRes) / r.total.Seconds()
r.average = r.avgTotal / float64(len(r.lats))
r.avgConn = r.avgConn / float64(len(r.lats))
r.avgDelay = r.avgDelay / float64(len(r.lats))
r.avgDNS = r.avgDNS / float64(len(r.lats))
r.avgReq = r.avgReq / float64(len(r.lats))
r.avgRes = r.avgRes / float64(len(r.lats))
r.print()
func jsonify(v interface{}) string {
d, _ := json.Marshal(v)
return string(d)
}

func (r *report) printCSV() {
r.printf("response-time,DNS+dialup,DNS,Request-write,Response-delay,Response-read\n")
for i, val := range r.lats {
r.printf("%4.4f,%4.4f,%4.4f,%4.4f,%4.4f,%4.4f\n",
val, r.connLats[i], r.dnsLats[i], r.reqLats[i], r.delayLats[i], r.resLats[i])
}
func formatNumber(duration float64) string {
return fmt.Sprintf("%4.4f", duration)
}

func (r *report) print() {
if r.output == "csv" {
r.printCSV()
return
}

if len(r.lats) > 0 {
sort.Float64s(r.lats)
r.fastest = r.lats[0]
r.slowest = r.lats[len(r.lats)-1]
r.printf("Summary:\n")
r.printf(" Total:\t%4.4f secs\n", r.total.Seconds())
r.printf(" Slowest:\t%4.4f secs\n", r.slowest)
r.printf(" Fastest:\t%4.4f secs\n", r.fastest)
r.printf(" Average:\t%4.4f secs\n", r.average)
r.printf(" Requests/sec:\t%4.4f\n", r.rps)
if r.sizeTotal > 0 {
r.printf(" Total data:\t%d bytes\n", r.sizeTotal)
r.printf(" Size/request:\t%d bytes\n", r.sizeTotal/int64(len(r.lats)))
}
if r.numRes > maxRes {
r.printf("\nNote: Distributions are for first %d results.", len(r.lats))
func histogram(buckets []Bucket) string {
max := 0
for _, b := range buckets {
if v := b.Count; v > max {
max = v
}
r.printHistogram()
r.printLatencies()
r.printf("\nDetails (average, fastest, slowest):")
r.printSection("DNS+dialup", r.avgConn, r.connLats)
r.printSection("DNS-lookup", r.avgDNS, r.dnsLats)
r.printSection("req write", r.avgReq, r.reqLats)
r.printSection("resp wait", r.avgDelay, r.delayLats)
r.printSection("resp read", r.avgRes, r.resLats)
r.printStatusCodes()
}
if len(r.errorDist) > 0 {
r.printErrors()
}
r.printf("\n")
}

// printSection prints details for http-trace fields
func (r *report) printSection(tag string, avg float64, lats []float64) {
sort.Float64s(lats)
fastest, slowest := lats[0], lats[len(lats)-1]
r.printf("\n %s:\t", tag)
r.printf(" %4.4f secs, %4.4f secs, %4.4f secs", avg, fastest, slowest)
}

// printLatencies prints percentile latencies.
func (r *report) printLatencies() {
pctls := []int{10, 25, 50, 75, 90, 95, 99}
data := make([]float64, len(pctls))
j := 0
for i := 0; i < len(r.lats) && j < len(pctls); i++ {
current := i * 100 / len(r.lats)
if current >= pctls[j] {
data[j] = r.lats[i]
j++
}
}
r.printf("\nLatency distribution:\n")
for i := 0; i < len(pctls); i++ {
if data[i] > 0 {
r.printf(" %v%% in %4.4f secs\n", pctls[i], data[i])
}
}
}

func (r *report) printHistogram() {
bc := 10
buckets := make([]float64, bc+1)
counts := make([]int, bc+1)
bs := (r.slowest - r.fastest) / float64(bc)
for i := 0; i < bc; i++ {
buckets[i] = r.fastest + bs*float64(i)
}
buckets[bc] = r.slowest
var bi int
var max int
for i := 0; i < len(r.lats); {
if r.lats[i] <= buckets[bi] {
i++
counts[bi]++
if max < counts[bi] {
max = counts[bi]
}
} else if bi < len(buckets)-1 {
bi++
}
}
r.printf("\nResponse time histogram:\n")
res := new(bytes.Buffer)
for i := 0; i < len(buckets); i++ {
// Normalize bar lengths.
var barLen int
if max > 0 {
barLen = (counts[i]*40 + max/2) / max
barLen = (buckets[i].Count*40 + max/2) / max
}
r.printf(" %4.3f [%v]\t|%v\n", buckets[i], counts[i], strings.Repeat(barChar, barLen))
res.WriteString(fmt.Sprintf(" %4.3f [%v]\t|%v\n", buckets[i].Mark, buckets[i].Count, strings.Repeat(barChar, barLen)))
}
return res.String()
}

// printStatusCodes prints status code distribution.
func (r *report) printStatusCodes() {
r.printf("\n\nStatus code distribution:\n")
for code, num := range r.statusCodeDist {
r.printf(" [%d]\t%d responses\n", code, num)
}
}

func (r *report) printErrors() {
r.printf("\nError distribution:\n")
for err, num := range r.errorDist {
r.printf(" [%d]\t%s\n", num, err)
}
}

func (r *report) printf(s string, v ...interface{}) {
fmt.Fprintf(r.w, s, v...)
}
var (
defaultTmpl = `
Summary:
Total: {{ formatNumber .Total.Seconds }} secs
Slowest: {{ formatNumber .Slowest }} secs
Fastest: {{ formatNumber .Fastest }} secs
Average: {{ formatNumber .Average }} secs
Requests/sec: {{ formatNumber .Rps }}
{{ if gt .SizeTotal 0 }}
Total data: {{ .SizeTotal }} bytes
Size/request: {{ .SizeReq }} bytes{{ end }}
Response time histogram:
{{ histogram .Histogram }}
Latency distribution:{{ range .LatencyDistribution }}
{{ .Percentage }}%% in {{ formatNumber .Latency }} secs{{ end }}
Details (average, fastest, slowest):
DNS+dialup: {{ formatNumber .AvgConn }} secs, {{ formatNumber .Fastest }} secs, {{ formatNumber .Slowest }} secs
DNS-lookup: {{ formatNumber .AvgDNS }} secs, {{ formatNumber .DnsMax }} secs, {{ formatNumber .DnsMin }} secs
req write: {{ formatNumber .AvgReq }} secs, {{ formatNumber .ReqMax }} secs, {{ formatNumber .ReqMin }} secs
resp wait: {{ formatNumber .AvgDelay }} secs, {{ formatNumber .DelayMax }} secs, {{ formatNumber .DelayMin }} secs
resp read: {{ formatNumber .AvgRes }} secs, {{ formatNumber .ResMax }} secs, {{ formatNumber .ResMin }} secs
Status code distribution:{{ range $code, $num := .StatusCodeDist }}
[{{ $code }}] {{ $num }} responses{{ end }}
{{ if gt (len .ErrorDist) 0 }}Error distribution:{{ range $err, $num := .ErrorDist }}
[{{ $num }}] {{ $err }}{{ end }}{{ end }}
`
csvTmpl = `{{ $connLats := .ConnLats }}{{ $dnsLats := .DnsLats }}{{ $dnsLats := .DnsLats }}{{ $reqLats := .ReqLats }}{{ $delayLats := .DelayLats }}{{ $resLats := .ResLats }}
response-time,DNS+dialup,DNS,Request-write,Response-delay,Response-read{{ range $i, $v := .Lats }}
{{ formatNumber $v }},{{ formatNumber (index $connLats $i) }},{{ formatNumber (index $dnsLats $i) }},{{ formatNumber (index $reqLats $i) }},{{ formatNumber (index $delayLats $i) }},{{ formatNumber (index $resLats $i) }}{{ end }}
`
)
Loading

0 comments on commit 61cf992

Please sign in to comment.