diff --git a/lib/attack.go b/lib/attack.go index 4cbc1cc..84d6d80 100644 --- a/lib/attack.go +++ b/lib/attack.go @@ -4,6 +4,7 @@ import ( "errors" "io/ioutil" "net/http" + "sort" "time" ) @@ -14,7 +15,7 @@ func Attack(targets Targets, rate uint64, duration time.Duration) []Result { total := rate * uint64(duration.Seconds()) hits := make(chan *http.Request, total) res := make(chan Result, total) - results := make([]Result, total) + results := make(Results, total) // Scatter go drill(rate, hits, res) for i := 0; i < cap(hits); i++ { @@ -27,6 +28,8 @@ func Attack(targets Targets, rate uint64, duration time.Duration) []Result { } close(res) + sort.Sort(results) + return results } @@ -40,6 +43,13 @@ type Result struct { Error error } +// Results is a slice of Result defined only to be sortable with sort.Interface +type Results []Result + +func (r Results) Len() int { return len(r) } +func (r Results) Less(i, j int) bool { return r[i].Timestamp.Before(r[j].Timestamp) } +func (r Results) Swap(i, j int) { r[i], r[j] = r[j], r[i] } + // drill loops over the passed reqs channel and executes each request. // It is throttled to the rate specified. func drill(rate uint64, reqs chan *http.Request, res chan Result) { diff --git a/lib/reporter.go b/lib/reporter.go deleted file mode 100644 index 63af051..0000000 --- a/lib/reporter.go +++ /dev/null @@ -1,11 +0,0 @@ -package vegeta - -import ( - "io" -) - -// Reporter represents any reporter of the results of the test -type Reporter interface { - Report(io.Writer) error - Add(res *Result) -} diff --git a/lib/reporters.go b/lib/reporters.go new file mode 100644 index 0000000..72feac9 --- /dev/null +++ b/lib/reporters.go @@ -0,0 +1,102 @@ +package vegeta + +import ( + "code.google.com/p/plotinum/plot" + "code.google.com/p/plotinum/plotter" + "code.google.com/p/plotinum/plotutil" + "code.google.com/p/plotinum/vg" + "code.google.com/p/plotinum/vg/vgsvg" + "fmt" + "io" + "text/tabwriter" + "time" +) + +// Reporter represents any function which takes a slice of Results and +// generates a report, writing it to an io.Writer and returning an error +// in case of failure +type Reporter func([]Result, io.Writer) error + +// ReportText computes and prints some metrics out of results +// as formatted text. Metrics include avg time per request, success ratio, +// total number of request, avg bytes in and avg bytes out. +func ReportText(results []Result, out io.Writer) error { + totalRequests := float64(len(results)) + totalTime := time.Duration(0) + totalBytesOut := uint64(0) + totalBytesIn := uint64(0) + totalSuccess := uint64(0) + histogram := map[uint64]uint64{} + errors := map[string]struct{}{} + + for _, res := range results { + histogram[res.Code]++ + totalTime += res.Timing + totalBytesOut += res.BytesOut + totalBytesIn += res.BytesIn + if res.Code >= 200 && res.Code < 300 { + totalSuccess++ + } + if res.Error != nil { + errors[res.Error.Error()] = struct{}{} + } + } + + avgTime := time.Duration(float64(totalTime) / totalRequests) + avgBytesOut := float64(totalBytesOut) / totalRequests + avgBytesIn := float64(totalBytesIn) / totalRequests + avgSuccess := float64(totalSuccess) / totalRequests + + w := tabwriter.NewWriter(out, 0, 8, 2, '\t', tabwriter.StripEscape) + fmt.Fprintf(w, "Time(avg)\tRequests\tSuccess\tBytes(rx/tx)\n") + fmt.Fprintf(w, "%s\t%d\t%.2f%%\t%.2f/%.2f\n", avgTime, int(totalRequests), avgSuccess*100, avgBytesIn, avgBytesOut) + + fmt.Fprintf(w, "\nCount:\t") + for _, count := range histogram { + fmt.Fprintf(w, "%d\t", count) + } + fmt.Fprintf(w, "\nStatus:\t") + for code, _ := range histogram { + fmt.Fprintf(w, "%d\t", code) + } + + fmt.Fprintln(w, "\n\nError Set:") + for err, _ := range errors { + fmt.Fprintln(w, err) + } + + return w.Flush() +} + +// ReportTimingsPlot builds up a plot of the response times of the requests +// in SVG format and writes it to out +func ReportTimingsPlot(results []Result, out io.Writer) error { + p, err := plot.New() + if err != nil { + return err + } + pts := make(plotter.XYs, len(results)) + for i := 0; i < len(pts); i++ { + pts[i].X = results[i].Timestamp.Sub(results[0].Timestamp).Seconds() + pts[i].Y = results[i].Timing.Seconds() * 1000 + } + + line, err := plotter.NewLine(pts) + if err != nil { + return err + } + line.Color = plotutil.Color(1) + + p.Add(line) + p.X.Padding = vg.Length(3.0) + p.X.Label.Text = "Time elapsed" + p.Y.Padding = vg.Length(3.0) + p.Y.Label.Text = "Latency (ms)" + + w, h := vg.Millimeters(float64(len(results))), vg.Centimeters(12.0) + canvas := vgsvg.New(w, h) + p.Draw(plot.MakeDrawArea(canvas)) + + _, err = canvas.WriteTo(out) + return err +} diff --git a/lib/text_reporter.go b/lib/text_reporter.go deleted file mode 100644 index 11120d6..0000000 --- a/lib/text_reporter.go +++ /dev/null @@ -1,76 +0,0 @@ -package vegeta - -import ( - "fmt" - "io" - "text/tabwriter" - "time" -) - -// TextReporter prints the test results as text -// Metrics incude avg time per request, success ratio, -// total number of request, avg bytes in and avg bytes out -type TextReporter struct { - results []*Result -} - -// NewTextReporter initializes a TextReporter with n responses -func NewTextReporter() *TextReporter { - return &TextReporter{results: make([]*Result, 0)} -} - -// Report computes and writes the report to out. -// It returns an error in case of failure. -func (r *TextReporter) Report(out io.Writer) error { - totalRequests := len(r.results) - totalTime := time.Duration(0) - totalBytesOut := uint64(0) - totalBytesIn := uint64(0) - totalSuccess := uint64(0) - histogram := map[uint64]uint64{} - errors := map[string]struct{}{} - - for _, res := range r.results { - histogram[res.Code]++ - totalTime += res.Timing - totalBytesOut += res.BytesOut - totalBytesIn += res.BytesIn - if res.Code >= 200 && res.Code < 300 { - totalSuccess++ - } - if res.Error != nil { - errors[res.Error.Error()] = struct{}{} - } - } - - avgTime := time.Duration(float64(totalTime) / float64(totalRequests)) - avgBytesOut := float64(totalBytesOut) / float64(totalRequests) - avgBytesIn := float64(totalBytesIn) / float64(totalRequests) - avgSuccess := float64(totalSuccess) / float64(totalRequests) - - w := tabwriter.NewWriter(out, 0, 8, 2, '\t', tabwriter.StripEscape) - fmt.Fprintf(w, "Time(avg)\tRequests\tSuccess\tBytes(rx/tx)\n") - fmt.Fprintf(w, "%s\t%d\t%.2f%%\t%.2f/%.2f\n", avgTime, totalRequests, avgSuccess*100, avgBytesOut, avgBytesIn) - - fmt.Fprintf(w, "\nCount:\t") - for _, count := range histogram { - fmt.Fprintf(w, "%d\t", count) - } - fmt.Fprintf(w, "\nStatus:\t") - for code, _ := range histogram { - fmt.Fprintf(w, "%d\t", code) - } - - fmt.Fprintln(w, "\n\nError Set:") - for err, _ := range errors { - fmt.Fprintln(w, err) - } - - return w.Flush() -} - -// Add adds a response to be used in the report -// Order of arrival is not relevant for this reporter -func (r *TextReporter) Add(res *Result) { - r.results = append(r.results, res) -} diff --git a/lib/timings_plot_reporter.go b/lib/timings_plot_reporter.go deleted file mode 100644 index e71596c..0000000 --- a/lib/timings_plot_reporter.go +++ /dev/null @@ -1,90 +0,0 @@ -package vegeta - -import ( - "code.google.com/p/plotinum/plot" - "code.google.com/p/plotinum/plotter" - "code.google.com/p/plotinum/plotutil" - "code.google.com/p/plotinum/vg" - "code.google.com/p/plotinum/vg/vgsvg" - "container/list" - "io" - "time" -) - -type TimingsPlotReporter struct { - results *list.List -} - -// NewTimingsPlotReporter initializes a TimingsPlotReporter -func NewTimingsPlotReporter() *TimingsPlotReporter { - return &TimingsPlotReporter{results: list.New()} -} - -// Add inserts response to be used in the report, sorted by timestamp. -func (r *TimingsPlotReporter) Add(res *Result) { - // Empty list - if r.results.Len() == 0 { - r.results.PushFront(res) - return - } - // Happened after all others - if last := r.results.Back().Value.(*Result); last.Timestamp.Before(res.Timestamp) { - r.results.PushBack(res) - return - } - // Happened before all others - if first := r.results.Front().Value.(*Result); first.Timestamp.After(res.Timestamp) { - r.results.PushFront(res) - return - } - // O(n) worst case insertion time - for e := r.results.Front(); e != nil; e = e.Next() { - needle := e.Value.(*Result) - if res.Timestamp.Before(needle.Timestamp) { - r.results.InsertBefore(res, e) - return - } - } -} - -// Report builds up a plot of the response times of the requests -// in SVG format and writes it to out -func (r *TimingsPlotReporter) Report(out io.Writer) error { - timestamps := make([]time.Time, 0) - timings := make([]time.Duration, 0) - - for e := r.results.Front(); e != nil; e = e.Next() { - r := e.Value.(*Result) - timestamps = append(timestamps, r.Timestamp) - timings = append(timings, r.Timing) - } - - p, err := plot.New() - if err != nil { - return err - } - pts := make(plotter.XYs, len(timestamps)) - for i := 0; i < len(pts); i++ { - pts[i].X = timestamps[i].Sub(timestamps[0]).Seconds() - pts[i].Y = timings[i].Seconds() * 1000 - } - - line, err := plotter.NewLine(pts) - if err != nil { - return err - } - line.Color = plotutil.Color(1) - - p.Add(line) - p.X.Padding = vg.Length(3.0) - p.X.Label.Text = "Time elapsed" - p.Y.Padding = vg.Length(3.0) - p.Y.Label.Text = "Latency (ms)" - - w, h := vg.Millimeters(float64(len(timestamps))), vg.Centimeters(12.0) - canvas := vgsvg.New(w, h) - p.Draw(plot.MakeDrawArea(canvas)) - - _, err = canvas.WriteTo(out) - return err -} diff --git a/main.go b/main.go index b230757..eccd08e 100644 --- a/main.go +++ b/main.go @@ -71,12 +71,12 @@ func run(rate uint64, duration time.Duration, targetsf, ordering, reporter, outp var rep vegeta.Reporter switch reporter { case "text": - rep = vegeta.NewTextReporter() + rep = vegeta.ReportText case "plot:timings": - rep = vegeta.NewTimingsPlotReporter() + rep = vegeta.ReportTimingsPlot default: log.Println("Reporter provided is not supported. Using text") - rep = vegeta.NewTextReporter() + rep = vegeta.ReportText } targets, err := vegeta.NewTargetsFromFile(targetsf) @@ -94,12 +94,10 @@ func run(rate uint64, duration time.Duration, targetsf, ordering, reporter, outp } log.Printf("Vegeta is attacking %d targets in %s order for %s...\n", len(targets), ordering, duration) - for _, result := range vegeta.Attack(targets, rate, duration) { - rep.Add(&result) - } + results := vegeta.Attack(targets, rate, duration) log.Println("Done!") log.Printf("Writing report to '%s'...", output) - if err = rep.Report(out); err != nil { + if err = rep(results, out); err != nil { return fmt.Errorf(errReportingPrefix+"%s", err) } return nil