diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dc9734d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +.vscode/launch.json +go-wrk diff --git a/README.md b/README.md index 5783768..d208571 100644 --- a/README.md +++ b/README.md @@ -41,21 +41,30 @@ Command line parameters (./go-wrk -help) Basic Usage ----------- - ./go-wrk -c 80 -d 5 http://192.168.1.118:8080/json + ./go-wrk -c 2048 -d 10 http://localhost:8080/plaintext -This runs a benchmark for 5 seconds, using 80 go routines (connections) +This runs a benchmark for 10 seconds, using 2048 go routines (connections) Output: - Running 10s test @ http://192.168.1.118:8080/json - 80 goroutine(s) running concurrently - 142470 requests in 4.949028953s, 19.57MB read - Requests/sec: 28787.47 - Transfer/sec: 3.95MB - Avg Req Time: 0.0347ms - Fastest Request: 0.0340ms - Slowest Request: 0.0421ms - Number of Errors: 0 + Running 10s test @ http://localhost:8080/plaintext + 2048 goroutine(s) running concurrently + 439977 requests in 10.012950719s, 52.45MB read + Requests/sec: 43940.79 + Transfer/sec: 5.24MB + Fastest Request: 98µs + Avg Req Time: 46.608ms + Slowest Request: 398.431ms + Number of Errors: 0 + Error Counts: map[] + 10%: 164µs + 50%: 2.382ms + 75%: 3.83ms + 99%: 5.403ms + 99.9%: 5.488ms + 99.9999%: 5.5ms + 99.99999%: 5.5ms + stddev: 29.744ms Benchmarking Tips diff --git a/go-wrk.go b/go-wrk.go index c5130d7..d891689 100644 --- a/go-wrk.go +++ b/go-wrk.go @@ -10,6 +10,7 @@ import ( "strings" "time" + histo "github.com/HdrHistogram/hdrhistogram-go" "github.com/tsliwowicz/go-wrk/loader" "github.com/tsliwowicz/go-wrk/util" ) @@ -38,6 +39,7 @@ var clientCert string var clientKey string var caCert string var http2 bool +var cpus int = 0 func init() { flag.BoolVar(&versionFlag, "v", false, "Print version details") @@ -49,6 +51,7 @@ func init() { flag.IntVar(&goroutines, "c", 10, "Number of goroutines to use (concurrent connections)") flag.IntVar(&duration, "d", 10, "Duration of test in seconds") flag.IntVar(&timeoutms, "T", 1000, "Socket/request timeout in ms") + flag.IntVar(&cpus, "cpus", 0, "Number of cpus, i.e. GOMAXPROCS. 0 = system default.") flag.StringVar(&method, "M", "GET", "HTTP method") flag.StringVar(&host, "host", "", "Host Header") flag.Var(&headerFlags, "H", "Header to add to each request (you can define multiple -H flags)") @@ -69,9 +72,15 @@ func printDefaults() { }) } +func mapToString(m map[string]int) string { + s := make([]string,0,len(m)) + for k,v := range m { + s = append(s,fmt.Sprint(k,"=",v)) + } + return strings.Join(s,",") +} + func main() { - //raising the limits. Some performance gains were achieved with the + goroutines (not a lot). - runtime.GOMAXPROCS(runtime.NumCPU() + goroutines) statsAggregator = make(chan *loader.RequesterStats, goroutines) sigChan := make(chan os.Signal, 1) @@ -112,6 +121,10 @@ func main() { return } + if cpus > 0 { + runtime.GOMAXPROCS(cpus) + } + fmt.Printf("Running %vs test @ %v\n %v goroutine(s) running concurrently\n", duration, testUrl, goroutines) if len(reqBody) > 0 && reqBody[0] == '@' { @@ -127,12 +140,14 @@ func main() { loadGen := loader.NewLoadCfg(duration, goroutines, testUrl, reqBody, method, host, header, statsAggregator, timeoutms, allowRedirectsFlag, disableCompression, disableKeepAlive, skipVerify, clientCert, clientKey, caCert, http2) + start := time.Now() + for i := 0; i < goroutines; i++ { go loadGen.RunSingleLoadSession() } responders := 0 - aggStats := loader.RequesterStats{MinRequestTime: time.Minute} + aggStats := loader.RequesterStats{ErrMap: make(map[string]int), Histogram: histo.New(1,int64(duration * 1000000),4)} for responders < goroutines { select { @@ -144,25 +159,54 @@ func main() { aggStats.NumRequests += stats.NumRequests aggStats.TotRespSize += stats.TotRespSize aggStats.TotDuration += stats.TotDuration - aggStats.MaxRequestTime = util.MaxDuration(aggStats.MaxRequestTime, stats.MaxRequestTime) - aggStats.MinRequestTime = util.MinDuration(aggStats.MinRequestTime, stats.MinRequestTime) responders++ + for k,v := range stats.ErrMap { + aggStats.ErrMap[k] += v + } + aggStats.Histogram.Merge(stats.Histogram) } } + duration := time.Now().Sub(start) + if aggStats.NumRequests == 0 { - fmt.Println("Error: No statistics collected / no requests found\n") + fmt.Println("Error: No statistics collected / no requests found") + fmt.Printf("Number of Errors:\t%v\n", aggStats.NumErrs) + if aggStats.NumErrs > 0 { + fmt.Printf("Error Counts:\t\t%v\n", mapToString(aggStats.ErrMap)) + } return } avgThreadDur := aggStats.TotDuration / time.Duration(responders) //need to average the aggregated duration reqRate := float64(aggStats.NumRequests) / avgThreadDur.Seconds() - avgReqTime := aggStats.TotDuration / time.Duration(aggStats.NumRequests) bytesRate := float64(aggStats.TotRespSize) / avgThreadDur.Seconds() + + overallReqRate := float64(aggStats.NumRequests) / duration.Seconds() + overallBytesRate := float64(aggStats.TotRespSize) / duration.Seconds() + fmt.Printf("%v requests in %v, %v read\n", aggStats.NumRequests, avgThreadDur, util.ByteSize{float64(aggStats.TotRespSize)}) - fmt.Printf("Requests/sec:\t\t%.2f\nTransfer/sec:\t\t%v\nAvg Req Time:\t\t%v\n", reqRate, util.ByteSize{bytesRate}, avgReqTime) - fmt.Printf("Fastest Request:\t%v\n", aggStats.MinRequestTime) - fmt.Printf("Slowest Request:\t%v\n", aggStats.MaxRequestTime) + fmt.Printf("Requests/sec:\t\t%.2f\nTransfer/sec:\t\t%v\n", reqRate, util.ByteSize{bytesRate}) + fmt.Printf("Overall Requests/sec:\t%.2f\nOverall Transfer/sec:\t%v\n", overallReqRate, util.ByteSize{overallBytesRate}) + fmt.Printf("Fastest Request:\t%v\n", toDuration(aggStats.Histogram.Min())) + fmt.Printf("Avg Req Time:\t\t%v\n", toDuration(int64(aggStats.Histogram.Mean()))) + fmt.Printf("Slowest Request:\t%v\n", toDuration(aggStats.Histogram.Max())) fmt.Printf("Number of Errors:\t%v\n", aggStats.NumErrs) + if aggStats.NumErrs > 0 { + fmt.Printf("Error Counts:\t\t%v\n", mapToString(aggStats.ErrMap)) + } + fmt.Printf("10%%:\t\t\t%v\n", toDuration(aggStats.Histogram.ValueAtPercentile(.10))) + fmt.Printf("50%%:\t\t\t%v\n", toDuration(aggStats.Histogram.ValueAtPercentile(.50))) + fmt.Printf("75%%:\t\t\t%v\n", toDuration(aggStats.Histogram.ValueAtPercentile(.75))) + fmt.Printf("99%%:\t\t\t%v\n", toDuration(aggStats.Histogram.ValueAtPercentile(.99))) + fmt.Printf("99.9%%:\t\t\t%v\n", toDuration(aggStats.Histogram.ValueAtPercentile(.999))) + fmt.Printf("99.9999%%:\t\t%v\n", toDuration(aggStats.Histogram.ValueAtPercentile(.999999))) + fmt.Printf("99.99999%%:\t\t%v\n", toDuration(aggStats.Histogram.ValueAtPercentile(.9999999))) + fmt.Printf("stddev:\t\t\t%v\n", toDuration(int64(aggStats.Histogram.StdDev()))) + // aggStats.Histogram.PercentilesPrint(os.Stdout,1,1) +} + +func toDuration(usecs int64) time.Duration { + return time.Duration(usecs*1000) } diff --git a/go.mod b/go.mod index 54ef4f4..ce2b604 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,9 @@ module github.com/tsliwowicz/go-wrk go 1.21 -require golang.org/x/net v0.28.0 +require ( + github.com/HdrHistogram/hdrhistogram-go v1.1.2 + golang.org/x/net v0.28.0 +) require golang.org/x/text v0.17.0 // indirect diff --git a/go.sum b/go.sum index 5ce9e30..6e9d6fa 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,64 @@ +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= +github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136 h1:A1gGSx58LAGVHUUsOf7IiR0u8Xb6W51gRwfDBhkdcaw= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.8.2 h1:CCXrcPKiGGotvnN6jfUsKk4rRqm7q09/YbKb5xCEvtM= +gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/loader/loader.go b/loader/loader.go index b33c22d..253c4be 100644 --- a/loader/loader.go +++ b/loader/loader.go @@ -2,9 +2,9 @@ package loader import ( "bytes" + "errors" "fmt" "io" - "io/ioutil" "log" "net/http" "net/url" @@ -12,6 +12,7 @@ import ( "sync/atomic" "time" + histo "github.com/HdrHistogram/hdrhistogram-go" "github.com/tsliwowicz/go-wrk/util" ) @@ -40,14 +41,14 @@ type LoadCfg struct { http2 bool } -// RequesterStats used for colelcting aggregate statistics +// RequesterStats used for collecting aggregate statistics type RequesterStats struct { TotRespSize int64 TotDuration time.Duration - MinRequestTime time.Duration - MaxRequestTime time.Duration NumRequests int NumErrs int + ErrMap map[string]int + Histogram *histo.Histogram } func NewLoadCfg(duration int, // seconds @@ -103,7 +104,7 @@ func escapeUrlStr(in string) string { // DoRequest single request implementation. Returns the size of the response and its duration // On error - returns -1 on both -func DoRequest(httpClient *http.Client, header map[string]string, method, host, loadUrl, reqBody string) (respSize int, duration time.Duration) { +func DoRequest(httpClient *http.Client, header map[string]string, method, host, loadUrl, reqBody string) (respSize int, duration time.Duration, err error) { respSize = -1 duration = -1 @@ -116,8 +117,7 @@ func DoRequest(httpClient *http.Client, header map[string]string, method, host, req, err := http.NewRequest(method, loadUrl, buf) if err != nil { - fmt.Println("An error occured doing request", err) - return + return 0,0,err } for hk, hv := range header { @@ -131,28 +131,25 @@ func DoRequest(httpClient *http.Client, header map[string]string, method, host, start := time.Now() resp, err := httpClient.Do(req) if err != nil { - fmt.Println("redirect?") // this is a bit weird. When redirection is prevented, a url.Error is retuned. This creates an issue to distinguish // between an invalid URL that was provided and and redirection error. - rr, ok := err.(*url.Error) + _, ok := err.(*url.Error) if !ok { - fmt.Println("An error occured doing request", err, rr) - return + return 0,0,err } - fmt.Println("An error occured doing request", err) + return 0,0,err } if resp == nil { - fmt.Println("empty response") - return + return 0,0,errors.New("empty response") } defer func() { if resp != nil && resp.Body != nil { resp.Body.Close() } }() - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { - fmt.Println("An error occured reading body", err) + return 0,0,err } if resp.StatusCode/100 == 2 { // Treat all 2XX as successful duration = time.Since(start) @@ -161,16 +158,23 @@ func DoRequest(httpClient *http.Client, header map[string]string, method, host, duration = time.Since(start) respSize = int(resp.ContentLength) + int(util.EstimateHttpHeadersSize(resp.Header)) } else { - fmt.Println("received status code", resp.StatusCode, "from", resp.Header, "content", string(body), req) + return 0,0,errors.New(fmt.Sprint("received status code ", resp.StatusCode)) } return } +func unwrap(err error) error { + for errors.Unwrap(err)!=nil { + err = errors.Unwrap(err); + } + return err +} + // Requester a go function for repeatedly making requests and aggregating statistics as long as required // When it is done, it sends the results using the statsAggregator channel func (cfg *LoadCfg) RunSingleLoadSession() { - stats := &RequesterStats{MinRequestTime: time.Minute} + stats := &RequesterStats{ErrMap: make(map[string]int), Histogram: histo.New(1,int64(cfg.duration * 1000000),4)} start := time.Now() httpClient, err := client(cfg.disableCompression, cfg.disableKeepAlive, cfg.skipVerify, @@ -180,12 +184,14 @@ func (cfg *LoadCfg) RunSingleLoadSession() { } for time.Since(start).Seconds() <= float64(cfg.duration) && atomic.LoadInt32(&cfg.interrupted) == 0 { - respSize, reqDur := DoRequest(httpClient, cfg.header, cfg.method, cfg.host, cfg.testUrl, cfg.reqBody) - if respSize > 0 { + respSize, reqDur, err := DoRequest(httpClient, cfg.header, cfg.method, cfg.host, cfg.testUrl, cfg.reqBody) + if err != nil { + stats.ErrMap[unwrap(err).Error()]+=1 + stats.NumErrs++ + } else if respSize > 0 { stats.TotRespSize += int64(respSize) stats.TotDuration += reqDur - stats.MaxRequestTime = util.MaxDuration(reqDur, stats.MaxRequestTime) - stats.MinRequestTime = util.MinDuration(reqDur, stats.MinRequestTime) + stats.Histogram.RecordValue(reqDur.Microseconds()); stats.NumRequests++ } else { stats.NumErrs++