k9s/internal/views/bench.go

347 lines
7.6 KiB
Go

package views
import (
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/resource"
"github.com/derailed/tview"
"github.com/fsnotify/fsnotify"
"github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
)
const (
benchTitle = "Benchmarks"
benchTitleFmt = " [seagreen::b]%s([fuchsia::b]%d[fuchsia::-])[seagreen::-] "
)
var (
totalRx = regexp.MustCompile(`Total:\s+([0-9.]+)\ssecs`)
reqRx = regexp.MustCompile(`Requests/sec:\s+([0-9.]+)`)
okRx = regexp.MustCompile(`\[2\d{2}\]\s+(\d+)\s+responses`)
errRx = regexp.MustCompile(`\[[4-5]\d{2}\]\s+(\d+)\s+responses`)
toastRx = regexp.MustCompile(`Error distribution`)
benchHeader = resource.Row{"NAMESPACE", "NAME", "STATUS", "TIME", "REQ/S", "2XX", "4XX/5XX", "REPORT", "AGE"}
)
type benchView struct {
*tview.Pages
app *appView
cancel context.CancelFunc
selectedItem string
selectedRow int
actions keyActions
}
func newBenchView(_ string, app *appView, _ resource.List) resourceViewer {
v := benchView{
Pages: tview.NewPages(),
actions: make(keyActions),
app: app,
}
tv := newTableView(app, benchTitle)
tv.SetSelectionChangedFunc(v.selChanged)
tv.SetBorderFocusColor(tcell.ColorSeaGreen)
tv.SetSelectedStyle(tcell.ColorWhite, tcell.ColorSeaGreen, tcell.AttrNone)
tv.colorerFn = benchColorer
tv.currentNS = ""
v.AddPage("table", tv, true, true)
details := newDetailsView(app, v.backCmd)
details.setCategory("Bench")
details.SetTextColor(app.styles.FgColor())
v.AddPage("details", details, true, false)
v.registerActions()
return &v
}
func (v *benchView) setEnterFn(enterFn) {}
func (v *benchView) setColorerFn(colorerFn) {}
func (v *benchView) setDecorateFn(decorateFn) {}
func (v *benchView) setExtraActionsFn(actionsFn) {}
// Init the view.
func (v *benchView) init(ctx context.Context, _ string) {
if err := v.watchBenchDir(ctx); err != nil {
v.app.flash().errf("Unable to watch benchmarks directory %s", err)
}
tv := v.getTV()
v.refresh()
tv.sortCol.index, tv.sortCol.asc = tv.nameColIndex()+7, true
tv.refresh()
tv.Select(1, 0)
v.app.SetFocus(tv)
}
func (v *benchView) refresh() {
tv := v.getTV()
tv.update(v.hydrate())
tv.resetTitle()
v.selChanged(v.selectedRow, 0)
}
func (v *benchView) registerActions() {
v.actions[KeyP] = newKeyAction("Previous", v.app.prevCmd, false)
v.actions[tcell.KeyEnter] = newKeyAction("Enter", v.enterCmd, false)
v.actions[tcell.KeyCtrlD] = newKeyAction("Delete", v.deleteCmd, false)
vu := v.getTV()
vu.setActions(v.actions)
v.app.setHints(vu.hints())
}
func (v *benchView) getTitle() string {
return benchTitle
}
func (v *benchView) selChanged(r, c int) {
tv := v.getTV()
if r == 0 || tv.GetCell(r, 0) == nil {
v.selectedItem = ""
return
}
v.selectedRow = r
v.selectedItem = trimCell(tv, r, 7)
}
func (v *benchView) sortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
return func(evt *tcell.EventKey) *tcell.EventKey {
tv := v.getTV()
tv.sortCol.index, tv.sortCol.asc = tv.nameColIndex()+col, asc
tv.refresh()
return nil
}
}
func (v *benchView) enterCmd(evt *tcell.EventKey) *tcell.EventKey {
if v.getTV().cmdBuff.isActive() {
return v.getTV().filterCmd(evt)
}
if v.selectedItem == "" {
return nil
}
data, err := readBenchFile(v.app.config, v.selectedItem)
if err != nil {
v.app.flash().errf("Unable to load bench file %s", err)
return nil
}
vu := v.getDetails()
vu.SetText(data)
vu.setTitle(v.selectedItem)
v.SwitchToPage("details")
return nil
}
func (v *benchView) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
sel := v.selectedItem
if sel == "" {
return nil
}
dir := filepath.Join(K9sBenchDir, v.app.config.K9s.CurrentCluster)
showModal(v.Pages, fmt.Sprintf("Delete benchmark `%s?", sel), "table", func() {
if err := os.Remove(filepath.Join(dir, sel)); err != nil {
v.app.flash().errf("Unable to delete file %s", err)
return
}
v.refresh()
v.app.flash().infof("Benchmark %s deleted!", sel)
})
return nil
}
func (v *benchView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
if v.cancel != nil {
v.cancel()
}
v.SwitchToPage("table")
return nil
}
func (v *benchView) hints() hints {
return v.CurrentPage().Item.(hinter).hints()
}
func (v *benchView) hydrate() resource.TableData {
ff, err := loadBenchDir(v.app.config)
if err != nil {
v.app.flash().errf("Unable to read bench directory %s", err)
}
data := initTable()
for _, f := range ff {
bench, err := readBenchFile(v.app.config, f.Name())
if err != nil {
log.Error().Err(err).Msgf("Unable to load bench file %s", f.Name())
continue
}
fields := make(resource.Row, len(benchHeader))
initRow(fields, f)
augmentRow(fields, bench)
data.Rows[f.Name()] = &resource.RowEvent{
Action: resource.New,
Fields: fields,
Deltas: fields,
}
}
return data
}
func initRow(row resource.Row, f os.FileInfo) {
tokens := strings.Split(f.Name(), "_")
row[0] = tokens[0]
row[1] = tokens[1]
row[7] = f.Name()
row[8] = time.Since(f.ModTime()).String()
}
func (v *benchView) getTV() *tableView {
if vu, ok := v.GetPrimitive("table").(*tableView); ok {
return vu
}
return nil
}
func (v *benchView) getDetails() *detailsView {
if vu, ok := v.GetPrimitive("details").(*detailsView); ok {
return vu
}
return nil
}
func (v *benchView) resetTitle1() {
v.SetTitle(fmt.Sprintf(benchTitleFmt, benchTitle, v.getTV().GetRowCount()-1))
}
func (v *benchView) watchBenchDir(ctx context.Context) error {
w, err := fsnotify.NewWatcher()
if err != nil {
return err
}
go func() {
for {
select {
case evt := <-w.Events:
log.Debug().Msgf("Bench event %#v", evt)
v.app.QueueUpdateDraw(func() {
v.refresh()
})
case err := <-w.Errors:
log.Info().Err(err).Msg("Dir Watcher failed")
return
case <-ctx.Done():
log.Debug().Msg("!!!! FS WATCHER DONE!!")
w.Close()
return
}
}
}()
return w.Add(benchDir(v.app.config))
}
// ----------------------------------------------------------------------------
// Helpers...
func initTable() resource.TableData {
return resource.TableData{
Header: benchHeader,
Rows: make(resource.RowEvents, 10),
NumCols: map[string]bool{
benchHeader[3]: true,
benchHeader[4]: true,
benchHeader[5]: true,
benchHeader[6]: true,
},
Namespace: resource.AllNamespaces,
}
}
func augmentRow(fields resource.Row, data string) {
if len(data) == 0 {
return
}
col := 2
fields[col] = "pass"
mf := toastRx.FindAllStringSubmatch(data, 1)
if len(mf) > 0 {
fields[col] = "fail"
}
col++
mt := totalRx.FindAllStringSubmatch(data, 1)
if len(mt) > 0 {
fields[col] = mt[0][1]
}
col++
mr := reqRx.FindAllStringSubmatch(data, 1)
if len(mr) > 0 {
fields[col] = mr[0][1]
}
col++
ms := okRx.FindAllStringSubmatch(data, -1)
fields[col] = "0"
if len(ms) > 0 {
var sum int
for _, m := range ms {
if m, err := strconv.Atoi(string(m[1])); err == nil {
sum += m
}
}
fields[col] = asNum(sum)
}
col++
me := errRx.FindAllStringSubmatch(data, -1)
fields[col] = "0"
if len(me) > 0 {
var sum int
for _, m := range me {
if m, err := strconv.Atoi(string(m[1])); err == nil {
sum += m
}
}
fields[col] = asNum(sum)
}
}
func benchDir(cfg *config.Config) string {
return filepath.Join(K9sBenchDir, cfg.K9s.CurrentCluster)
}
func loadBenchDir(cfg *config.Config) ([]os.FileInfo, error) {
return ioutil.ReadDir(benchDir(cfg))
}
func readBenchFile(cfg *config.Config, n string) (string, error) {
data, err := ioutil.ReadFile(filepath.Join(benchDir(cfg), n))
if err != nil {
return "", err
}
return string(data), nil
}