From 7fa23dd5c3fd0797c36f18c4428975d3dfbbf228 Mon Sep 17 00:00:00 2001 From: nakabonne Date: Tue, 15 Sep 2020 08:52:49 +0900 Subject: [PATCH] Add progress donut --- README.md | 18 +++++++++++++++++- attacker/attacker.go | 12 ++++++------ gui/drawer.go | 25 +++++++++++++++++++++++-- gui/gui.go | 22 ++++++++++++++++------ gui/widgets.go | 13 +++++++++++++ 5 files changed, 75 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 7beadd0..83e1afd 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,25 @@ Another load testing tool, inspired by [vegeta](https://github.com/tsenart/veget Executables are available through the [releases page](https://github.com/nakabonne/ali/releases). +## Usage + ## Features #### Mouse support +#### Visualizes the attack progress + [gif animations] -## Alternatives \ No newline at end of file +## Built with +- [mum4k/termdash](https://github.com/mum4k/termdash/wiki/Termbox-API) + - [nsf/termbox-go](https://github.com/nsf/termbox-go) +- [vegeta](https://github.com/tsenart/vegeta) + + +## LoadMap +- Plot Bytes In and Bytes Out (Press `Ctrl-w` to switch between charts) +- Plot status codes +- Better UI + +## Alternatives + diff --git a/attacker/attacker.go b/attacker/attacker.go index 5fe26a0..0fccfb7 100644 --- a/attacker/attacker.go +++ b/attacker/attacker.go @@ -9,9 +9,9 @@ import ( ) const ( - defaultRate = 50 - defaultDuration = 10 * time.Second - defaultMethod = http.MethodGet + DefaultRate = 50 + DefaultDuration = 10 * time.Second + DefaultMethod = http.MethodGet ) type Attacker interface { @@ -44,13 +44,13 @@ func Attack(ctx context.Context, target string, resCh chan *Result, opts Options return nil } if opts.Rate == 0 { - opts.Rate = defaultRate + opts.Rate = DefaultRate } if opts.Duration == 0 { - opts.Duration = defaultDuration + opts.Duration = DefaultDuration } if opts.Method == "" { - opts.Method = defaultMethod + opts.Method = DefaultMethod } if opts.Attacker == nil { opts.Attacker = vegeta.NewAttacker() diff --git a/gui/drawer.go b/gui/drawer.go index 216d2cf..388bfb7 100644 --- a/gui/drawer.go +++ b/gui/drawer.go @@ -13,7 +13,8 @@ import ( type drawer struct { widgets *widgets - chartsCh chan *attacker.Result + chartCh chan *attacker.Result + donutCh chan bool reportCh chan string // aims to avoid to perform multiple `redrawChart`. @@ -31,10 +32,12 @@ L: select { case <-ctx.Done(): break L - case res := <-d.chartsCh: + case res := <-d.chartCh: if res.End { + d.donutCh <- true break L } + d.donutCh <- false values = append(values, float64(res.Latency/time.Millisecond)) d.widgets.latencyChart.Series("latency", values, linechart.SeriesCellOpts(cell.FgColor(cell.ColorNumber(87))), @@ -47,6 +50,24 @@ L: d.chartDrawing = false } +func (d *drawer) redrawDonut(ctx context.Context, maxSize int) { + var count float64 + size := float64(maxSize) + d.widgets.progressDonut.Percent(0) + for { + select { + case <-ctx.Done(): + return + case end := <-d.donutCh: + if end { + return + } + count++ + d.widgets.progressDonut.Percent(int(count / size * 100)) + } + } +} + func (d *drawer) redrawReport(ctx context.Context) { for { select { diff --git a/gui/gui.go b/gui/gui.go index d72f737..97cc9a8 100644 --- a/gui/gui.go +++ b/gui/gui.go @@ -50,7 +50,8 @@ func Run() error { d := &drawer{ widgets: w, - chartsCh: make(chan *attacker.Result), + chartCh: make(chan *attacker.Result), + donutCh: make(chan bool), reportCh: make(chan string), } go d.redrawReport(ctx) @@ -76,7 +77,8 @@ func gridLayout(w *widgets) ([]container.Option, error) { grid.ColWidthPerc(49, grid.Widget(w.bodyInput, container.Border(linestyle.None))), ), ), - grid.ColWidthPerc(69, grid.Widget(w.reportText, container.Border(linestyle.Light), container.BorderTitle("Report"))), + grid.ColWidthPerc(35, grid.Widget(w.reportText, container.Border(linestyle.Light), container.BorderTitle("Report"))), + grid.ColWidthPerc(34, grid.Widget(w.progressDonut, container.Border(linestyle.Light), container.BorderTitle("Progress"))), ) raw3 := grid.RowHeightFixed(1, grid.ColWidthFixed(100, grid.Widget(w.navi, container.Border(linestyle.Light))), @@ -112,6 +114,7 @@ func attack(ctx context.Context, dr *drawer) { rate int duration time.Duration method string + body string err error ) target = dr.widgets.urlInput.Read() @@ -119,14 +122,18 @@ func attack(ctx context.Context, dr *drawer) { dr.reportCh <- fmt.Sprintf("Bad URL: %v", err) return } - if s := dr.widgets.rateLimitInput.Read(); s != "" { + if s := dr.widgets.rateLimitInput.Read(); s == "" { + rate = attacker.DefaultRate + } else { rate, err = strconv.Atoi(s) if err != nil { dr.reportCh <- fmt.Sprintf("Given rate limit %q isn't integer: %v", s, err) return } } - if s := dr.widgets.durationInput.Read(); s != "" { + if s := dr.widgets.durationInput.Read(); s == "" { + duration = attacker.DefaultDuration + } else { duration, err = time.ParseDuration(s) if err != nil { dr.reportCh <- fmt.Sprintf("Unparseable duration %q: %v", s, err) @@ -139,13 +146,16 @@ func attack(ctx context.Context, dr *drawer) { return } } + body = dr.widgets.bodyInput.Read() requestNum := rate * int(duration/time.Second) + // To pre-allocate, run redrawChart on a per-attack basis. go dr.redrawChart(ctx, requestNum) + go dr.redrawDonut(ctx, requestNum) go func(ctx context.Context, d *drawer, t string, r int, du time.Duration) { - metrics := attacker.Attack(ctx, t, d.chartsCh, attacker.Options{Rate: r, Duration: du, Method: method}) + metrics := attacker.Attack(ctx, t, d.chartCh, attacker.Options{Rate: r, Duration: du, Method: method, Body: []byte(body)}) d.reportCh <- metrics.String() - d.chartsCh <- &attacker.Result{End: true} + d.chartCh <- &attacker.Result{End: true} }(ctx, dr, target, rate, duration) } diff --git a/gui/widgets.go b/gui/widgets.go index 674a743..30d7f19 100644 --- a/gui/widgets.go +++ b/gui/widgets.go @@ -4,6 +4,7 @@ import ( "time" "github.com/mum4k/termdash/cell" + "github.com/mum4k/termdash/widgets/donut" "github.com/mum4k/termdash/widgets/linechart" "github.com/mum4k/termdash/widgets/text" "github.com/mum4k/termdash/widgets/textinput" @@ -22,6 +23,7 @@ type widgets struct { bodyInput *textinput.TextInput latencyChart *linechart.LineChart reportText *text.Text + progressDonut *donut.Donut navi *text.Text } @@ -58,6 +60,10 @@ func newWidgets() (*widgets, error) { if err != nil { return nil, err } + progressDonut, err := newDonut() + if err != nil { + return nil, err + } return &widgets{ urlInput: urlInput, rateLimitInput: rateLimitInput, @@ -66,6 +72,7 @@ func newWidgets() (*widgets, error) { bodyInput: bodyInput, latencyChart: latencyChart, reportText: reportText, + progressDonut: progressDonut, navi: navi, }, nil } @@ -96,3 +103,9 @@ func newTextInput(label, placeHolder string, cells int) (*textinput.TextInput, e textinput.PlaceHolder(placeHolder), ) } + +func newDonut() (*donut.Donut, error) { + return donut.New( + donut.CellOpts(cell.FgColor(cell.ColorGreen)), + ) +}