diff --git a/config/config.go b/config/config.go index c5f71bc..f435985 100644 --- a/config/config.go +++ b/config/config.go @@ -6,6 +6,7 @@ import ( "os" "time" + . "github.com/aykhans/dodo/types" "github.com/aykhans/dodo/utils" "github.com/jedib0t/go-pretty/v6/table" ) @@ -92,12 +93,12 @@ func (config *RequestConfig) GetMaxConns(minConns uint) uint { } type Config struct { - Method string `json:"method" validate:"http_method"` // custom validations: http_method - URL string `json:"url" validate:"http_url,required"` - Timeout uint32 `json:"timeout" validate:"gte=1,lte=100000"` - DodosCount uint `json:"dodos_count" validate:"gte=1"` - RequestCount uint `json:"request_count" validation_name:"request-count" validate:"gte=1"` - NoProxyCheck utils.Option[bool] `json:"no_proxy_check"` + Method string `json:"method" validate:"http_method"` // custom validations: http_method + URL string `json:"url" validate:"http_url,required"` + Timeout uint32 `json:"timeout" validate:"gte=1,lte=100000"` + DodosCount uint `json:"dodos_count" validate:"gte=1"` + RequestCount uint `json:"request_count" validation_name:"request-count" validate:"gte=1"` + NoProxyCheck Option[bool] `json:"no_proxy_check"` } func NewConfig( @@ -105,10 +106,10 @@ func NewConfig( timeout uint32, dodosCount uint, requestCount uint, - noProxyCheck utils.Option[bool], + noProxyCheck Option[bool], ) *Config { if noProxyCheck == nil { - noProxyCheck = utils.NewNoneOption[bool]() + noProxyCheck = NewNoneOption[bool]() } return &Config{ @@ -155,7 +156,7 @@ func (config *Config) SetDefaults() { config.RequestCount = DefaultRequestCount } if config.NoProxyCheck.IsNone() { - config.NoProxyCheck = utils.NewOption(false) + config.NoProxyCheck = NewOption(false) } } diff --git a/readers/cli.go b/readers/cli.go index b16b556..6ed52a4 100644 --- a/readers/cli.go +++ b/readers/cli.go @@ -5,6 +5,7 @@ import ( "github.com/aykhans/dodo/config" "github.com/aykhans/dodo/custom_errors" + . "github.com/aykhans/dodo/types" "github.com/aykhans/dodo/utils" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -61,7 +62,7 @@ func CLIConfigReader() (*config.CLIConfig, error) { case "timeout": cliConfig.Timeout = timeout case "no-proxy-check": - cliConfig.NoProxyCheck = utils.NewOption(noProxyCheck) + cliConfig.NoProxyCheck = NewOption(noProxyCheck) } }) if returnNil { diff --git a/requests/response.go b/requests/response.go index 7955bfe..68df76a 100644 --- a/requests/response.go +++ b/requests/response.go @@ -5,7 +5,7 @@ import ( "os" "time" - "github.com/aykhans/dodo/utils" + . "github.com/aykhans/dodo/types" "github.com/jedib0t/go-pretty/v6/table" ) @@ -18,24 +18,32 @@ type Response struct { type Responses []*Response // Print prints the responses in a tabular format, including information such as -// response count, minimum time, maximum time, and average time. +// response count, minimum time, maximum time, average time, and latency percentiles. func (respones Responses) Print() { - var ( - totalMinDuration time.Duration = respones[0].Time - totalMaxDuration time.Duration = respones[0].Time - totalDuration time.Duration - totalCount int = len(respones) - ) - mergedResponses := make(map[string][]time.Duration) + total := struct { + Count int + Min time.Duration + Max time.Duration + Sum time.Duration + P90 time.Duration + P95 time.Duration + P99 time.Duration + }{ + Count: len(respones), + Min: respones[0].Time, + Max: respones[0].Time, + } + mergedResponses := make(map[string]Durations) + var allDurations Durations for _, response := range respones { - if response.Time < totalMinDuration { - totalMinDuration = response.Time + if response.Time < total.Min { + total.Min = response.Time } - if response.Time > totalMaxDuration { - totalMaxDuration = response.Time + if response.Time > total.Max { + total.Max = response.Time } - totalDuration += response.Time + total.Sum += response.Time if response.Error != nil { mergedResponses[response.Error.Error()] = append( @@ -48,38 +56,60 @@ func (respones Responses) Print() { response.Time, ) } + allDurations = append(allDurations, response.Time) } + allDurations.Sort() + allDurationsLenAsFloat := float64(len(allDurations) - 1) + total.P90 = allDurations[int(0.90*allDurationsLenAsFloat)] + total.P95 = allDurations[int(0.95*allDurationsLenAsFloat)] + total.P99 = allDurations[int(0.99*allDurationsLenAsFloat)] t := table.NewWriter() t.SetOutputMirror(os.Stdout) t.SetStyle(table.StyleLight) t.SetColumnConfigs([]table.ColumnConfig{ - {Number: 1, WidthMax: 80}, + {Number: 1, WidthMax: 40}, }) - t.AppendHeader(table.Row{ "Response", "Count", "Min Time", "Max Time", "Average Time", + "P90", + "P95", + "P99", }) + for key, durations := range mergedResponses { + durations.Sort() + durationsLen := len(durations) + durationsLenAsFloat := float64(durationsLen - 1) + t.AppendRow(table.Row{ key, - len(durations), - utils.MinDuration(durations...), - utils.MaxDuration(durations...), - utils.AvgDuration(durations...), + durationsLen, + durations.First(), + durations.Last(), + durations.Avg(), + durations[int(0.90*durationsLenAsFloat)], + durations[int(0.95*durationsLenAsFloat)], + durations[int(0.99*durationsLenAsFloat)], }) t.AppendSeparator() } - t.AppendRow(table.Row{ - "Total", - totalCount, - totalMinDuration, - totalMaxDuration, - totalDuration / time.Duration(totalCount), - }) + + if len(mergedResponses) > 1 { + t.AppendRow(table.Row{ + "Total", + total.Count, + total.Min, + total.Max, + total.Sum / time.Duration(total.Count), // Average + total.P90, + total.P95, + total.P99, + }) + } t.Render() } diff --git a/types/durations.go b/types/durations.go new file mode 100644 index 0000000..18fcf9c --- /dev/null +++ b/types/durations.go @@ -0,0 +1,41 @@ +package types + +import ( + "sort" + "time" +) + +type Durations []time.Duration + +func (d Durations) Sort(ascending ...bool) { + // If ascending is provided and is false, sort in descending order + if len(ascending) > 0 && ascending[0] == false { + sort.Slice(d, func(i, j int) bool { + return d[i] > d[j] + }) + } else { // Otherwise, sort in ascending order + sort.Slice(d, func(i, j int) bool { + return d[i] < d[j] + }) + } +} + +func (d Durations) First() *time.Duration { + return &d[0] +} + +func (d Durations) Last() *time.Duration { + return &d[len(d)-1] +} + +func (d Durations) Sum() time.Duration { + sum := time.Duration(0) + for _, duration := range d { + sum += duration + } + return sum +} + +func (d Durations) Avg() time.Duration { + return d.Sum() / time.Duration(len(d)) +} diff --git a/utils/types.go b/types/option.go similarity index 99% rename from utils/types.go rename to types/option.go index 92f65b9..55dc6ee 100644 --- a/utils/types.go +++ b/types/option.go @@ -1,4 +1,4 @@ -package utils +package types import ( "encoding/json" diff --git a/utils/time.go b/utils/time.go deleted file mode 100644 index a502833..0000000 --- a/utils/time.go +++ /dev/null @@ -1,31 +0,0 @@ -package utils - -import "time" - -func MinDuration(durations ...time.Duration) time.Duration { - min := durations[0] - for _, d := range durations { - if d < min { - min = d - } - } - return min -} - -func MaxDuration(durations ...time.Duration) time.Duration { - max := durations[0] - for _, d := range durations { - if d > max { - max = d - } - } - return max -} - -func AvgDuration(durations ...time.Duration) time.Duration { - total := time.Duration(0) - for _, d := range durations { - total += d - } - return total / time.Duration(len(durations)) -}