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  }