diff --git a/README.md b/README.md index 1cb05667..3b51e782 100644 --- a/README.md +++ b/README.md @@ -135,13 +135,15 @@ report command: -output string Output file (default "stdout") -type string - Report type to generate [text, json, hist[buckets], hdrplot] (default "text") + Report type to generate [text, json, hist[buckets], expoHist[hi,bucketNo], hdrplot] (default "text") examples: echo "GET http://localhost/" | vegeta attack -duration=5s | tee results.bin | vegeta report vegeta report -type=json results.bin > metrics.json cat results.bin | vegeta plot > plot.html cat results.bin | vegeta report -type="hist[0,100ms,200ms,300ms]" + cat results.bin | vegeta report -type="geoHist[800ms, 4]" + cat results.bin | vegeta report -type="expoHist[800ms, 60]" ``` #### `-cpus` @@ -519,6 +521,67 @@ Bucket # % Histogram [6ms, +Inf] 4771 25.93% ################### ``` +#### `report -type=expoHist` +Computes and prints a text based histogram for the given charaterisitc of buckets, the trait presented as exponential sequence. +Each bucket upper bound is non-inclusive. + +```console +cat testPlot.bin | vegeta report -type="expoHist[1s, 50]" +Bucket # % Histogram +[0s, 1.148153ms] 0 0.00% +[1.148153ms, 1.318256ms] 0 0.00% +[1.318256ms, 1.513561ms] 0 0.00% +[1.513561ms, 1.7378ms] 0 0.00% +[1.7378ms, 1.995262ms] 0 0.00% +[1.995262ms, 2.290867ms] 0 0.00% +[2.290867ms, 2.630267ms] 0 0.00% +[2.630267ms, 3.019951ms] 0 0.00% +[3.019951ms, 3.467368ms] 0 0.00% +[3.467368ms, 3.981071ms] 0 0.00% +[3.981071ms, 4.570881ms] 0 0.00% +[4.570881ms, 5.248074ms] 0 0.00% +[5.248074ms, 6.025595ms] 0 0.00% +[6.025595ms, 6.918309ms] 0 0.00% +[6.918309ms, 7.943282ms] 0 0.00% +[7.943282ms, 9.120108ms] 0 0.00% +[9.120108ms, 10.471285ms] 0 0.00% +[10.471285ms, 12.022644ms] 0 0.00% +[12.022644ms, 13.803842ms] 0 0.00% +[13.803842ms, 15.848931ms] 0 0.00% +[15.848931ms, 18.197008ms] 0 0.00% +[18.197008ms, 20.892961ms] 0 0.00% +[20.892961ms, 23.988329ms] 0 0.00% +[23.988329ms, 27.542287ms] 0 0.00% +[27.542287ms, 31.622776ms] 0 0.00% +[31.622776ms, 36.307805ms] 0 0.00% +[36.307805ms, 41.686938ms] 7 3.50% ## +[41.686938ms, 47.863009ms] 132 66.00% ################################################# +[47.863009ms, 54.954087ms] 19 9.50% ####### +[54.954087ms, 63.095734ms] 15 7.50% ##### +[63.095734ms, 72.443596ms] 6 3.00% ## +[72.443596ms, 83.176377ms] 8 4.00% ### +[83.176377ms, 95.499258ms] 3 1.50% # +[95.499258ms, 109.647819ms] 0 0.00% +[109.647819ms, 125.892541ms] 2 1.00% +[125.892541ms, 144.543977ms] 3 1.50% # +[144.543977ms, 165.95869ms] 2 1.00% +[165.95869ms, 190.546071ms] 2 1.00% +[190.546071ms, 218.776162ms] 1 0.50% +[218.776162ms, 251.188643ms] 0 0.00% +[251.188643ms, 288.40315ms] 0 0.00% +[288.40315ms, 331.131121ms] 0 0.00% +[331.131121ms, 380.189396ms] 0 0.00% +[380.189396ms, 436.515832ms] 0 0.00% +[436.515832ms, 501.187233ms] 0 0.00% +[501.187233ms, 575.439937ms] 0 0.00% +[575.439937ms, 660.693448ms] 0 0.00% +[660.693448ms, 758.577575ms] 0 0.00% +[758.577575ms, 870.963589ms] 0 0.00% +[870.963589ms, 999.999999ms] 0 0.00% +[999.999999ms, 1s] 0 0.00% +[1s, +Inf] 0 0.00% +``` + #### `report -type=hdrplot` Writes out results in a format plottable by https://hdrhistogram.github.io/HdrHistogram/plotFiles.html. diff --git a/lib/histogram.go b/lib/histogram.go index acb546f1..7e8f28c1 100644 --- a/lib/histogram.go +++ b/lib/histogram.go @@ -3,6 +3,8 @@ package vegeta import ( "bytes" "fmt" + "math" + "strconv" "strings" "time" ) @@ -84,3 +86,59 @@ func (bs *Buckets) UnmarshalText(value []byte) error { } return nil } + +// UnmarshalExpoSeqText generate buckets in the way implementing exponential sequence +// , whose args would be [hi, bucketAmount], in which hi represents upper bound and bucketAmount represents required number of buckets +// Find n in the formula: A1+r^n= An, or n=e^(ln(hi)/bucketAmount), where variable n is the interval base in this case. +func (bs *Buckets) UnmarshalExpoSeqText(value []byte) error { + if len(value) < 2 || value[0] != '[' || value[len(value)-1] != ']' { + return fmt.Errorf("bad buckets: %s", value) + } + + args := strings.Split(string(value[1:len(value)-1]), ",") + if len(args) != 2 { + return fmt.Errorf("bad buckets: %s", value) + } + + hi, power, err := parseDuration(strings.TrimSpace(args[0])) + if err != nil { + return err + } + if hi == 0 { + return fmt.Errorf("bad buckets, upper boundary should not be 0: %s", value) + } + + bucketAmt, err := strconv.ParseFloat(strings.TrimSpace(args[1]), 64) + if err != nil { + return err + } + + interval := math.Exp(math.Log(float64(hi)) / float64(bucketAmt)) + + for i := float64(0); float64(i) < float64(hi); i *= interval { + // add a default range of [0-Buckets[0]) if needed + if i == 0 { + *bs = append(*bs, 0) + i += interval + } + *bs = append(*bs, time.Duration(i*math.Pow10(power))) + } + *bs = append(*bs, time.Duration(hi*math.Pow10(power))) + + return nil +} + +func parseDuration(arg string) (float64, int, error) { + floatArg, err := time.ParseDuration(strings.TrimSpace(arg)) + if err != nil { + return 0, 0, err + } + + c := float64(floatArg) + power := 0 + for c > 1000 { + c = c / 1000 + power += 3 + } + return c, power, nil +} diff --git a/lib/histogram_test.go b/lib/histogram_test.go index 8bdd366f..6ba5980b 100644 --- a/lib/histogram_test.go +++ b/lib/histogram_test.go @@ -67,3 +67,33 @@ func TestBuckets_UnmarshalText(t *testing.T) { } } } + +func TestBuckets_UnmarshalExpoSeqText(t *testing.T) { + t.Parallel() + for value, want := range map[string]string{ + "": "bad buckets: ", + " ": "bad buckets: ", + "{0, 2}": "bad buckets: {0, 2}", + "[100ms]": "bad buckets: [100ms]", + "[2ms, 8ms, 3]": "bad buckets: [2ms, 8ms, 3]", + "[]": "bad buckets: []", + "[0, 2]": "bad buckets, upper boundary should not be 0: [0, 2]", + "[2ms, 8ms]": `strconv.ParseFloat: parsing "8ms": invalid syntax`, + } { + if got := (&Buckets{}).UnmarshalExpoSeqText([]byte(value)).Error(); got != want { + t.Errorf("got: %v, want: %v", got, want) + } + } + + for value, want := range map[string]Buckets{ + "[8s, 3]": {0, 2 * time.Second, 4 * time.Second, 8 * time.Second}, + "[8s,3]": {0, 2 * time.Second, 4 * time.Second, 8 * time.Second}, + } { + var got Buckets + if err := got.UnmarshalExpoSeqText([]byte(value)); err != nil { + t.Errorf("got: %v, want: %v", got, want) + } else if !reflect.DeepEqual(got, want) { + t.Errorf("got: %v, want: %v", got, want) + } + } +} diff --git a/report.go b/report.go index 94074b7f..1d0ff42d 100644 --- a/report.go +++ b/report.go @@ -44,7 +44,7 @@ func reportCmd() command { buckets := fs.String("buckets", "", "Histogram buckets, e.g.: \"[0,1ms,10ms]\"") fs.Usage = func() { - fmt.Fprintln(os.Stderr, reportUsage) + fmt.Fprint(os.Stderr, reportUsage) } return command{fs, func(args []string) error { @@ -81,7 +81,7 @@ func report(files []string, typ, output string, every time.Duration, bucketsStr switch typ { case "plot": - return fmt.Errorf("The plot reporter has been deprecated and succeeded by the vegeta plot command") + return fmt.Errorf("the plot reporter has been deprecated and succeeded by the vegeta plot command") case "text": var m vegeta.Metrics rep, report = vegeta.NewTextReporter(&m), &m @@ -111,6 +111,18 @@ func report(files []string, typ, output string, every time.Duration, bucketsStr return err } rep, report = vegeta.NewHistogramReporter(&hist), &hist + case strings.HasPrefix(typ, "expoHist"): + var hist vegeta.Histogram + if bucketsStr == "" { // Old way + if len(typ) < 10 { + return fmt.Errorf("bad buckets: '%s'", typ[8:]) + } + bucketsStr = typ[8:] + } + if err := hist.Buckets.UnmarshalExpoSeqText([]byte(bucketsStr)); err != nil { + return err + } + rep, report = vegeta.NewHistogramReporter(&hist), &hist default: return fmt.Errorf("unknown report type: %q", typ) }