1package gui
2
3import (
4	"context"
5	"fmt"
6	"log"
7	"sort"
8	"sync"
9	"time"
10
11	"github.com/mum4k/termdash/cell"
12	"github.com/mum4k/termdash/widgets/linechart"
13	"github.com/mum4k/termdash/widgets/text"
14	"go.uber.org/atomic"
15
16	"github.com/nakabonne/ali/attacker"
17	"github.com/nakabonne/ali/storage"
18)
19
20// drawer periodically queries data points from the storage and passes them to the termdash API.
21type drawer struct {
22	// specify the data points range to show on the UI
23	queryRange     time.Duration
24	redrawInterval time.Duration
25	widgets        *widgets
26	gridOpts       *gridOpts
27
28	metricsCh chan *attacker.Metrics
29
30	// aims to avoid to perform multiple `appendChartValues`.
31	chartDrawing *atomic.Bool
32
33	mu      sync.RWMutex
34	metrics *attacker.Metrics
35	storage storage.Reader
36}
37
38// redrawCharts sets the values held by itself as chart values, at the specified interval as redrawInterval.
39func (d *drawer) redrawCharts(ctx context.Context) {
40	ticker := time.NewTicker(d.redrawInterval)
41	defer ticker.Stop()
42
43	d.chartDrawing.Store(true)
44L:
45	for {
46		select {
47		case <-ctx.Done():
48			break L
49		case <-ticker.C:
50			end := time.Now()
51			start := end.Add(-d.queryRange)
52
53			latencies, err := d.storage.Select(storage.LatencyMetricName, start, end)
54			if err != nil {
55				log.Printf("failed to select latency data points: %v\n", err)
56			}
57			d.widgets.latencyChart.Series("latency", latencies,
58				linechart.SeriesCellOpts(cell.FgColor(cell.ColorNumber(87))),
59				linechart.SeriesXLabels(map[int]string{
60					0: "req",
61				}),
62			)
63
64			p50, err := d.storage.Select(storage.P50MetricName, start, end)
65			if err != nil {
66				log.Printf("failed to select p50 data points: %v\n", err)
67			}
68			d.widgets.percentilesChart.Series("p50", p50,
69				linechart.SeriesCellOpts(d.widgets.p50Legend.cellOpts...),
70			)
71
72			p90, err := d.storage.Select(storage.P90MetricName, start, end)
73			if err != nil {
74				log.Printf("failed to select p90 data points: %v\n", err)
75			}
76			d.widgets.percentilesChart.Series("p90", p90,
77				linechart.SeriesCellOpts(d.widgets.p90Legend.cellOpts...),
78			)
79
80			p95, err := d.storage.Select(storage.P95MetricName, start, end)
81			if err != nil {
82				log.Printf("failed to select p95 data points: %v\n", err)
83			}
84			d.widgets.percentilesChart.Series("p95", p95,
85				linechart.SeriesCellOpts(d.widgets.p95Legend.cellOpts...),
86			)
87
88			p99, err := d.storage.Select(storage.P99MetricName, start, end)
89			if err != nil {
90				log.Printf("failed to select p99 data points: %v\n", err)
91			}
92			d.widgets.percentilesChart.Series("p99", p99,
93				linechart.SeriesCellOpts(d.widgets.p99Legend.cellOpts...),
94			)
95		}
96	}
97	d.chartDrawing.Store(false)
98}
99
100func (d *drawer) redrawGauge(ctx context.Context, duration time.Duration) {
101	ticker := time.NewTicker(d.redrawInterval)
102	defer ticker.Stop()
103
104	totalTime := float64(duration)
105
106	d.widgets.progressGauge.Percent(0)
107	for start := time.Now(); ; {
108		select {
109		case <-ctx.Done():
110			return
111		case <-ticker.C:
112			passed := float64(time.Since(start))
113			percent := int(passed / totalTime * 100)
114			// as time.Duration is the unit of nanoseconds
115			// small duration can exceed 100 on slow machines
116			if percent > 100 {
117				continue
118			}
119			d.widgets.progressGauge.Percent(percent)
120		}
121	}
122}
123
124const (
125	latenciesTextFormat = `Total: %v
126Mean: %v
127P50: %v
128P90: %v
129P95: %v
130P99: %v
131Max: %v
132Min: %v`
133
134	bytesTextFormat = `In:
135  Total: %v
136  Mean: %v
137Out:
138  Total: %v
139  Mean: %v`
140
141	othersTextFormat = `Duration: %v
142Wait: %v
143Requests: %d
144Rate: %f
145Throughput: %f
146Success: %f
147Earliest: %v
148Latest: %v
149End: %v`
150)
151
152// redrawMetrics writes the metrics held by itself into the widgets, at the specified interval as redrawInterval.
153func (d *drawer) redrawMetrics(ctx context.Context) {
154	ticker := time.NewTicker(d.redrawInterval)
155	defer ticker.Stop()
156
157	for {
158		select {
159		case <-ctx.Done():
160			return
161		case <-ticker.C:
162			d.mu.RLock()
163			m := *d.metrics
164			d.mu.RUnlock()
165
166			d.widgets.latenciesText.Write(
167				fmt.Sprintf(latenciesTextFormat,
168					m.Latencies.Total,
169					m.Latencies.Mean,
170					m.Latencies.P50,
171					m.Latencies.P90,
172					m.Latencies.P95,
173					m.Latencies.P99,
174					m.Latencies.Max,
175					m.Latencies.Min,
176				), text.WriteReplace())
177
178			d.widgets.bytesText.Write(
179				fmt.Sprintf(bytesTextFormat,
180					m.BytesIn.Total,
181					m.BytesIn.Mean,
182					m.BytesOut.Total,
183					m.BytesOut.Mean,
184				), text.WriteReplace())
185
186			d.widgets.othersText.Write(fmt.Sprintf(othersTextFormat,
187				m.Duration,
188				m.Wait,
189				m.Requests,
190				m.Rate,
191				m.Throughput,
192				m.Success,
193				m.Earliest.Format(time.RFC3339),
194				m.Latest.Format(time.RFC3339),
195				m.End.Format(time.RFC3339),
196			), text.WriteReplace())
197
198			// To guarantee that status codes are in order
199			// taking the slice of keys and sorting them.
200			codesText := ""
201			var keys []string
202			for k := range m.StatusCodes {
203				keys = append(keys, k)
204			}
205			sort.Strings(keys)
206			for _, k := range keys {
207				codesText += fmt.Sprintf(`%q: %d
208`, k, m.StatusCodes[k])
209			}
210			d.widgets.statusCodesText.Write(codesText, text.WriteReplace())
211
212			errorsText := ""
213			for _, e := range m.Errors {
214				errorsText += fmt.Sprintf(`- %s
215`, e)
216			}
217			d.widgets.errorsText.Write(errorsText, text.WriteReplace())
218		}
219	}
220}
221
222func (d *drawer) updateMetrics(ctx context.Context) {
223	for {
224		select {
225		case <-ctx.Done():
226			return
227		case metrics := <-d.metricsCh:
228			if metrics == nil {
229				continue
230			}
231			d.mu.Lock()
232			d.metrics = metrics
233			d.mu.Unlock()
234		}
235	}
236}
237