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