-
Notifications
You must be signed in to change notification settings - Fork 382
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Percentile support #86
Changes from all commits
e165ab9
2cb7be8
da6968d
97cbdd4
6a39bcd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,8 @@ | |
package report | ||
|
||
import ( | ||
"math" | ||
"sort" | ||
"strings" | ||
"time" | ||
|
||
|
@@ -39,6 +41,7 @@ func aggregate(result *Result, response *types.Response) { | |
StatusCodeDist: make(map[int]int, 0), | ||
ErrorDist: make(map[string]int), | ||
Durations: map[string]float32{}, | ||
TotalDurations: map[string][]float32{}, | ||
} | ||
} | ||
item := result.ItemReports[rr.ScenarioItemID] | ||
|
@@ -60,7 +63,12 @@ func aggregate(result *Result, response *types.Response) { | |
} | ||
} | ||
} | ||
} | ||
|
||
for _, report := range result.ItemReports { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't need this extra loop. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can add below code snippet at line 57 or anywhere in that
I think the aggregator should store durations only if the percentage report is enabled. |
||
for key, duration := range report.Durations { | ||
report.TotalDurations[key] = append(report.TotalDurations[key], duration) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't need to store other durations (DNS, TLS, etc.) total duration would be enough ("duration") for now. |
||
} | ||
} | ||
|
||
// Don't change avg duration if there is a error | ||
|
@@ -96,12 +104,29 @@ func (r *Result) failedPercentage() int { | |
} | ||
|
||
type ScenarioItemReport struct { | ||
Name string `json:"name"` | ||
StatusCodeDist map[int]int `json:"status_code_dist"` | ||
ErrorDist map[string]int `json:"error_dist"` | ||
Durations map[string]float32 `json:"durations"` | ||
SuccessCount int64 `json:"success_count"` | ||
FailedCount int64 `json:"fail_count"` | ||
Name string `json:"name"` | ||
StatusCodeDist map[int]int `json:"status_code_dist"` | ||
ErrorDist map[string]int `json:"error_dist"` | ||
Durations map[string]float32 `json:"durations"` | ||
TotalDurations map[string][]float32 `json:"total_durations"` | ||
SuccessCount int64 `json:"success_count"` | ||
FailedCount int64 `json:"fail_count"` | ||
} | ||
|
||
func (s *ScenarioItemReport) DurationPercentile(p int) float32 { | ||
if p < 0 || p > 100 { | ||
return 0 | ||
} | ||
|
||
durations, ok := s.TotalDurations["duration"] | ||
if !ok { | ||
return 0 | ||
} | ||
|
||
// todo: it could be optimized by always sorted array being used in TotalDurations so we would not make this call. | ||
sort.Slice(durations, func(i, j int) bool { return durations[i] < durations[j] }) | ||
|
||
return Percentile(durations, p) | ||
} | ||
|
||
func (s *ScenarioItemReport) successPercentage() int { | ||
|
@@ -118,3 +143,17 @@ func (s *ScenarioItemReport) failedPercentage() int { | |
} | ||
return 100 - s.successPercentage() | ||
} | ||
|
||
func Percentile(list []float32, p int) float32 { | ||
if p < 0 || p > 100 { | ||
return 0 | ||
} | ||
|
||
n := int(math.Round((float64(p) / 100.0) * float64(len(list)))) | ||
if n > 0 { | ||
// I am not sure about the case where n == 0 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to cover this unless we need a percentile less than p50, we can remove this check. |
||
n-- | ||
} | ||
|
||
return list[n] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -45,22 +45,24 @@ func init() { | |
} | ||
|
||
type stdout struct { | ||
doneChan chan struct{} | ||
result *Result | ||
printTicker *time.Ticker | ||
mu sync.Mutex | ||
doneChan chan struct{} | ||
result *Result | ||
printTicker *time.Ticker | ||
mu sync.Mutex | ||
reportPercentiles bool | ||
} | ||
|
||
var blue = color.New(color.FgHiBlue).SprintFunc() | ||
var green = color.New(color.FgHiGreen).SprintFunc() | ||
var red = color.New(color.FgHiRed).SprintFunc() | ||
var realTimePrintInterval = time.Duration(1500) * time.Millisecond | ||
|
||
func (s *stdout) Init() (err error) { | ||
func (s *stdout) Init(reportPercentiles bool) (err error) { | ||
s.doneChan = make(chan struct{}) | ||
s.result = &Result{ | ||
ItemReports: make(map[int16]*ScenarioItemReport), | ||
} | ||
s.reportPercentiles = reportPercentiles | ||
|
||
color.Cyan("%s Initializing... \n", emoji.Gear) | ||
return | ||
|
@@ -108,11 +110,10 @@ func (s *stdout) realTimePrintStart() { | |
|
||
func (s *stdout) liveResultPrint() { | ||
fmt.Fprintf(out, "%s %s %s\n", | ||
green(fmt.Sprintf("%s Successful Run: %-6d %3d%% %5s", | ||
emoji.CheckMark, s.result.SuccessCount, s.result.successPercentage(), "")), | ||
red(fmt.Sprintf("%s Failed Run: %-6d %3d%% %5s", | ||
emoji.CrossMark, s.result.FailedCount, s.result.failedPercentage(), "")), | ||
blue(fmt.Sprintf("%s Avg. Duration: %.5fs", emoji.Stopwatch, s.result.AvgDuration))) | ||
green(fmt.Sprintf("%s Successful Run: %-6d %3d%% %5s", emoji.CheckMark, s.result.SuccessCount, s.result.successPercentage(), "")), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The max. line-length is limited to 120 chars as stated in the linter settings (at .golangci.yml). Let's follow the standards. |
||
red(fmt.Sprintf("%s Failed Run: %-6d %3d%% %5s", emoji.CrossMark, s.result.FailedCount, s.result.failedPercentage(), "")), | ||
blue(fmt.Sprintf("%s Avg. Duration: %.5fs", emoji.Stopwatch, s.result.AvgDuration)), | ||
) | ||
} | ||
|
||
func (s *stdout) realTimePrintStop() { | ||
|
@@ -173,6 +174,22 @@ func (s *stdout) printDetails() { | |
fmt.Fprintf(w, " %s\t:%.4fs\n", v.name, v.duration) | ||
} | ||
|
||
if s.reportPercentiles { | ||
// print percentalies | ||
percentiles := []map[string]float32{ | ||
{"P99": v.DurationPercentile(99)}, | ||
{"P95": v.DurationPercentile(95)}, | ||
{"P90": v.DurationPercentile(90)}, | ||
{"P80": v.DurationPercentile(80)}, | ||
} | ||
fmt.Fprintln(w, "\nPercentiles:") | ||
for _, val := range percentiles { | ||
for name, percentile := range val { | ||
fmt.Fprintf(w, " %s\t:%.4fs\n", name, percentile) | ||
} | ||
} | ||
} | ||
|
||
if len(v.StatusCodeDist) > 0 { | ||
fmt.Fprintln(w, "\nStatus Code (Message) :Count") | ||
for s, c := range v.StatusCodeDist { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,8 +22,8 @@ package report | |
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"math" | ||
"os" | ||
|
||
"go.ddosify.com/ddosify/core/types" | ||
) | ||
|
@@ -35,15 +35,18 @@ func init() { | |
} | ||
|
||
type stdoutJson struct { | ||
doneChan chan struct{} | ||
result *Result | ||
doneChan chan struct{} | ||
result *Result | ||
reportPercentiles bool | ||
} | ||
|
||
func (s *stdoutJson) Init() (err error) { | ||
func (s *stdoutJson) Init(reportPercentiles bool) (err error) { | ||
s.doneChan = make(chan struct{}) | ||
s.result = &Result{ | ||
ItemReports: make(map[int16]*ScenarioItemReport), | ||
} | ||
s.reportPercentiles = reportPercentiles | ||
|
||
return | ||
} | ||
|
||
|
@@ -54,12 +57,57 @@ func (s *stdoutJson) Start(input chan *types.Response) { | |
s.doneChan <- struct{}{} | ||
} | ||
|
||
type jsonResult struct { | ||
SuccessCount int64 `json:"success_count"` | ||
FailedCount int64 `json:"fail_count"` | ||
AvgDuration float32 `json:"avg_duration"` | ||
ItemReports map[int16]*jsonScenarioItemReport `json:"steps"` | ||
} | ||
|
||
type jsonScenarioItemReport struct { | ||
Name string `json:"name"` | ||
StatusCodeDist map[int]int `json:"status_code_dist"` | ||
ErrorDist map[string]int `json:"error_dist"` | ||
Durations map[string]float32 `json:"durations"` | ||
Percentiles map[string]float32 `json:"percentiles"` | ||
SuccessCount int64 `json:"success_count"` | ||
FailedCount int64 `json:"fail_count"` | ||
} | ||
|
||
func (s *stdoutJson) Report() { | ||
p := 1e3 | ||
jsonResult := jsonResult{ | ||
SuccessCount: s.result.SuccessCount, | ||
FailedCount: s.result.FailedCount, | ||
AvgDuration: s.result.AvgDuration, | ||
ItemReports: map[int16]*jsonScenarioItemReport{}, | ||
} | ||
|
||
for key, item := range s.result.ItemReports { | ||
jsonResult.ItemReports[key] = &jsonScenarioItemReport{ | ||
Name: item.Name, | ||
StatusCodeDist: item.StatusCodeDist, | ||
ErrorDist: item.ErrorDist, | ||
Durations: item.Durations, | ||
SuccessCount: item.SuccessCount, | ||
FailedCount: item.FailedCount, | ||
} | ||
|
||
if s.reportPercentiles { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If possible, let's not show the Current state:
|
||
jsonResult.ItemReports[key].Percentiles = map[string]float32{ | ||
"p99": item.DurationPercentile(99), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Milliseconds would be enough. Current stdout-json example:
|
||
"p95": item.DurationPercentile(95), | ||
"p90": item.DurationPercentile(90), | ||
"p80": item.DurationPercentile(80), | ||
} | ||
} | ||
|
||
} | ||
|
||
p := 1e3 | ||
s.result.AvgDuration = float32(math.Round(float64(s.result.AvgDuration)*p) / p) | ||
|
||
for _, itemReport := range s.result.ItemReports { | ||
for _, item := range jsonResult.ItemReports { | ||
itemReport := item | ||
durations := make(map[string]float32) | ||
for d, s := range itemReport.Durations { | ||
// Less precision for durations. | ||
|
@@ -69,8 +117,8 @@ func (s *stdoutJson) Report() { | |
itemReport.Durations = durations | ||
} | ||
|
||
j, _ := json.Marshal(s.result) | ||
printJson(j) | ||
encoder := json.NewEncoder(os.Stdout) | ||
encoder.Encode(&jsonResult) | ||
} | ||
|
||
func (s *stdoutJson) DoneChan() <-chan struct{} { | ||
|
@@ -107,10 +155,6 @@ func (s ScenarioItemReport) MarshalJSON() ([]byte, error) { | |
}) | ||
} | ||
|
||
var printJson = func(j []byte) { | ||
fmt.Println(string(j)) | ||
} | ||
|
||
var strKeyToJsonKey = map[string]string{ | ||
"dnsDuration": "dns", | ||
"connDuration": "connection", | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some places use "Percentile" others use "Percentiles". I think we can use "Percentile" everywhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or "Percentiles". Whatever you want, but let's make the naming consistent