Repository: bojand/ghz (go)

Issue:
Percentiles calculation explained?
**Command line arguments / config**
./ghz --insecure --total 15 --connections 1 --concurrency 10 --timeout 0 --proto ./data/svc.proto --call svc.v1.Service.Start. --binary-file data.pb --duration 0 --skipFirst 0 -O json localhost:50051

**Describe the bug**
I believe the percentiles are not accurate. See example for median.
This is the `details` section of the output json:  
```
"details": [
      {
         "timestamp":"2020-08-19T15:45:58.495351217+03:00",
         "latency":1042165691,
         "error":"",
         "status":"OK"
      },
      {
         "timestamp":"2020-08-19T15:45:58.501101627+03:00",
         "latency":1047885273,
         "error":"",
         "status":"OK"
      },
      {
         "timestamp":"2020-08-19T15:45:58.505083345+03:00",
         "latency":1051897867,
         "error":"",
         "status":"OK"
      },
      {
         "timestamp":"2020-08-19T15:45:58.515618616+03:00",
         "latency":1068984262,
         "error":"",
         "status":"OK"
      },
      {
         "timestamp":"2020-08-19T15:45:58.529694695+03:00",
         "latency":1083060826,
         "error":"",
         "status":"OK"
      },
      {
         "timestamp":"2020-08-19T15:45:59.506724066+03:00",
         "latency":1011303003,
         "error":"",
         "status":"OK"
      },
      {
         "timestamp":"2020-08-19T15:45:59.512665534+03:00",
         "latency":996989754,
         "error":"",
         "status":"OK"
      },
      {
         "timestamp":"2020-08-19T15:45:59.522287957+03:00",
         "latency":992551499,
         "error":"",
         "status":"OK"
      },
      {
         "timestamp":"2020-08-19T15:45:59.530468266+03:00",
         "latency":1029325988,
         "error":"",
         "status":"OK"
      },
      {
         "timestamp":"2020-08-19T15:45:59.544120387+03:00",
         "latency":1038985451,
         "error":"",
         "status":"OK"
      },
      {
         "timestamp":"2020-08-19T15:46:00.537823106+03:00",
         "latency":993612515,
         "error":"",
         "status":"OK"
      },
      {
         "timestamp":"2020-08-19T15:46:00.538416936+03:00",
         "latency":1007881907,
         "error":"",
         "status":"OK"
      },
      {
         "timestamp":"2020-08-19T15:46:00.543929337+03:00",
         "latency":1031228969,
         "error":"",
         "status":"OK"
      },
      {
         "timestamp":"2020-08-19T15:46:00.56012676+03:00",
         "latency":1053347631,
         "error":"",
         "status":"OK"
      },
      {
         "timestamp":"2020-08-19T15:46:00.563120347+03:00",
         "latency":1040774187,
         "error":"",
         "status":"OK"
      }
   ]
```
These are the percentiles:
```
"latencyDistribution":[
      {
         "percentage":10,
         "latency":996989754
      },
      {
         "percentage":25,
         "latency":1011303003
      },
      {
         "percentage":50,
         "latency":1040774187
      },
      {
         "percentage":75,
         "latency":1053347631
      },
      {
         "percentage":90,
         "latency":1083060826
      },
      {
         "percentage":0,
         "latency":0
      },
      {
         "percentage":0,
         "latency":0
      }
   ]
```

**Expected behavior**
The sorted latency values are:
`[ 992551499,  993612515,  996989754, 1007881907, 1011303003,
       1029325988, 1031228969, 1038985451, 1040774187, 1042165691,
       1047885273, 1051897867, 1053347631, 1068984262, 1083060826]`

**There are 15 items, the median is is the 8th item which is 1038985451 (and not 1040774187 as in the output.).**

Using numpy:
`np.percentile(l, [10, 25, 50, 75, 90], interpolation='higher') `
Output:
`>> array([ 996989754, 1011303003, 1038985451, 1051897867, 1068984262])`
Which is slightly different for the 50% and the 75%.

Second, Why I the two last items in the array with percentage=0?


**Environment**
 - OS: Ubuntu 18.04
 - ghz: 0.56.0
 

