github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/top-api-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  	"sync/atomic"
    26  	"time"
    27  
    28  	"github.com/charmbracelet/bubbles/spinner"
    29  	tea "github.com/charmbracelet/bubbletea"
    30  	"github.com/charmbracelet/lipgloss"
    31  	"github.com/dustin/go-humanize"
    32  	"github.com/minio/madmin-go/v3"
    33  	"github.com/olekukonko/tablewriter"
    34  )
    35  
    36  type topAPIStats struct {
    37  	TotalCalls         uint64
    38  	TotalBytesRX       uint64
    39  	TotalBytesTX       uint64
    40  	TotalErrors        uint64
    41  	TotalDurationNanos uint64
    42  	MaxDurationNanos   uint64
    43  	MinDurationNanos   uint64
    44  }
    45  
    46  func (s *topAPIStats) addAPICall(n int) {
    47  	atomic.AddUint64(&s.TotalCalls, uint64(n))
    48  }
    49  
    50  func (s *topAPIStats) addAPIBytesRX(n int) {
    51  	atomic.AddUint64(&s.TotalBytesRX, uint64(n))
    52  }
    53  
    54  func (s *topAPIStats) addAPIBytesTX(n int) {
    55  	atomic.AddUint64(&s.TotalBytesTX, uint64(n))
    56  }
    57  
    58  func (s *topAPIStats) addAPIErrors(n int) {
    59  	atomic.AddUint64(&s.TotalErrors, uint64(n))
    60  }
    61  
    62  func (s *topAPIStats) addAPIDurationNanos(n int64) {
    63  	atomic.AddUint64(&s.TotalDurationNanos, uint64(n))
    64  	if s.MinDurationNanos == 0 {
    65  		s.MinDurationNanos = uint64(n)
    66  	}
    67  	s.MinDurationNanos = uint64(math.Min(float64(n), float64(s.MinDurationNanos)))
    68  	s.MaxDurationNanos = uint64(math.Max(float64(n), float64(s.MaxDurationNanos)))
    69  }
    70  
    71  func (s *topAPIStats) loadAPICall() uint64 {
    72  	return atomic.LoadUint64(&s.TotalCalls)
    73  }
    74  
    75  func (s *topAPIStats) loadAPIBytesRX() uint64 {
    76  	return atomic.LoadUint64(&s.TotalBytesRX)
    77  }
    78  
    79  func (s *topAPIStats) loadAPIBytesTX() uint64 {
    80  	return atomic.LoadUint64(&s.TotalBytesTX)
    81  }
    82  
    83  func (s *topAPIStats) loadAPIErrors() uint64 {
    84  	return atomic.LoadUint64(&s.TotalErrors)
    85  }
    86  
    87  func (s *topAPIStats) loadAPIDurationNanos() uint64 {
    88  	return atomic.LoadUint64(&s.TotalDurationNanos)
    89  }
    90  
    91  func (s *topAPIStats) loadAPIMinDurationNanos() uint64 {
    92  	return atomic.LoadUint64(&s.MinDurationNanos)
    93  }
    94  
    95  func (s *topAPIStats) loadAPIMaxDurationNanos() uint64 {
    96  	return atomic.LoadUint64(&s.MaxDurationNanos)
    97  }
    98  
    99  type traceUI struct {
   100  	spinner     spinner.Model
   101  	quitting    bool
   102  	startTime   time.Time
   103  	result      topAPIResult
   104  	lastResult  topAPIResult
   105  	apiStatsMap map[string]*topAPIStats
   106  }
   107  
   108  type topAPIResult struct {
   109  	final       bool
   110  	apiCallInfo madmin.ServiceTraceInfo
   111  }
   112  
   113  func initTraceUI() *traceUI {
   114  	s := spinner.New()
   115  	s.Spinner = spinner.Points
   116  	s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
   117  	return &traceUI{
   118  		spinner:     s,
   119  		apiStatsMap: make(map[string]*topAPIStats),
   120  	}
   121  }
   122  
   123  func (m *traceUI) Init() tea.Cmd {
   124  	return m.spinner.Tick
   125  }
   126  
   127  func (m *traceUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
   128  	switch msg := msg.(type) {
   129  	case tea.KeyMsg:
   130  		switch msg.String() {
   131  		case "ctrl+c", "q", "esc":
   132  			m.quitting = true
   133  			return m, tea.Quit
   134  		default:
   135  			return m, nil
   136  		}
   137  	case topAPIResult:
   138  		m.result = msg
   139  		if m.result.apiCallInfo.Trace.FuncName != "" {
   140  			m.lastResult = m.result
   141  		}
   142  		if msg.final {
   143  			m.quitting = true
   144  			return m, tea.Quit
   145  		}
   146  		return m, nil
   147  	}
   148  
   149  	var cmd tea.Cmd
   150  	m.spinner, cmd = m.spinner.Update(msg)
   151  	return m, cmd
   152  }
   153  
   154  func (m *traceUI) View() string {
   155  	var s strings.Builder
   156  	s.WriteString("\n")
   157  
   158  	// Set table header
   159  	table := tablewriter.NewWriter(&s)
   160  	table.SetAutoWrapText(false)
   161  	table.SetAutoFormatHeaders(true)
   162  	table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
   163  	table.SetAlignment(tablewriter.ALIGN_LEFT)
   164  	table.SetCenterSeparator("")
   165  	table.SetColumnSeparator("")
   166  	table.SetRowSeparator("")
   167  	table.SetHeaderLine(false)
   168  	table.SetBorder(false)
   169  	table.SetTablePadding("\t") // pad with tabs
   170  	table.SetNoWhiteSpace(true)
   171  
   172  	res := m.result.apiCallInfo
   173  	if m.startTime.IsZero() && !res.Trace.Time.IsZero() {
   174  		m.startTime = res.Trace.Time
   175  	}
   176  	if res.Trace.FuncName != "" && res.Trace.FuncName != "errorResponseHandler" {
   177  		traceSt, ok := m.apiStatsMap[res.Trace.FuncName]
   178  		if !ok {
   179  			traceSt = &topAPIStats{}
   180  		}
   181  		traceSt.addAPICall(1)
   182  		if res.Trace.HTTP != nil {
   183  			traceSt.addAPIBytesRX(res.Trace.HTTP.CallStats.InputBytes)
   184  			traceSt.addAPIBytesTX(res.Trace.HTTP.CallStats.OutputBytes)
   185  			traceSt.addAPIDurationNanos(res.Trace.Duration.Nanoseconds())
   186  		}
   187  		if res.Trace.HTTP.RespInfo.StatusCode >= 499 {
   188  			traceSt.addAPIErrors(1)
   189  		}
   190  		m.apiStatsMap[res.Trace.FuncName] = traceSt
   191  	}
   192  
   193  	table.SetHeader([]string{"API", "RX", "TX", "CALLS", "ERRORS", "MinRT", "MaxRT", "AvgRT", "AvgTP"})
   194  	data := make([][]string, 0, len(m.apiStatsMap))
   195  
   196  	for k, stats := range m.apiStatsMap {
   197  		secs := time.Duration(stats.loadAPIDurationNanos()).Seconds()
   198  		bytes := float64(stats.loadAPIBytesRX() + stats.loadAPIBytesTX())
   199  		data = append(data, []string{
   200  			k,
   201  			whiteStyle.Render(humanize.IBytes(stats.loadAPIBytesRX())),
   202  			whiteStyle.Render(humanize.IBytes(stats.loadAPIBytesTX())),
   203  			whiteStyle.Render(fmt.Sprintf("%d", stats.loadAPICall())),
   204  			whiteStyle.Render(fmt.Sprintf("%d", stats.loadAPIErrors())),
   205  			whiteStyle.Render(fmt.Sprintf("%.03f s", time.Duration(stats.loadAPIMinDurationNanos()).Seconds())),
   206  			whiteStyle.Render(fmt.Sprintf("%.03f s", time.Duration(stats.loadAPIMaxDurationNanos()).Seconds())),
   207  			whiteStyle.Render(fmt.Sprintf("%.03f s", time.Duration(stats.loadAPIDurationNanos()/stats.loadAPICall()).Seconds())),
   208  			whiteStyle.Render(fmt.Sprintf("%s/s", humanize.IBytes(uint64(bytes/secs)))),
   209  		})
   210  	}
   211  	sort.Slice(data, func(i, j int) bool {
   212  		return data[i][0] < data[j][0]
   213  	})
   214  
   215  	table.AppendBulk(data)
   216  	table.Render()
   217  
   218  	if !m.quitting {
   219  		s.WriteString(fmt.Sprintf("\nTopAPI: %s", m.spinner.View()))
   220  	} else {
   221  		var totalTX, totalRX, totalCalls uint64
   222  		lastReqTime := m.lastResult.apiCallInfo.Trace.Time
   223  		if m.lastResult.apiCallInfo.Trace.Time.IsZero() {
   224  			lastReqTime = time.Now()
   225  		}
   226  		for _, stats := range m.apiStatsMap {
   227  			totalRX += stats.loadAPIBytesRX()
   228  			totalTX += stats.loadAPIBytesTX()
   229  			totalCalls += stats.loadAPICall()
   230  		}
   231  
   232  		msg := fmt.Sprintf("\nSummary:\n\nTotal: %d CALLS, %s RX, %s TX",
   233  			totalCalls,
   234  			humanize.IBytes(totalRX),
   235  			humanize.IBytes(totalTX),
   236  		)
   237  		if !m.startTime.IsZero() {
   238  			msg += fmt.Sprintf(" - in %.02fs", lastReqTime.Sub(m.startTime).Seconds())
   239  		}
   240  
   241  		s.WriteString(msg)
   242  		s.WriteString("\n")
   243  	}
   244  	return s.String()
   245  }