github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/speedtest-spinner.go (about) 1 // Copyright (c) 2015-2022 MinIO, Inc. 2 // 3 // This file is part of MinIO Object Storage stack 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package cmd 19 20 import ( 21 "fmt" 22 "sort" 23 "strings" 24 "time" 25 26 "github.com/charmbracelet/bubbles/spinner" 27 tea "github.com/charmbracelet/bubbletea" 28 "github.com/charmbracelet/lipgloss" 29 humanize "github.com/dustin/go-humanize" 30 "github.com/minio/madmin-go/v3" 31 "github.com/olekukonko/tablewriter" 32 ) 33 34 var whiteStyle = lipgloss.NewStyle(). 35 Bold(true). 36 Foreground(lipgloss.Color("#ffffff")) 37 38 type speedTestUI struct { 39 spinner spinner.Model 40 quitting bool 41 result PerfTestResult 42 } 43 44 // PerfTestType - The type of performance test (net/drive/object) 45 type PerfTestType byte 46 47 // Constants for performance test type 48 const ( 49 NetPerfTest PerfTestType = 1 << iota 50 DrivePerfTest 51 ObjectPerfTest 52 SiteReplicationPerfTest 53 ClientPerfTest 54 ) 55 56 // Name - returns name of the performance test 57 func (p PerfTestType) Name() string { 58 switch p { 59 case NetPerfTest: 60 return "NetPerf" 61 case DrivePerfTest: 62 return "DrivePerf" 63 case ObjectPerfTest: 64 return "ObjectPerf" 65 case SiteReplicationPerfTest: 66 return "SiteReplication" 67 case ClientPerfTest: 68 return "Client" 69 } 70 return "<unknown>" 71 } 72 73 // PerfTestResult - stores the result of a performance test 74 type PerfTestResult struct { 75 Type PerfTestType `json:"type"` 76 ObjectResult *madmin.SpeedTestResult `json:"object,omitempty"` 77 NetResult *madmin.NetperfResult `json:"network,omitempty"` 78 SiteReplicationResult *madmin.SiteNetPerfResult `json:"siteReplication,omitempty"` 79 ClientResult *madmin.ClientPerfResult `json:"client,omitempty"` 80 DriveResult []madmin.DriveSpeedTestResult `json:"drive,omitempty"` 81 Err string `json:"err,omitempty"` 82 Final bool `json:"final,omitempty"` 83 } 84 85 func initSpeedTestUI() *speedTestUI { 86 s := spinner.New() 87 s.Spinner = spinner.Points 88 s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) 89 return &speedTestUI{ 90 spinner: s, 91 } 92 } 93 94 func (m *speedTestUI) Init() tea.Cmd { 95 return m.spinner.Tick 96 } 97 98 func (m *speedTestUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 99 switch msg := msg.(type) { 100 case tea.KeyMsg: 101 switch msg.String() { 102 case "q", "esc", "ctrl+c": 103 m.quitting = true 104 return m, tea.Quit 105 default: 106 return m, nil 107 } 108 case PerfTestResult: 109 m.result = msg 110 if msg.Final { 111 m.quitting = true 112 return m, tea.Quit 113 } 114 return m, nil 115 default: 116 var cmd tea.Cmd 117 m.spinner, cmd = m.spinner.Update(msg) 118 return m, cmd 119 } 120 } 121 122 func (m *speedTestUI) View() string { 123 // Quit when there is an error 124 if m.result.Err != "" { 125 return fmt.Sprintf("\n%s: %s (Err: %s)\n", m.result.Type.Name(), crossTickCell, m.result.Err) 126 } 127 128 var s strings.Builder 129 130 // Set table header 131 table := tablewriter.NewWriter(&s) 132 table.SetAutoWrapText(false) 133 table.SetAutoFormatHeaders(true) 134 table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) 135 table.SetAlignment(tablewriter.ALIGN_LEFT) 136 table.SetCenterSeparator("") 137 table.SetColumnSeparator("") 138 table.SetRowSeparator("") 139 table.SetHeaderLine(false) 140 table.SetBorder(false) 141 table.SetTablePadding("\t") // pad with tabs 142 table.SetNoWhiteSpace(true) 143 144 ores := m.result.ObjectResult 145 nres := m.result.NetResult 146 sres := m.result.SiteReplicationResult 147 dres := m.result.DriveResult 148 cres := m.result.ClientResult 149 150 trailerIfGreaterThan := func(in string, max int) string { 151 if len(in) < max { 152 return in 153 } 154 return in[:max] + "..." 155 } 156 157 // Print the spinner 158 if !m.quitting { 159 s.WriteString(fmt.Sprintf("\n%s: %s\n\n", m.result.Type.Name(), m.spinner.View())) 160 } else { 161 s.WriteString(fmt.Sprintf("\n%s: %s\n\n", m.result.Type.Name(), m.spinner.Style.Render(tickCell))) 162 } 163 164 if ores != nil { 165 table.SetHeader([]string{"", "Throughput", "IOPS"}) 166 data := make([][]string, 2) 167 168 if ores.Version == "" { 169 data[0] = []string{ 170 "PUT", 171 whiteStyle.Render("-- KiB/sec"), 172 whiteStyle.Render("-- objs/sec"), 173 } 174 data[1] = []string{ 175 "GET", 176 whiteStyle.Render("-- KiB/sec"), 177 whiteStyle.Render("-- objs/sec"), 178 } 179 } else { 180 data[0] = []string{ 181 "PUT", 182 whiteStyle.Render(humanize.IBytes(ores.PUTStats.ThroughputPerSec) + "/s"), 183 whiteStyle.Render(humanize.Comma(int64(ores.PUTStats.ObjectsPerSec)) + " objs/s"), 184 } 185 data[1] = []string{ 186 "GET", 187 whiteStyle.Render(humanize.IBytes(ores.GETStats.ThroughputPerSec) + "/s"), 188 whiteStyle.Render(humanize.Comma(int64(ores.GETStats.ObjectsPerSec)) + " objs/s"), 189 } 190 } 191 table.AppendBulk(data) 192 table.Render() 193 194 if m.quitting { 195 s.WriteString("\n" + objectTestShortResult(ores)) 196 if globalPerfTestVerbose { 197 s.WriteString("\n\n") 198 s.WriteString(objectTestVerboseResult(ores)) 199 } 200 s.WriteString("\n") 201 } 202 } else if nres != nil { 203 table.SetHeader([]string{"Node", "RX", "TX", ""}) 204 data := make([][]string, 0, len(nres.NodeResults)) 205 206 if len(nres.NodeResults) == 0 { 207 data = append(data, []string{ 208 "...", 209 whiteStyle.Render("-- MiB/s"), 210 whiteStyle.Render("-- MiB/s"), 211 "", 212 }) 213 } else { 214 for _, nodeResult := range nres.NodeResults { 215 nodeErr := "" 216 if nodeResult.Error != "" { 217 nodeErr = "Err: " + nodeResult.Error 218 } 219 data = append(data, []string{ 220 trailerIfGreaterThan(nodeResult.Endpoint, 64), 221 whiteStyle.Render(humanize.IBytes(uint64(nodeResult.RX))) + "/s", 222 whiteStyle.Render(humanize.IBytes(uint64(nodeResult.TX))) + "/s", 223 nodeErr, 224 }) 225 } 226 } 227 228 sort.Slice(data, func(i, j int) bool { 229 return data[i][0] < data[j][0] 230 }) 231 232 table.AppendBulk(data) 233 table.Render() 234 } else if sres != nil { 235 table.SetHeader([]string{"Endpoint", "RX", "TX", ""}) 236 data := make([][]string, 0, len(sres.NodeResults)) 237 if len(sres.NodeResults) == 0 { 238 data = append(data, []string{ 239 "...", 240 whiteStyle.Render("-- MiB"), 241 whiteStyle.Render("-- MiB"), 242 "", 243 }) 244 } else { 245 for _, nodeResult := range sres.NodeResults { 246 if nodeResult.Error != "" { 247 data = append(data, []string{ 248 trailerIfGreaterThan(nodeResult.Endpoint, 64), 249 crossTickCell, 250 crossTickCell, 251 "Err: " + nodeResult.Error, 252 }) 253 } else { 254 dataItem := []string{} 255 dataError := "" 256 // show endpoint 257 dataItem = append(dataItem, trailerIfGreaterThan(nodeResult.Endpoint, 64)) 258 // show RX 259 if uint64(nodeResult.RXTotalDuration.Seconds()) == 0 { 260 dataError += "- RXTotalDuration are zero " 261 dataItem = append(dataItem, crossTickCell) 262 } else { 263 dataItem = append(dataItem, whiteStyle.Render(humanize.IBytes(nodeResult.RX/uint64(nodeResult.RXTotalDuration.Seconds())))+"/s") 264 } 265 // show TX 266 if uint64(nodeResult.TXTotalDuration.Seconds()) == 0 { 267 dataError += "- TXTotalDuration are zero" 268 dataItem = append(dataItem, crossTickCell) 269 } else { 270 dataItem = append(dataItem, whiteStyle.Render(humanize.IBytes(nodeResult.TX/uint64(nodeResult.TXTotalDuration.Seconds())))+"/s") 271 } 272 // show message 273 dataItem = append(dataItem, dataError) 274 data = append(data, dataItem) 275 } 276 } 277 } 278 279 sort.Slice(data, func(i, j int) bool { 280 return data[i][0] < data[j][0] 281 }) 282 283 table.AppendBulk(data) 284 table.Render() 285 } else if dres != nil { 286 table.SetHeader([]string{"Node", "Path", "Read", "Write", ""}) 287 data := make([][]string, 0, len(dres)) 288 289 if len(dres) == 0 { 290 data = append(data, []string{ 291 "...", 292 "...", 293 whiteStyle.Render("-- KiB/s"), 294 whiteStyle.Render("-- KiB/s"), 295 "", 296 }) 297 } else { 298 for _, driveResult := range dres { 299 for _, result := range driveResult.DrivePerf { 300 if result.Error != "" { 301 data = append(data, []string{ 302 trailerIfGreaterThan(driveResult.Endpoint, 64), 303 result.Path, 304 crossTickCell, 305 crossTickCell, 306 "Err: " + result.Error, 307 }) 308 } else { 309 data = append(data, []string{ 310 trailerIfGreaterThan(driveResult.Endpoint, 64), 311 result.Path, 312 whiteStyle.Render(humanize.IBytes(result.ReadThroughput)) + "/s", 313 whiteStyle.Render(humanize.IBytes(result.WriteThroughput)) + "/s", 314 "", 315 }) 316 } 317 } 318 } 319 } 320 table.AppendBulk(data) 321 table.Render() 322 } else if cres != nil { 323 table.SetHeader([]string{"Endpoint", "Tx"}) 324 data := make([][]string, 0, 2) 325 tx := uint64(0) 326 if cres.TimeSpent > 0 { 327 tx = uint64(float64(cres.BytesSend) / time.Duration(cres.TimeSpent).Seconds()) 328 } 329 if tx == 0 { 330 data = append(data, []string{ 331 "...", 332 whiteStyle.Render("-- KiB/s"), 333 "", 334 }) 335 } else { 336 data = append(data, []string{ 337 cres.Endpoint, 338 whiteStyle.Render(humanize.IBytes(tx)) + "/s", 339 cres.Error, 340 }) 341 } 342 table.AppendBulk(data) 343 table.Render() 344 } 345 346 return s.String() 347 }