github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/top-net-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  	"math"
    23  	"sort"
    24  	"strings"
    25  	"time"
    26  
    27  	"github.com/charmbracelet/bubbles/spinner"
    28  	tea "github.com/charmbracelet/bubbletea"
    29  	"github.com/charmbracelet/lipgloss"
    30  	"github.com/dustin/go-humanize"
    31  	"github.com/minio/madmin-go/v3"
    32  	"github.com/olekukonko/tablewriter"
    33  	"github.com/prometheus/procfs"
    34  )
    35  
    36  type topNetUI struct {
    37  	spinner  spinner.Model
    38  	quitting bool
    39  
    40  	sortAsc bool
    41  
    42  	prevTopMap map[string]topNetResult
    43  	currTopMap map[string]topNetResult
    44  }
    45  
    46  type topNetResult struct {
    47  	final    bool
    48  	endPoint string
    49  	error    string
    50  	stats    madmin.NetMetrics
    51  }
    52  
    53  func (t topNetResult) GetTotalBytes() uint64 {
    54  	return t.stats.NetStats.RxBytes + t.stats.NetStats.TxBytes
    55  }
    56  
    57  func (m *topNetUI) Init() tea.Cmd {
    58  	return m.spinner.Tick
    59  }
    60  
    61  func (m *topNetUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    62  	switch msg := msg.(type) {
    63  	case tea.KeyMsg:
    64  		switch msg.String() {
    65  		case "ctrl+c", "q", "esc":
    66  			m.quitting = true
    67  			return m, tea.Quit
    68  		}
    69  		return m, nil
    70  	case topNetResult:
    71  		m.prevTopMap[msg.endPoint] = m.currTopMap[msg.endPoint]
    72  		m.currTopMap[msg.endPoint] = msg
    73  		if msg.final {
    74  			m.quitting = true
    75  			return m, tea.Quit
    76  		}
    77  		return m, nil
    78  
    79  	case spinner.TickMsg:
    80  		var cmd tea.Cmd
    81  		m.spinner, cmd = m.spinner.Update(msg)
    82  		return m, cmd
    83  	default:
    84  		return m, nil
    85  	}
    86  }
    87  
    88  func (m *topNetUI) calculationRate(prev, curr uint64, dur time.Duration) uint64 {
    89  	if curr < prev {
    90  		return uint64(float64(math.MaxUint64-prev+curr) / dur.Seconds())
    91  	}
    92  	return uint64(float64(curr-prev) / dur.Seconds())
    93  }
    94  
    95  func (m *topNetUI) View() string {
    96  	var s strings.Builder
    97  	// Set table header
    98  	table := tablewriter.NewWriter(&s)
    99  	table.SetAutoWrapText(false)
   100  	table.SetAutoFormatHeaders(true)
   101  	table.SetHeaderAlignment(tablewriter.ALIGN_CENTER)
   102  	table.SetAlignment(tablewriter.ALIGN_CENTER)
   103  	table.SetCenterSeparator("")
   104  	table.SetColumnSeparator("")
   105  	table.SetRowSeparator("")
   106  	table.SetHeaderLine(false)
   107  	table.SetBorder(false)
   108  	table.SetTablePadding("\t") // pad with tabs
   109  	table.SetNoWhiteSpace(true)
   110  	table.SetHeader([]string{"SERVER", "INTERFACE", "RECEIVE", "TRANSMIT", ""})
   111  
   112  	data := make([]topNetResult, 0, len(m.currTopMap))
   113  
   114  	for endPoint, curr := range m.currTopMap {
   115  		if prev, ok := m.prevTopMap[endPoint]; ok {
   116  			data = append(data, topNetResult{
   117  				final:    curr.final,
   118  				endPoint: curr.endPoint,
   119  				error:    curr.error,
   120  				stats: madmin.NetMetrics{
   121  					CollectedAt:   curr.stats.CollectedAt,
   122  					InterfaceName: curr.stats.InterfaceName,
   123  					NetStats: procfs.NetDevLine{
   124  						RxBytes: m.calculationRate(prev.stats.NetStats.RxBytes, curr.stats.NetStats.RxBytes, curr.stats.CollectedAt.Sub(prev.stats.CollectedAt)),
   125  						TxBytes: m.calculationRate(prev.stats.NetStats.TxBytes, curr.stats.NetStats.TxBytes, curr.stats.CollectedAt.Sub(prev.stats.CollectedAt)),
   126  					},
   127  				},
   128  			})
   129  		}
   130  	}
   131  
   132  	sort.Slice(data, func(i, j int) bool {
   133  		if m.sortAsc {
   134  			return data[i].GetTotalBytes() < data[j].GetTotalBytes()
   135  		}
   136  		return data[i].GetTotalBytes() >= data[j].GetTotalBytes()
   137  	})
   138  
   139  	dataRender := make([][]string, 0, len(data))
   140  	for _, d := range data {
   141  		if d.error == "" {
   142  			dataRender = append(dataRender, []string{
   143  				d.endPoint,
   144  				whiteStyle.Render(d.stats.InterfaceName),
   145  				whiteStyle.Render(fmt.Sprintf("%s/s", humanize.IBytes(d.stats.NetStats.RxBytes))),
   146  				whiteStyle.Render(fmt.Sprintf("%s/s", humanize.IBytes(d.stats.NetStats.TxBytes))),
   147  				"",
   148  			})
   149  		} else {
   150  			dataRender = append(dataRender, []string{
   151  				d.endPoint,
   152  				whiteStyle.Render(d.stats.NetStats.Name),
   153  				crossTickCell,
   154  				crossTickCell,
   155  				d.error,
   156  			})
   157  		}
   158  	}
   159  
   160  	table.AppendBulk(dataRender)
   161  	table.Render()
   162  	return s.String()
   163  }
   164  
   165  func initTopNetUI() *topNetUI {
   166  	s := spinner.New()
   167  	s.Spinner = spinner.Points
   168  	s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
   169  	return &topNetUI{
   170  		spinner:    s,
   171  		currTopMap: make(map[string]topNetResult),
   172  		prevTopMap: make(map[string]topNetResult),
   173  	}
   174  }