From e165ab9df7cd34a7f982f66e5fcf7056850103fa Mon Sep 17 00:00:00 2001 From: Maxim Zhiburt Date: Fri, 7 Oct 2022 16:56:09 +0300 Subject: [PATCH 1/4] Add Percentile support Signed-off-by: Maxim Zhiburt --- core/report/aggregator.go | 58 ++++++++++++++++++++++++++++------ core/report/stdout.go | 23 +++++++++----- core/report/stdoutJson.go | 5 +-- core/report/stdoutJson_test.go | 12 +++---- core/report/stdout_test.go | 6 ++-- 5 files changed, 75 insertions(+), 29 deletions(-) diff --git a/core/report/aggregator.go b/core/report/aggregator.go index 893bdc7a..264c56b7 100644 --- a/core/report/aggregator.go +++ b/core/report/aggregator.go @@ -21,6 +21,7 @@ package report import ( + "sort" "strings" "time" @@ -34,14 +35,17 @@ func aggregate(result *Result, response *types.Response) { scenarioDuration += float32(rr.Duration.Seconds()) if _, ok := result.ItemReports[rr.ScenarioItemID]; !ok { - result.ItemReports[rr.ScenarioItemID] = &ScenarioItemReport{ - Name: rr.ScenarioItemName, - StatusCodeDist: make(map[int]int, 0), - ErrorDist: make(map[string]int), - Durations: map[string]float32{}, + result.ItemReports[rr.ScenarioItemID] = &ScenarioResult{ + Report: &ScenarioItemReport{ + Name: rr.ScenarioItemName, + StatusCodeDist: make(map[int]int, 0), + ErrorDist: make(map[string]int), + Durations: map[string]float32{}, + }, + Durations: map[string]*ScenarioStats{}, } } - item := result.ItemReports[rr.ScenarioItemID] + item := result.ItemReports[rr.ScenarioItemID].Report if rr.Err.Type != "" { errOccured = true @@ -60,7 +64,17 @@ func aggregate(result *Result, response *types.Response) { } } } + } + + for _, report := range result.ItemReports { + for key, duration := range report.Report.Durations { + if report.Durations[key] == nil { + report.Durations[key] = &ScenarioStats{} + } + report.Durations[key].Durations = append(report.Durations[key].Durations, duration) + sort.Slice(report.Durations[key].Durations, func(i, j int) bool { return report.Durations[key].Durations[i] < report.Durations[key].Durations[j] }) + } } // Don't change avg duration if there is a error @@ -74,10 +88,10 @@ func aggregate(result *Result, response *types.Response) { } type Result struct { - SuccessCount int64 `json:"success_count"` - FailedCount int64 `json:"fail_count"` - AvgDuration float32 `json:"avg_duration"` - ItemReports map[int16]*ScenarioItemReport `json:"steps"` + SuccessCount int64 `json:"success_count"` + FailedCount int64 `json:"fail_count"` + AvgDuration float32 `json:"avg_duration"` + ItemReports map[int16]*ScenarioResult `json:"steps"` } func (r *Result) successPercentage() int { @@ -95,6 +109,30 @@ func (r *Result) failedPercentage() int { return 100 - r.successPercentage() } +type ScenarioResult struct { + Report *ScenarioItemReport `json:"report"` + Durations map[string]*ScenarioStats `json:"durations"` +} + +type ScenarioStats struct { + Durations []float32 `json:"durations"` +} + +func (r *ScenarioStats) Percentile(p int) float32 { + if p < 0 || p > 100 { + return 0 + } + + // n = (P/100) x N + n := (p / 100) * len(r.Durations) + if n > 0 { + // I am not sure about the case where n == 0 + n-- + } + + return r.Durations[n] +} + type ScenarioItemReport struct { Name string `json:"name"` StatusCodeDist map[int]int `json:"status_code_dist"` diff --git a/core/report/stdout.go b/core/report/stdout.go index 6708cd2b..a4531f53 100644 --- a/core/report/stdout.go +++ b/core/report/stdout.go @@ -54,12 +54,13 @@ type stdout struct { var blue = color.New(color.FgHiBlue).SprintFunc() var green = color.New(color.FgHiGreen).SprintFunc() var red = color.New(color.FgHiRed).SprintFunc() +var cyan = color.New(color.FgHiCyan).SprintFunc() var realTimePrintInterval = time.Duration(1500) * time.Millisecond func (s *stdout) Init() (err error) { s.doneChan = make(chan struct{}) s.result = &Result{ - ItemReports: make(map[int16]*ScenarioItemReport), + ItemReports: make(map[int16]*ScenarioResult), } color.Cyan("%s Initializing... \n", emoji.Gear) @@ -107,12 +108,18 @@ 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))) + fmt.Fprintf(out, "%s %s %s", + 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)), + ) + + for key, item := range s.result.ItemReports { + p := item.Durations["duration"].Percentile(95) + fmt.Fprintf(out, " %s", cyan(fmt.Sprintf("[%d] p(95): %.5fs", key, p))) + } + + fmt.Fprintln(out, "") } func (s *stdout) realTimePrintStop() { @@ -145,7 +152,7 @@ func (s *stdout) printDetails() { sort.Ints(keys) for _, k := range keys { - v := s.result.ItemReports[int16(k)] + v := s.result.ItemReports[int16(k)].Report if len(keys) > 1 { stepHeader := v.Name diff --git a/core/report/stdoutJson.go b/core/report/stdoutJson.go index a4017ae6..bcc12aac 100644 --- a/core/report/stdoutJson.go +++ b/core/report/stdoutJson.go @@ -42,7 +42,7 @@ type stdoutJson struct { func (s *stdoutJson) Init() (err error) { s.doneChan = make(chan struct{}) s.result = &Result{ - ItemReports: make(map[int16]*ScenarioItemReport), + ItemReports: make(map[int16]*ScenarioResult), } return } @@ -59,7 +59,8 @@ func (s *stdoutJson) Report() { s.result.AvgDuration = float32(math.Round(float64(s.result.AvgDuration)*p) / p) - for _, itemReport := range s.result.ItemReports { + for _, item := range s.result.ItemReports { + itemReport := item.Report durations := make(map[string]float32) for d, s := range itemReport.Durations { // Less precision for durations. diff --git a/core/report/stdoutJson_test.go b/core/report/stdoutJson_test.go index e1abc820..052d4ce1 100644 --- a/core/report/stdoutJson_test.go +++ b/core/report/stdoutJson_test.go @@ -123,9 +123,9 @@ func TestStdoutJsonStart(t *testing.T) { SuccessCount: 1, FailedCount: 1, AvgDuration: 90, - ItemReports: map[int16]*ScenarioItemReport{ - int16(1): itemReport1, - int16(2): itemReport2, + ItemReports: map[int16]*ScenarioResult{ + int16(1): {Report: itemReport1}, + int16(2): {Report: itemReport2}, }, } @@ -186,9 +186,9 @@ func TestStdoutJsonOutput(t *testing.T) { SuccessCount: 9, FailedCount: 2, AvgDuration: 0.25637, - ItemReports: map[int16]*ScenarioItemReport{ - int16(1): itemReport1, - int16(2): itemReport2, + ItemReports: map[int16]*ScenarioResult{ + int16(1): {Report: itemReport1}, + int16(2): {Report: itemReport2}, }, } diff --git a/core/report/stdout_test.go b/core/report/stdout_test.go index 08d206b1..f413cfbf 100644 --- a/core/report/stdout_test.go +++ b/core/report/stdout_test.go @@ -183,9 +183,9 @@ func TestStart(t *testing.T) { SuccessCount: 1, FailedCount: 1, AvgDuration: 90, - ItemReports: map[int16]*ScenarioItemReport{ - int16(1): itemReport1, - int16(2): itemReport2, + ItemReports: map[int16]*ScenarioResult{ + int16(1): {Report: itemReport1}, + int16(2): {Report: itemReport2}, }, } From 2cb7be89c2acb60726bf7bc4e38eeeb880e7b075 Mon Sep 17 00:00:00 2001 From: Maxim Zhiburt Date: Fri, 7 Oct 2022 17:32:34 +0300 Subject: [PATCH 2/4] Merge scenarius when doing p(95) Signed-off-by: Maxim Zhiburt --- core/report/aggregator.go | 19 +++++++++++++++++++ core/report/stdout.go | 12 ++++-------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/core/report/aggregator.go b/core/report/aggregator.go index 264c56b7..246a6ff5 100644 --- a/core/report/aggregator.go +++ b/core/report/aggregator.go @@ -109,6 +109,25 @@ func (r *Result) failedPercentage() int { return 100 - r.successPercentage() } +func (r *Result) DurationPercentile(p int) float32 { + if p < 0 || p > 100 { + return 0 + } + + var durations []float32 + for _, rr := range r.ItemReports { + if list, ok := rr.Durations["duration"]; ok { + durations = append(durations, list.Durations...) + } + } + + sort.Slice(durations, func(i, j int) bool { return durations[i] < durations[j] }) + + d := ScenarioStats{Durations: durations} + + return d.Percentile(p) +} + type ScenarioResult struct { Report *ScenarioItemReport `json:"report"` Durations map[string]*ScenarioStats `json:"durations"` diff --git a/core/report/stdout.go b/core/report/stdout.go index a4531f53..40766c86 100644 --- a/core/report/stdout.go +++ b/core/report/stdout.go @@ -108,18 +108,14 @@ func (s *stdout) realTimePrintStart() { } func (s *stdout) liveResultPrint() { - fmt.Fprintf(out, "%s %s %s", + p := s.result.DurationPercentile(95) + + fmt.Fprintf(out, "%s %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)), + cyan(fmt.Sprintf("%s p(95): %.5fs", emoji.HourglassNotDone, p)), ) - - for key, item := range s.result.ItemReports { - p := item.Durations["duration"].Percentile(95) - fmt.Fprintf(out, " %s", cyan(fmt.Sprintf("[%d] p(95): %.5fs", key, p))) - } - - fmt.Fprintln(out, "") } func (s *stdout) realTimePrintStop() { From 97cbdd436faa828386afc98d1dec2000db681a17 Mon Sep 17 00:00:00 2001 From: Maxim Zhiburt Date: Wed, 19 Oct 2022 00:18:14 +0300 Subject: [PATCH 3/4] Report percentile only on report stage + add CLI flags Signed-off-by: Maxim Zhiburt --- config/json.go | 16 ++--- core/engine.go | 2 +- core/report/aggregator.go | 104 ++++++++++++++------------------- core/report/base.go | 8 +-- core/report/stdout.go | 36 ++++++++---- core/report/stdoutJson.go | 71 +++++++++++++++++----- core/report/stdoutJson_test.go | 21 ++++--- core/report/stdout_test.go | 10 ++-- core/types/hammer.go | 3 + main.go | 3 + main_test.go | 5 ++ 11 files changed, 167 insertions(+), 112 deletions(-) diff --git a/config/json.go b/config/json.go index 03fceb55..dd7f9bbe 100644 --- a/config/json.go +++ b/config/json.go @@ -98,13 +98,14 @@ func (s *step) UnmarshalJSON(data []byte) error { } type JsonReader struct { - ReqCount int `json:"request_count"` - LoadType string `json:"load_type"` - Duration int `json:"duration"` - TimeRunCount timeRunCount `json:"manual_load"` - Steps []step `json:"steps"` - Output string `json:"output"` - Proxy string `json:"proxy"` + ReqCount int `json:"request_count"` + LoadType string `json:"load_type"` + Duration int `json:"duration"` + TimeRunCount timeRunCount `json:"manual_load"` + Steps []step `json:"steps"` + Output string `json:"output"` + Proxy string `json:"proxy"` + OutputPercentile bool `json:"output_percentile"` } func (j *JsonReader) UnmarshalJSON(data []byte) error { @@ -182,6 +183,7 @@ func (j *JsonReader) CreateHammer() (h types.Hammer, err error) { Scenario: s, Proxy: p, ReportDestination: j.Output, + ReportPercentiles: j.OutputPercentile, } return } diff --git a/core/engine.go b/core/engine.go index d89c50b0..6573bb15 100644 --- a/core/engine.go +++ b/core/engine.go @@ -99,7 +99,7 @@ func (e *engine) Init() (err error) { return } - if err = e.reportService.Init(); err != nil { + if err = e.reportService.Init(e.hammer.ReportPercentiles); err != nil { return } diff --git a/core/report/aggregator.go b/core/report/aggregator.go index 246a6ff5..218712aa 100644 --- a/core/report/aggregator.go +++ b/core/report/aggregator.go @@ -21,6 +21,7 @@ package report import ( + "math" "sort" "strings" "time" @@ -35,17 +36,15 @@ func aggregate(result *Result, response *types.Response) { scenarioDuration += float32(rr.Duration.Seconds()) if _, ok := result.ItemReports[rr.ScenarioItemID]; !ok { - result.ItemReports[rr.ScenarioItemID] = &ScenarioResult{ - Report: &ScenarioItemReport{ - Name: rr.ScenarioItemName, - StatusCodeDist: make(map[int]int, 0), - ErrorDist: make(map[string]int), - Durations: map[string]float32{}, - }, - Durations: map[string]*ScenarioStats{}, + result.ItemReports[rr.ScenarioItemID] = &ScenarioItemReport{ + Name: rr.ScenarioItemName, + StatusCodeDist: make(map[int]int, 0), + ErrorDist: make(map[string]int), + Durations: map[string]float32{}, + TotalDurations: map[string][]float32{}, } } - item := result.ItemReports[rr.ScenarioItemID].Report + item := result.ItemReports[rr.ScenarioItemID] if rr.Err.Type != "" { errOccured = true @@ -67,13 +66,8 @@ func aggregate(result *Result, response *types.Response) { } for _, report := range result.ItemReports { - for key, duration := range report.Report.Durations { - if report.Durations[key] == nil { - report.Durations[key] = &ScenarioStats{} - } - - report.Durations[key].Durations = append(report.Durations[key].Durations, duration) - sort.Slice(report.Durations[key].Durations, func(i, j int) bool { return report.Durations[key].Durations[i] < report.Durations[key].Durations[j] }) + for key, duration := range report.Durations { + report.TotalDurations[key] = append(report.TotalDurations[key], duration) } } @@ -88,10 +82,10 @@ func aggregate(result *Result, response *types.Response) { } type Result struct { - SuccessCount int64 `json:"success_count"` - FailedCount int64 `json:"fail_count"` - AvgDuration float32 `json:"avg_duration"` - ItemReports map[int16]*ScenarioResult `json:"steps"` + SuccessCount int64 `json:"success_count"` + FailedCount int64 `json:"fail_count"` + AvgDuration float32 `json:"avg_duration"` + ItemReports map[int16]*ScenarioItemReport `json:"steps"` } func (r *Result) successPercentage() int { @@ -109,56 +103,30 @@ func (r *Result) failedPercentage() int { return 100 - r.successPercentage() } -func (r *Result) DurationPercentile(p int) float32 { - if p < 0 || p > 100 { - return 0 - } - - var durations []float32 - for _, rr := range r.ItemReports { - if list, ok := rr.Durations["duration"]; ok { - durations = append(durations, list.Durations...) - } - } - - sort.Slice(durations, func(i, j int) bool { return durations[i] < durations[j] }) - - d := ScenarioStats{Durations: durations} - - return d.Percentile(p) -} - -type ScenarioResult struct { - Report *ScenarioItemReport `json:"report"` - Durations map[string]*ScenarioStats `json:"durations"` -} - -type ScenarioStats struct { - Durations []float32 `json:"durations"` +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"` + TotalDurations map[string][]float32 `json:"total_durations"` + SuccessCount int64 `json:"success_count"` + FailedCount int64 `json:"fail_count"` } -func (r *ScenarioStats) Percentile(p int) float32 { +func (s *ScenarioItemReport) DurationPercentile(p int) float32 { if p < 0 || p > 100 { return 0 } - // n = (P/100) x N - n := (p / 100) * len(r.Durations) - if n > 0 { - // I am not sure about the case where n == 0 - n-- + durations, ok := s.TotalDurations["duration"] + if !ok { + return 0 } - return r.Durations[n] -} + // 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] }) -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"` + return Percentile(durations, p) } func (s *ScenarioItemReport) successPercentage() int { @@ -175,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 + n-- + } + + return list[n] +} diff --git a/core/report/base.go b/core/report/base.go index 2e385a01..cd5812b8 100644 --- a/core/report/base.go +++ b/core/report/base.go @@ -32,18 +32,18 @@ var AvailableOutputServices = make(map[string]ReportService) // ReportService is the interface that abstracts different report implementations. type ReportService interface { DoneChan() <-chan struct{} - Init() error + Init(reportPercentiles bool) error Start(input chan *types.Response) Report() } // NewReportService is the factory method of the ReportService. -func NewReportService(s string) (service ReportService, err error) { - if val, ok := AvailableOutputServices[s]; ok { +func NewReportService(output string) (service ReportService, err error) { + if val, ok := AvailableOutputServices[output]; ok { // Create a new object from the service type service = reflect.New(reflect.TypeOf(val).Elem()).Interface().(ReportService) } else { - err = fmt.Errorf("unsupported output type: %s", s) + err = fmt.Errorf("unsupported output type: %s", output) } return diff --git a/core/report/stdout.go b/core/report/stdout.go index 40766c86..3132cf46 100644 --- a/core/report/stdout.go +++ b/core/report/stdout.go @@ -45,23 +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 cyan = color.New(color.FgHiCyan).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]*ScenarioResult), + ItemReports: make(map[int16]*ScenarioItemReport), } + s.reportPercentiles = reportPercentiles color.Cyan("%s Initializing... \n", emoji.Gear) return @@ -108,13 +109,10 @@ func (s *stdout) realTimePrintStart() { } func (s *stdout) liveResultPrint() { - p := s.result.DurationPercentile(95) - - fmt.Fprintf(out, "%s %s %s %s\n", + 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)), - cyan(fmt.Sprintf("%s p(95): %.5fs", emoji.HourglassNotDone, p)), ) } @@ -148,7 +146,7 @@ func (s *stdout) printDetails() { sort.Ints(keys) for _, k := range keys { - v := s.result.ItemReports[int16(k)].Report + v := s.result.ItemReports[int16(k)] if len(keys) > 1 { stepHeader := v.Name @@ -176,6 +174,20 @@ 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 name, percentile := range percentiles { + 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 { diff --git a/core/report/stdoutJson.go b/core/report/stdoutJson.go index bcc12aac..d4881a38 100644 --- a/core/report/stdoutJson.go +++ b/core/report/stdoutJson.go @@ -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]*ScenarioResult), + ItemReports: make(map[int16]*ScenarioItemReport), } + s.reportPercentiles = reportPercentiles + return } @@ -54,13 +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 { + jsonResult.ItemReports[key].Percentiles = map[string]float32{ + "p99": item.DurationPercentile(99), + "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 _, item := range s.result.ItemReports { - itemReport := item.Report + for _, item := range jsonResult.ItemReports { + itemReport := item durations := make(map[string]float32) for d, s := range itemReport.Durations { // Less precision for durations. @@ -70,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{} { @@ -108,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", diff --git a/core/report/stdoutJson_test.go b/core/report/stdoutJson_test.go index 052d4ce1..2c5e7228 100644 --- a/core/report/stdoutJson_test.go +++ b/core/report/stdoutJson_test.go @@ -22,6 +22,7 @@ package report import ( "bytes" "encoding/json" + "fmt" "reflect" "testing" "time" @@ -31,7 +32,7 @@ import ( func TestInitStdoutJson(t *testing.T) { sj := &stdoutJson{} - sj.Init() + sj.Init(false) if sj.doneChan == nil { t.Errorf("DoneChan should be initialized") @@ -123,14 +124,14 @@ func TestStdoutJsonStart(t *testing.T) { SuccessCount: 1, FailedCount: 1, AvgDuration: 90, - ItemReports: map[int16]*ScenarioResult{ - int16(1): {Report: itemReport1}, - int16(2): {Report: itemReport2}, + ItemReports: map[int16]*ScenarioItemReport{ + int16(1): itemReport1, + int16(2): itemReport2, }, } s := &stdoutJson{} - s.Init() + s.Init(false) responseChan := make(chan *types.Response, len(responses)) go s.Start(responseChan) @@ -186,9 +187,9 @@ func TestStdoutJsonOutput(t *testing.T) { SuccessCount: 9, FailedCount: 2, AvgDuration: 0.25637, - ItemReports: map[int16]*ScenarioResult{ - int16(1): {Report: itemReport1}, - int16(2): {Report: itemReport2}, + ItemReports: map[int16]*ScenarioItemReport{ + int16(1): itemReport1, + int16(2): itemReport2, }, } @@ -254,3 +255,7 @@ func TestStdoutJsonOutput(t *testing.T) { t.Errorf("Expected: %v, Found: %v", expectedOutput, output) } } + +var printJson = func(j []byte) { + fmt.Println(string(j)) +} diff --git a/core/report/stdout_test.go b/core/report/stdout_test.go index f413cfbf..cdaca9b1 100644 --- a/core/report/stdout_test.go +++ b/core/report/stdout_test.go @@ -91,7 +91,7 @@ func TestResult(t *testing.T) { func TestInit(t *testing.T) { s := &stdout{} - s.Init() + s.Init(false) if s.doneChan == nil { t.Errorf("DoneChan should be initialized") @@ -183,14 +183,14 @@ func TestStart(t *testing.T) { SuccessCount: 1, FailedCount: 1, AvgDuration: 90, - ItemReports: map[int16]*ScenarioResult{ - int16(1): {Report: itemReport1}, - int16(2): {Report: itemReport2}, + ItemReports: map[int16]*ScenarioItemReport{ + int16(1): itemReport1, + int16(2): itemReport2, }, } s := &stdout{} - s.Init() + s.Init(false) responseChan := make(chan *types.Response, len(responses)) go s.Start(responseChan) diff --git a/core/types/hammer.go b/core/types/hammer.go index 25443bdd..e194b257 100644 --- a/core/types/hammer.go +++ b/core/types/hammer.go @@ -77,6 +77,9 @@ type Hammer struct { // Destination of the results data. ReportDestination string + // Report percentiles + ReportPercentiles bool + // Dynamic field for extra parameters. Others map[string]interface{} } diff --git a/main.go b/main.go index 194abc41..d1d2bff5 100644 --- a/main.go +++ b/main.go @@ -74,6 +74,8 @@ var ( certPath = flag.String("cert_path", "", "A path to a certificate file (usually called 'cert.pem')") certKeyPath = flag.String("cert_key_path", "", "A path to a certificate key file (usually called 'key.pem')") + outputPercentile = flag.Bool("output-percentile", false, "Report percentile") + version = flag.Bool("version", false, "Prints version, git commit, built date (utc), go information and quit") ) @@ -190,6 +192,7 @@ var createHammerFromFlags = func() (h types.Hammer, err error) { Scenario: s, Proxy: p, ReportDestination: *output, + ReportPercentiles: *outputPercentile, } return } diff --git a/main_test.go b/main_test.go index f7d3ebce..06ccc0e3 100644 --- a/main_test.go +++ b/main_test.go @@ -63,6 +63,8 @@ func resetFlags() { *certPath = "" *certKeyPath = "" + + *outputPercentile = false } func TestDefaultFlagValues(t *testing.T) { @@ -114,6 +116,9 @@ func TestDefaultFlagValues(t *testing.T) { if *certKeyPath != "" { t.Errorf("TestDefaultFlagValues failed, expected %#v, found %#v", "", *certKeyPath) } + if *outputPercentile != false { + t.Errorf("TestDefaultFlagValues failed, expected %#v, found %#v", "", *outputPercentile) + } } func TestCreateHammer(t *testing.T) { From 6a39bcd09b02f5dbf1d8b1141a0df3e514bc38a9 Mon Sep 17 00:00:00 2001 From: Maxim Zhiburt Date: Wed, 19 Oct 2022 00:22:05 +0300 Subject: [PATCH 4/4] Fix ordering of percentiles in stdout Signed-off-by: Maxim Zhiburt --- core/report/stdout.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/core/report/stdout.go b/core/report/stdout.go index 3132cf46..d02f6084 100644 --- a/core/report/stdout.go +++ b/core/report/stdout.go @@ -176,15 +176,17 @@ func (s *stdout) printDetails() { 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), + 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 name, percentile := range percentiles { - fmt.Fprintf(w, " %s\t:%.4fs\n", name, percentile) + for _, val := range percentiles { + for name, percentile := range val { + fmt.Fprintf(w, " %s\t:%.4fs\n", name, percentile) + } } }