From bab3d9f89a26d1c22eca331c6e8351b2fa9997ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Senart?= Date: Mon, 9 Sep 2013 00:56:38 +0100 Subject: [PATCH 1/6] Publicize vegeta.Result --- lib/attack.go | 46 ++++++++++++++++++------------------ lib/reporter.go | 2 +- lib/text_reporter.go | 26 ++++++++++---------- lib/timings_plot_reporter.go | 34 +++++++++++++------------- 4 files changed, 54 insertions(+), 54 deletions(-) diff --git a/lib/attack.go b/lib/attack.go index 1730f66..971cbe7 100644 --- a/lib/attack.go +++ b/lib/attack.go @@ -13,31 +13,31 @@ import ( func Attack(targets Targets, rate uint64, duration time.Duration, rep Reporter) { hits := make(chan *http.Request, rate*uint64((duration).Seconds())) defer close(hits) - responses := make(chan *result, cap(hits)) - defer close(responses) - go drill(rate, hits, responses) // Attack! + results := make(chan *Result, cap(hits)) + defer close(results) + go drill(rate, hits, results) // Attack! for i := 0; i < cap(hits); i++ { hits <- targets[i%len(targets)] } // Wait for all requests to finish - for i := 0; i < cap(responses); i++ { - rep.add(<-responses) + for i := 0; i < cap(results); i++ { + rep.add(<-results) } } -// result represents the metrics we want out of an http.Response -type result struct { - code uint64 - timestamp time.Time - timing time.Duration - bytesOut uint64 - bytesIn uint64 - err error +// Result represents the metrics we want out of an http.Response +type Result struct { + Code uint64 + Timestamp time.Time + Timing time.Duration + BytesOut uint64 + BytesIn uint64 + Error error } // 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) { +func drill(rate uint64, reqs chan *http.Request, res chan *Result) { throttle := time.Tick(time.Duration(1e9 / rate)) for req := range reqs { <-throttle @@ -48,19 +48,19 @@ func drill(rate uint64, reqs chan *http.Request, res chan *result) { // hit executes the passed http.Request and puts a generated *result into res. // Both transport errors and unsucessfull requests (non {2xx,3xx}) are // considered errors which are set in the Response. -func hit(req *http.Request, res chan *result) { +func hit(req *http.Request, res chan *Result) { began := time.Now() r, err := http.DefaultClient.Do(req) - result := &result{ - timestamp: began, - timing: time.Since(began), - bytesOut: uint64(req.ContentLength), - err: err, + result := &Result{ + Timestamp: began, + Timing: time.Since(began), + BytesOut: uint64(req.ContentLength), + Error: err, } if err == nil { - result.bytesIn, result.code = uint64(r.ContentLength), uint64(r.StatusCode) - if body, err := ioutil.ReadAll(r.Body); err != nil && (result.code < 200 || result.code >= 300) { - result.err = errors.New(string(body)) + result.BytesIn, result.Code = uint64(r.ContentLength), uint64(r.StatusCode) + if body, err := ioutil.ReadAll(r.Body); err != nil && (result.Code < 200 || result.Code >= 300) { + result.Error = errors.New(string(body)) } } diff --git a/lib/reporter.go b/lib/reporter.go index 1a26694..533cc1b 100644 --- a/lib/reporter.go +++ b/lib/reporter.go @@ -7,5 +7,5 @@ import ( // Reporter represents any reporter of the results of the test type Reporter interface { Report(io.Writer) error - add(res *result) + add(res *Result) } diff --git a/lib/text_reporter.go b/lib/text_reporter.go index 7faf129..898e4eb 100644 --- a/lib/text_reporter.go +++ b/lib/text_reporter.go @@ -11,18 +11,18 @@ import ( // Metrics incude avg time per request, success ratio, // total number of request, avg bytes in and avg bytes out type TextReporter struct { - responses []*result + results []*Result } // NewTextReporter initializes a TextReporter with n responses func NewTextReporter() *TextReporter { - return &TextReporter{responses: make([]*result, 0)} + 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.responses) + totalRequests := len(r.results) totalTime := time.Duration(0) totalBytesOut := uint64(0) totalBytesIn := uint64(0) @@ -30,16 +30,16 @@ func (r *TextReporter) Report(out io.Writer) error { histogram := map[uint64]uint64{} errors := map[string]struct{}{} - for _, res := range r.responses { - histogram[res.code]++ - totalTime += res.timing - totalBytesOut += res.bytesOut - totalBytesIn += res.bytesIn - if res.code >= 200 && res.code < 300 { + 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.err != nil { - errors[res.err.Error()] = struct{}{} + if res.Error != nil { + errors[res.Error.Error()] = struct{}{} } } @@ -71,6 +71,6 @@ func (r *TextReporter) Report(out io.Writer) error { // 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.responses = append(r.responses, res) +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 index 31f8028..5f0744d 100644 --- a/lib/timings_plot_reporter.go +++ b/lib/timings_plot_reporter.go @@ -12,36 +12,36 @@ import ( ) type TimingsPlotReporter struct { - responses *list.List + results *list.List } // NewTimingsPlotReporter initializes a TimingsPlotReporter func NewTimingsPlotReporter() *TimingsPlotReporter { - return &TimingsPlotReporter{responses: list.New()} + return &TimingsPlotReporter{results: list.New()} } // add inserts response to be used in the report, sorted by timestamp. -func (r *TimingsPlotReporter) add(res *result) { +func (r *TimingsPlotReporter) add(res *Result) { // Empty list - if r.responses.Len() == 0 { - r.responses.PushFront(res) + if r.results.Len() == 0 { + r.results.PushFront(res) return } // Happened after all others - if last := r.responses.Back().Value.(*result); last.timestamp.Before(res.timestamp) { - r.responses.PushBack(res) + if last := r.results.Back().Value.(*Result); last.Timestamp.Before(res.Timestamp) { + r.results.PushBack(res) return } // Happened before all others - if first := r.responses.Front().Value.(*result); first.timestamp.After(res.timestamp) { - r.responses.PushFront(res) + 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.responses.Front(); e != nil; e = e.Next() { - needle := e.Value.(*result) - if res.timestamp.Before(needle.timestamp) { - r.responses.InsertBefore(res, e) + 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 } } @@ -53,10 +53,10 @@ func (r *TimingsPlotReporter) Report(out io.Writer) error { timestamps := make([]time.Time, 0) timings := make([]time.Duration, 0) - for e := r.responses.Front(); e != nil; e = e.Next() { - r := e.Value.(*result) - timestamps = append(timestamps, r.timestamp) - timings = append(timings, r.timing) + 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() From f689362188365f2416be38dde77e8dd50cfab23f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Senart?= Date: Mon, 9 Sep 2013 01:49:57 +0100 Subject: [PATCH 2/6] Decouple Reporters from Attack function This changeset breaks the API of Attack in order to decouple Reporters and the Attack function. Attack now returns a slice with non-deterministic order of Results which one can use on the calling code with or without Reporters, hence making it much more useful on a library usage setting. These developments could be of interest to issue #11 which was closed in the past. --- README.md | 12 +++++++++--- lib/attack.go | 36 ++++++++++++++++++++---------------- lib/attack_test.go | 5 +---- lib/reporter.go | 2 +- lib/text_reporter.go | 4 ++-- lib/timings_plot_reporter.go | 4 ++-- main.go | 5 +++-- 7 files changed, 38 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 70390b9..02025ff 100644 --- a/README.md +++ b/README.md @@ -97,17 +97,23 @@ import ( vegeta "github.com/tsenart/vegeta/lib" "time" "os" + "fmt" ) func main() { targets, _ := vegeta.NewTargets([]string{"GET http://localhost:9100/"}) rate := uint64(100) // per second duration := 4 * time.Second - reporter := vegeta.NewTextReporter() - vegeta.Attack(targets, rate, duration, reporter) + results := vegeta.Attack(targets, rate, duration) - reporter.Report(os.Stdout) + totalTime := time.Duration(0) + for _, result := range results { + totalTime += result.Timing + } + meanTime := time.Duration(float64(totalTime) / float64(len(results))) + + fmt.Printf("Average timing: %s", meanTime) } ``` diff --git a/lib/attack.go b/lib/attack.go index 971cbe7..4cbc1cc 100644 --- a/lib/attack.go +++ b/lib/attack.go @@ -9,20 +9,25 @@ import ( // Attack hits the passed Targets (http.Requests) at the rate specified for // duration time and then waits for all the requests to come back. -// The results of the attack are put into the rep Reporter. -func Attack(targets Targets, rate uint64, duration time.Duration, rep Reporter) { - hits := make(chan *http.Request, rate*uint64((duration).Seconds())) - defer close(hits) - results := make(chan *Result, cap(hits)) - defer close(results) - go drill(rate, hits, results) // Attack! +// The results of the attack are put into a slice which is returned. +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) + // Scatter + go drill(rate, hits, res) for i := 0; i < cap(hits); i++ { hits <- targets[i%len(targets)] } - // Wait for all requests to finish - for i := 0; i < cap(results); i++ { - rep.add(<-results) + close(hits) + // Gather + for i := 0; i < cap(res); i++ { + results[i] = <-res } + close(res) + + return results } // Result represents the metrics we want out of an http.Response @@ -37,7 +42,7 @@ type Result struct { // 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) { +func drill(rate uint64, reqs chan *http.Request, res chan Result) { throttle := time.Tick(time.Duration(1e9 / rate)) for req := range reqs { <-throttle @@ -45,13 +50,13 @@ func drill(rate uint64, reqs chan *http.Request, res chan *Result) { } } -// hit executes the passed http.Request and puts a generated *result into res. +// hit executes the passed http.Request and puts the result into results. // Both transport errors and unsucessfull requests (non {2xx,3xx}) are -// considered errors which are set in the Response. -func hit(req *http.Request, res chan *Result) { +// considered errors. +func hit(req *http.Request, res chan Result) { began := time.Now() r, err := http.DefaultClient.Do(req) - result := &Result{ + result := Result{ Timestamp: began, Timing: time.Since(began), BytesOut: uint64(req.ContentLength), @@ -63,6 +68,5 @@ func hit(req *http.Request, res chan *Result) { result.Error = errors.New(string(body)) } } - res <- result } diff --git a/lib/attack_test.go b/lib/attack_test.go index 34cdc93..c5b1ce9 100644 --- a/lib/attack_test.go +++ b/lib/attack_test.go @@ -3,7 +3,6 @@ package vegeta import ( "net/http" "net/http/httptest" - "os" "sync/atomic" "testing" "time" @@ -18,10 +17,8 @@ func TestAttackRate(t *testing.T) { ) request, _ := http.NewRequest("GET", server.URL, nil) rate := uint64(5000) - rep := NewTextReporter() - Attack(Targets{request}, rate, 1*time.Second, rep) + Attack(Targets{request}, rate, 1*time.Second) if hits := atomic.LoadUint64(&hitCount); hits != rate { - rep.Report(os.Stdout) t.Fatalf("Wrong number of hits: want %d, got %d\n", rate, hits) } } diff --git a/lib/reporter.go b/lib/reporter.go index 533cc1b..63af051 100644 --- a/lib/reporter.go +++ b/lib/reporter.go @@ -7,5 +7,5 @@ import ( // Reporter represents any reporter of the results of the test type Reporter interface { Report(io.Writer) error - add(res *Result) + Add(res *Result) } diff --git a/lib/text_reporter.go b/lib/text_reporter.go index 898e4eb..11120d6 100644 --- a/lib/text_reporter.go +++ b/lib/text_reporter.go @@ -69,8 +69,8 @@ func (r *TextReporter) Report(out io.Writer) error { return w.Flush() } -// add adds a response to be used in the report +// 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) { +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 index 5f0744d..e71596c 100644 --- a/lib/timings_plot_reporter.go +++ b/lib/timings_plot_reporter.go @@ -20,8 +20,8 @@ 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) { +// 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) diff --git a/main.go b/main.go index a1cba4e..b230757 100644 --- a/main.go +++ b/main.go @@ -94,9 +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) - vegeta.Attack(targets, rate, duration, rep) + for _, result := range vegeta.Attack(targets, rate, duration) { + rep.Add(&result) + } log.Println("Done!") - log.Printf("Writing report to '%s'...", output) if err = rep.Report(out); err != nil { return fmt.Errorf(errReportingPrefix+"%s", err) From 93310995c74cf3d1042eba4ea60cb85c20bf0c31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Senart?= Date: Mon, 9 Sep 2013 03:03:13 +0100 Subject: [PATCH 3/6] Simplify Reporter objects to be simple functions --- lib/attack.go | 12 ++++- lib/reporter.go | 11 ---- lib/reporters.go | 102 +++++++++++++++++++++++++++++++++++ lib/text_reporter.go | 76 -------------------------- lib/timings_plot_reporter.go | 90 ------------------------------- main.go | 12 ++--- 6 files changed, 118 insertions(+), 185 deletions(-) delete mode 100644 lib/reporter.go create mode 100644 lib/reporters.go delete mode 100644 lib/text_reporter.go delete mode 100644 lib/timings_plot_reporter.go 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 From 67fb276a76c14f51339cf31e760147da84786117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Senart?= Date: Mon, 9 Sep 2013 03:04:33 +0100 Subject: [PATCH 4/6] Use actual body length for BytesIn --- lib/attack.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/attack.go b/lib/attack.go index 84d6d80..dc15a0c 100644 --- a/lib/attack.go +++ b/lib/attack.go @@ -73,9 +73,13 @@ func hit(req *http.Request, res chan Result) { Error: err, } if err == nil { - result.BytesIn, result.Code = uint64(r.ContentLength), uint64(r.StatusCode) - if body, err := ioutil.ReadAll(r.Body); err != nil && (result.Code < 200 || result.Code >= 300) { - result.Error = errors.New(string(body)) + result.Code = uint64(r.StatusCode) + if body, err := ioutil.ReadAll(r.Body); err != nil { + if result.Code < 200 || result.Code >= 300 { + result.Error = errors.New(string(body)) + } + } else { + result.BytesIn = uint64(len(body)) } } res <- result From 15ad005f5a391d5e8ad88cf29918ac38df9b4312 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Senart?= Date: Mon, 9 Sep 2013 13:15:37 +0100 Subject: [PATCH 5/6] Go lint warning fixes --- lib/reporters.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/reporters.go b/lib/reporters.go index 72feac9..e4db44d 100644 --- a/lib/reporters.go +++ b/lib/reporters.go @@ -56,12 +56,12 @@ func ReportText(results []Result, out io.Writer) error { fmt.Fprintf(w, "%d\t", count) } fmt.Fprintf(w, "\nStatus:\t") - for code, _ := range histogram { + for code := range histogram { fmt.Fprintf(w, "%d\t", code) } fmt.Fprintln(w, "\n\nError Set:") - for err, _ := range errors { + for err := range errors { fmt.Fprintln(w, err) } From 2147e1840bbff5571614d468178d070eeba2b5b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Senart?= Date: Mon, 9 Sep 2013 13:25:29 +0100 Subject: [PATCH 6/6] uint16 for HTTP Status codes --- lib/attack.go | 4 ++-- lib/reporters.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/attack.go b/lib/attack.go index dc15a0c..98d56e3 100644 --- a/lib/attack.go +++ b/lib/attack.go @@ -35,7 +35,7 @@ func Attack(targets Targets, rate uint64, duration time.Duration) []Result { // Result represents the metrics we want out of an http.Response type Result struct { - Code uint64 + Code uint16 Timestamp time.Time Timing time.Duration BytesOut uint64 @@ -73,7 +73,7 @@ func hit(req *http.Request, res chan Result) { Error: err, } if err == nil { - result.Code = uint64(r.StatusCode) + result.Code = uint16(r.StatusCode) if body, err := ioutil.ReadAll(r.Body); err != nil { if result.Code < 200 || result.Code >= 300 { result.Error = errors.New(string(body)) diff --git a/lib/reporters.go b/lib/reporters.go index e4db44d..73b7890 100644 --- a/lib/reporters.go +++ b/lib/reporters.go @@ -26,7 +26,7 @@ func ReportText(results []Result, out io.Writer) error { totalBytesOut := uint64(0) totalBytesIn := uint64(0) totalSuccess := uint64(0) - histogram := map[uint64]uint64{} + histogram := map[uint16]uint64{} errors := map[string]struct{}{} for _, res := range results {