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  }