Pull request:
Percentile Latencies Fix
Use [nearest rank](https://en.wikipedia.org/wiki/Percentile#The_nearest-rank_method) method to calculate percentiles. Fixes #215.

Patch:
diff --git a/runner/reporter.go b/runner/reporter.go
index ed166af..473f9dd 100644
--- a/runner/reporter.go
+++ b/runner/reporter.go
@@ -240,14 +240,25 @@ func (r *Reporter) Finalize(stopReason StopReason, total time.Duration) *Report
 func latencies(latencies []float64) []LatencyDistribution {
 	pctls := []int{10, 25, 50, 75, 90, 95, 99}
 	data := make([]float64, len(pctls))
-	j := 0
-	for i := 0; i < len(latencies) && j < len(pctls); i++ {
-		current := i * 100 / len(latencies)
-		if current >= pctls[j] {
-			data[j] = latencies[i]
-			j++
+	lt := float64(len(latencies))
+	for i, p := range pctls {
+		ip := (float64(p) / 100.0) * lt
+		di := int(ip)
+
+		// since we're dealing with 0th based ranks we need to
+		// check if ordinal is a whole number that lands on the percentile
+		// if so adjust accordingly
+		if ip == float64(di) {
+			di = di - 1
 		}
+
+		if di < 0 {
+			di = 0
+		}
+
+		data[i] = latencies[di]
 	}
+
 	res := make([]LatencyDistribution, len(pctls))
 	for i := 0; i < len(pctls); i++ {
 		if data[i] > 0 {


Test patch:
diff --git a/runner/reporter_test.go b/runner/reporter_test.go
index 9434f2f..fe5a7af 100644
--- a/runner/reporter_test.go
+++ b/runner/reporter_test.go
@@ -3,6 +3,7 @@ package runner
 import (
 	"context"
 	"encoding/json"
+	"strconv"
 	"testing"
 	"time"
 
@@ -58,3 +59,66 @@ func TestReport_CorrectDetails(t *testing.T) {
 	assert.Equal(t, ResultDetail{Error: "", Latency: cr1.duration, Status: cr1.status, Timestamp: cr1.timestamp}, report.Details[0])
 	assert.Equal(t, ResultDetail{Error: cr2.err.Error(), Latency: cr2.duration, Status: cr2.status, Timestamp: cr2.timestamp}, report.Details[1])
 }
+
+func TestReport_latencies(t *testing.T) {
+	var tests = []struct {
+		input    []float64
+		expected []LatencyDistribution
+	}{
+		{
+			input: []float64{15, 20, 35, 40, 50},
+			expected: []LatencyDistribution{
+				{Percentage: 10, Latency: 15 * time.Second},
+				{Percentage: 25, Latency: 20 * time.Second},
+				{Percentage: 50, Latency: 35 * time.Second},
+				{Percentage: 75, Latency: 40 * time.Second},
+				{Percentage: 90, Latency: 50 * time.Second},
+				{Percentage: 95, Latency: 50 * time.Second},
+				{Percentage: 99, Latency: 50 * time.Second},
+			},
+		},
+		{
+			input: []float64{3, 6, 7, 8, 8, 10, 13, 15, 16, 20},
+			expected: []LatencyDistribution{
+				{Percentage: 10, Latency: 3 * time.Second},
+				{Percentage: 25, Latency: 7 * time.Second},
+				{Percentage: 50, Latency: 8 * time.Second},
+				{Percentage: 75, Latency: 15 * time.Second},
+				{Percentage: 90, Latency: 16 * time.Second},
+				{Percentage: 95, Latency: 20 * time.Second},
+				{Percentage: 99, Latency: 20 * time.Second},
+			},
+		},
+		{
+			input: []float64{3, 6, 7, 8, 8, 9, 10, 13, 15, 16, 20},
+			expected: []LatencyDistribution{
+				{Percentage: 10, Latency: 6 * time.Second},
+				{Percentage: 25, Latency: 7 * time.Second},
+				{Percentage: 50, Latency: 9 * time.Second},
+				{Percentage: 75, Latency: 15 * time.Second},
+				{Percentage: 90, Latency: 16 * time.Second},
+				{Percentage: 95, Latency: 20 * time.Second},
+				{Percentage: 99, Latency: 20 * time.Second},
+			},
+		},
+		{
+			input: []float64{2.1, 3.2, 4.5, 6.3, 7.4, 8.5, 9.6, 10.7, 13.8, 15.9, 16.11, 18.17, 20.11, 22.34},
+			expected: []LatencyDistribution{
+				{Percentage: 10, Latency: time.Duration(3.2 * float64(time.Second))},
+				{Percentage: 25, Latency: time.Duration(6.3 * float64(time.Second))},
+				{Percentage: 50, Latency: time.Duration(9.6 * float64(time.Second))},
+				{Percentage: 75, Latency: time.Duration(16.11 * float64(time.Second))},
+				{Percentage: 90, Latency: time.Duration(20.11 * float64(time.Second))},
+				{Percentage: 95, Latency: time.Duration(22.34 * float64(time.Second))},
+				{Percentage: 99, Latency: time.Duration(22.34 * float64(time.Second))},
+			},
+		},
+	}
+
+	for i, tt := range tests {
+		t.Run("latencies "+strconv.FormatInt(int64(i), 10), func(t *testing.T) {
+			lats := latencies(tt.input)
+			assert.Equal(t, tt.expected, lats)
+		})
+	}
+}