github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/top-drives-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 "cmp" 22 "fmt" 23 "sort" 24 "strings" 25 26 "github.com/charmbracelet/bubbles/spinner" 27 tea "github.com/charmbracelet/bubbletea" 28 "github.com/charmbracelet/lipgloss" 29 "github.com/minio/madmin-go/v3" 30 "github.com/olekukonko/tablewriter" 31 ) 32 33 type topDriveUI struct { 34 spinner spinner.Model 35 quitting bool 36 37 sortBy drivesSorter 38 sortAsc bool 39 count int 40 pool, maxPool int 41 42 drivesInfo map[string]madmin.Disk 43 44 prevTopMap map[string]madmin.DiskIOStats 45 currTopMap map[string]madmin.DiskIOStats 46 } 47 48 type topDriveResult struct { 49 final bool 50 diskName string 51 stats madmin.DiskIOStats 52 } 53 54 func initTopDriveUI(disks []madmin.Disk, count int) *topDriveUI { 55 maxPool := 0 56 drivesInfo := make(map[string]madmin.Disk) 57 for i := range disks { 58 drivesInfo[disks[i].Endpoint] = disks[i] 59 if disks[i].PoolIndex > maxPool { 60 maxPool = disks[i].PoolIndex 61 } 62 } 63 64 s := spinner.New() 65 s.Spinner = spinner.Points 66 s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) 67 return &topDriveUI{ 68 count: count, 69 sortBy: sortByName, 70 pool: 0, 71 maxPool: maxPool, 72 drivesInfo: drivesInfo, 73 spinner: s, 74 prevTopMap: make(map[string]madmin.DiskIOStats), 75 currTopMap: make(map[string]madmin.DiskIOStats), 76 } 77 } 78 79 func (m *topDriveUI) Init() tea.Cmd { 80 return m.spinner.Tick 81 } 82 83 func (m *topDriveUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 84 switch msg := msg.(type) { 85 case tea.KeyMsg: 86 switch msg.String() { 87 case "ctrl+c", "q", "esc": 88 m.quitting = true 89 return m, tea.Quit 90 case "right": 91 m.pool++ 92 if m.pool >= m.maxPool { 93 m.pool = m.maxPool 94 } 95 case "left": 96 m.pool-- 97 if m.pool < 0 { 98 m.pool = 0 99 } 100 case "u": 101 m.sortBy = sortByUsed 102 case "t": 103 m.sortBy = sortByTps 104 case "r": 105 m.sortBy = sortByRead 106 case "w": 107 m.sortBy = sortByWrite 108 case "d": 109 m.sortBy = sortByDiscard 110 case "a": 111 m.sortBy = sortByAwait 112 case "U": 113 m.sortBy = sortByUtil 114 case "o", "O": 115 m.sortAsc = !m.sortAsc 116 } 117 118 return m, nil 119 case topDriveResult: 120 m.prevTopMap[msg.diskName] = m.currTopMap[msg.diskName] 121 m.currTopMap[msg.diskName] = msg.stats 122 if msg.final { 123 m.quitting = true 124 return m, tea.Quit 125 } 126 return m, nil 127 128 case spinner.TickMsg: 129 var cmd tea.Cmd 130 m.spinner, cmd = m.spinner.Update(msg) 131 return m, cmd 132 default: 133 return m, nil 134 } 135 } 136 137 type driveIOStat struct { 138 endpoint string 139 util float64 140 await float64 141 readMBs float64 142 writeMBs float64 143 discardMBs float64 144 tps uint64 145 used uint64 146 } 147 148 func generateDriveStat(disk madmin.Disk, curr, prev madmin.DiskIOStats, interval uint64) (d driveIOStat) { 149 if disk.TotalSpace == 0 { 150 return d 151 } 152 d.endpoint = disk.Endpoint 153 d.used = 100 * disk.UsedSpace / disk.TotalSpace 154 d.util = 100 * float64(curr.TotalTicks-prev.TotalTicks) / float64(interval) 155 currTotalIOs := curr.ReadIOs + curr.WriteIOs + curr.DiscardIOs 156 prevTotalIOs := prev.ReadIOs + prev.WriteIOs + prev.DiscardIOs 157 totalTicksDiff := curr.ReadTicks - prev.ReadTicks + curr.WriteTicks - prev.WriteTicks + curr.DiscardTicks - prev.DiscardTicks 158 if currTotalIOs > prevTotalIOs { 159 d.tps = currTotalIOs - prevTotalIOs 160 d.await = float64(totalTicksDiff) / float64(currTotalIOs-prevTotalIOs) 161 } 162 intervalInSec := float64(interval / 1000) 163 d.readMBs = float64(curr.ReadSectors-prev.ReadSectors) / (2048 * intervalInSec) 164 d.writeMBs = float64(curr.WriteSectors-prev.WriteSectors) / (2048 * intervalInSec) 165 d.discardMBs = float64(curr.DiscardSectors-prev.DiscardSectors) / (2048 * intervalInSec) 166 return d 167 } 168 169 type drivesSorter int 170 171 const ( 172 sortByName drivesSorter = iota 173 sortByUsed 174 sortByAwait 175 sortByUtil 176 sortByRead 177 sortByWrite 178 sortByDiscard 179 sortByTps 180 ) 181 182 func (s drivesSorter) String() string { 183 switch s { 184 case sortByName: 185 return "name" 186 case sortByUsed: 187 return "used" 188 case sortByAwait: 189 return "await" 190 case sortByUtil: 191 return "util" 192 case sortByRead: 193 return "read" 194 case sortByWrite: 195 return "write" 196 case sortByDiscard: 197 return "discard" 198 case sortByTps: 199 return "tps" 200 } 201 return "unknown" 202 } 203 204 func sortDriveIOStat(sortBy drivesSorter, asc bool, data []driveIOStat) { 205 sort.SliceStable(data, func(i, j int) bool { 206 c := 0 207 switch sortBy { 208 case sortByName: 209 c = cmp.Compare(data[i].endpoint, data[j].endpoint) 210 case sortByUsed: 211 c = cmp.Compare(data[i].used, data[j].used) 212 case sortByAwait: 213 c = cmp.Compare(data[i].await, data[j].await) 214 case sortByUtil: 215 c = cmp.Compare(data[i].util, data[j].util) 216 case sortByRead: 217 c = cmp.Compare(data[i].readMBs, data[j].readMBs) 218 case sortByWrite: 219 c = cmp.Compare(data[i].writeMBs, data[j].writeMBs) 220 case sortByDiscard: 221 c = cmp.Compare(data[i].discardMBs, data[j].discardMBs) 222 case sortByTps: 223 c = cmp.Compare(data[i].tps, data[j].tps) 224 } 225 226 less := c < 0 227 if sortBy != sortByName && !asc { 228 less = !less 229 } 230 return less 231 }) 232 } 233 234 func (m *topDriveUI) View() string { 235 var s strings.Builder 236 s.WriteString("\n") 237 238 // Set table header 239 table := tablewriter.NewWriter(&s) 240 table.SetAutoWrapText(false) 241 table.SetAutoFormatHeaders(true) 242 table.SetHeaderAlignment(tablewriter.ALIGN_CENTER) 243 table.SetAlignment(tablewriter.ALIGN_CENTER) 244 table.SetCenterSeparator("") 245 table.SetColumnSeparator("") 246 table.SetRowSeparator("") 247 table.SetHeaderLine(false) 248 table.SetBorder(false) 249 table.SetTablePadding("\t") // pad with tabs 250 table.SetNoWhiteSpace(true) 251 252 table.SetHeader([]string{"Drive", "used", "tps", "read", "write", "discard", "await", "util"}) 253 254 var data []driveIOStat 255 256 for disk := range m.currTopMap { 257 currDisk, ok := m.drivesInfo[disk] 258 if !ok || currDisk.PoolIndex != m.pool { 259 continue 260 } 261 data = append(data, generateDriveStat(m.drivesInfo[disk], m.currTopMap[disk], m.prevTopMap[disk], 1000)) 262 } 263 264 sortDriveIOStat(m.sortBy, m.sortAsc, data) 265 266 if len(data) > m.count { 267 data = data[:m.count] 268 } 269 270 dataRender := make([][]string, 0, len(data)) 271 for _, d := range data { 272 endpoint := d.endpoint 273 diskInfo := m.drivesInfo[endpoint] 274 if diskInfo.Healing { 275 endpoint += "!" 276 } 277 if diskInfo.Scanning { 278 endpoint += "*" 279 } 280 if diskInfo.TotalSpace == 0 { 281 endpoint += crossTickCell 282 } 283 284 dataRender = append(dataRender, []string{ 285 endpoint, 286 whiteStyle.Render(fmt.Sprintf("%d%%", d.used)), 287 whiteStyle.Render(fmt.Sprintf("%v", d.tps)), 288 whiteStyle.Render(fmt.Sprintf("%.2f MiB/s", d.readMBs)), 289 whiteStyle.Render(fmt.Sprintf("%.2f MiB/s", d.writeMBs)), 290 whiteStyle.Render(fmt.Sprintf("%.2f MiB/s", d.discardMBs)), 291 whiteStyle.Render(fmt.Sprintf("%.1f ms", d.await)), 292 whiteStyle.Render(fmt.Sprintf("%.1f%%", d.util)), 293 }) 294 } 295 296 table.AppendBulk(dataRender) 297 table.Render() 298 299 if !m.quitting { 300 order := "DESC" 301 if m.sortAsc { 302 order = "ASC" 303 } 304 305 s.WriteString(fmt.Sprintf("\n%s \u25C0 Pool %d \u25B6 | Sort By: %s (u,t,r,w,d,a,U) | (O)rder: %s ", m.spinner.View(), m.pool+1, m.sortBy, order)) 306 } 307 return s.String() + "\n" 308 }