--- /dev/null
+// lib - library routines for pstop.
+//
+// this file contains the library routines related to the stored state in pstop.
+package app
+
+import (
+ "database/sql"
+ "errors"
+ "fmt"
+ "log"
+ "os"
+ "os/signal"
+ "regexp"
+ "strings"
+ "syscall"
+ "time"
+
+ "github.com/nsf/termbox-go"
+
+ "github.com/sjmudd/pstop/i_s/processlist"
+ "github.com/sjmudd/pstop/lib"
+ essgben "github.com/sjmudd/pstop/p_s/events_stages_summary_global_by_event_name"
+ ewsgben "github.com/sjmudd/pstop/p_s/events_waits_summary_global_by_event_name"
+ fsbi "github.com/sjmudd/pstop/p_s/file_summary_by_instance"
+ "github.com/sjmudd/pstop/p_s/ps_table"
+ "github.com/sjmudd/pstop/p_s/setup_instruments"
+ tiwsbt "github.com/sjmudd/pstop/p_s/table_io_waits_summary_by_table"
+ tlwsbt "github.com/sjmudd/pstop/p_s/table_lock_waits_summary_by_table"
+ "github.com/sjmudd/pstop/screen"
+ "github.com/sjmudd/pstop/version"
+ "github.com/sjmudd/pstop/wait_info"
+)
+
+// what information to show
+type Show int
+
+const (
+ showLatency = iota
+ showOps = iota
+ showIO = iota
+ showLocks = iota
+ showUsers = iota
+ showMutex = iota
+ showStages = iota
+)
+
+var (
+ re_valid_version = regexp.MustCompile(`^(5\.[67]\.|10\.[01])`)
+)
+
+type App struct {
+ finished bool
+ datadir string
+ dbh *sql.DB
+ help bool
+ hostname string
+ fsbi ps_table.Tabler // ufsbi.File_summary_by_instance
+ tiwsbt tiwsbt.Object
+ tlwsbt ps_table.Tabler // tlwsbt.Table_lock_waits_summary_by_table
+ ewsgben ps_table.Tabler // ewsgben.Events_waits_summary_global_by_event_name
+ essgben ps_table.Tabler // essgben.Events_stages_summary_global_by_event_name
+ users processlist.Object
+ screen screen.TermboxScreen
+ show Show
+ mysql_version string
+ want_relative_stats bool
+ wait_info.WaitInfo // embedded
+ setup_instruments setup_instruments.SetupInstruments
+}
+
+func (app *App) Setup(dbh *sql.DB) {
+ app.dbh = dbh
+
+ if err := app.validate_mysql_version(); err != nil {
+ log.Fatal(err)
+ }
+
+ app.finished = false
+
+ app.screen.Initialise()
+
+ app.setup_instruments = setup_instruments.NewSetupInstruments(dbh)
+ app.setup_instruments.EnableMonitoring()
+
+ _, variables := lib.SelectAllGlobalVariablesByVariableName(app.dbh)
+ // setup to their initial types/values
+ app.fsbi = fsbi.NewFileSummaryByInstance(variables)
+ app.tlwsbt = new(tlwsbt.Object)
+ app.ewsgben = new(ewsgben.Object)
+ app.essgben = new(essgben.Object)
+
+ app.want_relative_stats = true // we show info from the point we start collecting data
+ app.fsbi.SetWantRelativeStats(app.want_relative_stats)
+ app.fsbi.SetNow()
+ app.tlwsbt.SetWantRelativeStats(app.want_relative_stats)
+ app.tlwsbt.SetNow()
+ app.tiwsbt.SetWantRelativeStats(app.want_relative_stats)
+ app.tiwsbt.SetNow()
+ app.users.SetWantRelativeStats(app.want_relative_stats) // ignored
+ app.users.SetNow() // ignored
+ app.essgben.SetWantRelativeStats(app.want_relative_stats)
+ app.essgben.SetNow()
+ app.ewsgben.SetWantRelativeStats(app.want_relative_stats) // ignored
+ app.ewsgben.SetNow() // ignored
+
+ app.ResetDBStatistics()
+
+ app.SetHelp(false)
+ app.show = showLatency
+ app.tiwsbt.SetWantsLatency(true)
+
+ // get short name (to save space)
+ _, hostname := lib.SelectGlobalVariableByVariableName(app.dbh, "HOSTNAME")
+ if index := strings.Index(hostname, "."); index >= 0 {
+ hostname = hostname[0:index]
+ }
+ _, mysql_version := lib.SelectGlobalVariableByVariableName(app.dbh, "VERSION")
+ _, datadir := lib.SelectGlobalVariableByVariableName(app.dbh, "DATADIR")
+ app.SetHostname(hostname)
+ app.SetMySQLVersion(mysql_version)
+ app.SetDatadir(datadir)
+}
+
+// have we finished ?
+func (app App) Finished() bool {
+ return app.finished
+}
+
+// indicate we have finished
+func (app *App) SetFinished() {
+ app.finished = true
+}
+
+// do a fresh collection of data and then update the initial values based on that.
+func (app *App) ResetDBStatistics() {
+ app.CollectAll()
+ app.SyncReferenceValues()
+}
+
+func (app *App) SyncReferenceValues() {
+ start := time.Now()
+ app.fsbi.SyncReferenceValues()
+ app.tlwsbt.SyncReferenceValues()
+ app.tiwsbt.SyncReferenceValues()
+ app.essgben.SyncReferenceValues()
+ lib.Logger.Println("app.SyncReferenceValues() took", time.Duration(time.Since(start)).String())
+}
+
+// collect all initial values on startup / reset
+func (app *App) CollectAll() {
+ app.fsbi.Collect(app.dbh)
+ app.tlwsbt.Collect(app.dbh)
+ app.tiwsbt.Collect(app.dbh)
+}
+
+// Only collect the data we are looking at.
+func (app *App) Collect() {
+ start := time.Now()
+
+ switch app.show {
+ case showLatency, showOps:
+ app.tiwsbt.Collect(app.dbh)
+ case showIO:
+ app.fsbi.Collect(app.dbh)
+ case showLocks:
+ app.tlwsbt.Collect(app.dbh)
+ case showUsers:
+ app.users.Collect(app.dbh)
+ case showMutex:
+ app.ewsgben.Collect(app.dbh)
+ case showStages:
+ app.essgben.Collect(app.dbh)
+ }
+ lib.Logger.Println("app.Collect() took", time.Duration(time.Since(start)).String())
+}
+
+func (app App) MySQLVersion() string {
+ return app.mysql_version
+}
+
+func (app App) Datadir() string {
+ return app.datadir
+}
+
+func (app *App) SetHelp(newHelp bool) {
+ app.help = newHelp
+
+ app.screen.Clear()
+ app.screen.Flush()
+}
+
+func (app *App) SetDatadir(datadir string) {
+ app.datadir = datadir
+}
+
+func (app *App) SetMySQLVersion(mysql_version string) {
+ app.mysql_version = mysql_version
+}
+
+func (app *App) SetHostname(hostname string) {
+ app.hostname = hostname
+}
+
+func (app App) Help() bool {
+ return app.help
+}
+
+// apps go: showLatency -> showOps -> showIO -> showLocks -> showUsers -> showMutex -> showStages
+
+// display the output according to the mode we are in
+func (app *App) Display() {
+ if app.help {
+ app.screen.DisplayHelp()
+ } else {
+ app.displayHeading()
+ switch app.show {
+ case showLatency, showOps:
+ app.displayOpsOrLatency()
+ case showIO:
+ app.displayIO()
+ case showLocks:
+ app.displayLocks()
+ case showUsers:
+ app.displayUsers()
+ case showMutex:
+ app.displayMutex()
+ case showStages:
+ app.displayStages()
+ }
+ }
+}
+
+// fix_latency_setting() ensures the SetWantsLatency() value is
+// correct. This needs to be done more cleanly.
+func (app *App) fix_latency_setting() {
+ if app.show == showLatency {
+ app.tiwsbt.SetWantsLatency(true)
+ }
+ if app.show == showOps {
+ app.tiwsbt.SetWantsLatency(false)
+ }
+}
+
+// change to the previous display mode
+func (app *App) DisplayPrevious() {
+ if app.show == showLatency {
+ app.show = showStages
+ } else {
+ app.show--
+ }
+ app.fix_latency_setting()
+ app.screen.Clear()
+ app.screen.Flush()
+}
+
+// change to the next display mode
+func (app *App) DisplayNext() {
+ if app.show == showStages {
+ app.show = showLatency
+ } else {
+ app.show++
+ }
+ app.fix_latency_setting()
+ app.screen.Clear()
+ app.screen.Flush()
+}
+
+func (app App) displayHeading() {
+ app.displayLine0()
+ app.displayDescription()
+}
+
+func (app App) displayLine0() {
+ _, uptime := lib.SelectGlobalStatusByVariableName(app.dbh, "UPTIME")
+ top_line := lib.MyName() + " " + version.Version() + " - " + now_hhmmss() + " " + app.hostname + " / " + app.mysql_version + ", up " + fmt.Sprintf("%-16s", lib.Uptime(uptime))
+ if app.want_relative_stats {
+ now := time.Now()
+
+ var initial time.Time
+
+ switch app.show {
+ case showLatency, showOps:
+ initial = app.tiwsbt.Last()
+ case showIO:
+ initial = app.fsbi.Last()
+ case showLocks:
+ initial = app.tlwsbt.Last()
+ case showUsers:
+ initial = app.users.Last()
+ case showStages:
+ initial = app.essgben.Last()
+ case showMutex:
+ initial = app.ewsgben.Last()
+ default:
+ // should not get here !
+ }
+
+ d := now.Sub(initial)
+
+ top_line = top_line + " [REL] " + fmt.Sprintf("%.0f seconds", d.Seconds())
+ } else {
+ top_line = top_line + " [ABS] "
+ }
+ app.screen.PrintAt(0, 0, top_line)
+}
+
+func (app App) displayDescription() {
+ description := "UNKNOWN"
+
+ switch app.show {
+ case showLatency, showOps:
+ description = app.tiwsbt.Description()
+ case showIO:
+ description = app.fsbi.Description()
+ case showLocks:
+ description = app.tlwsbt.Description()
+ case showUsers:
+ description = app.users.Description()
+ case showMutex:
+ description = app.ewsgben.Description()
+ case showStages:
+ description = app.essgben.Description()
+ }
+
+ app.screen.PrintAt(0, 1, description)
+}
+
+func (app *App) displayOpsOrLatency() {
+ app.screen.BoldPrintAt(0, 2, app.tiwsbt.Headings())
+
+ max_rows := app.screen.Height() - 3
+ row_content := app.tiwsbt.RowContent(max_rows)
+
+ // print out rows
+ for k := range row_content {
+ y := 3 + k
+ app.screen.PrintAt(0, y, row_content[k])
+ }
+ // print out empty rows
+ for k := len(row_content); k < (app.screen.Height() - 3); k++ {
+ y := 3 + k
+ if y < app.screen.Height()-1 {
+ app.screen.PrintAt(0, y, app.tiwsbt.EmptyRowContent())
+ }
+ }
+
+ // print out the totals at the bottom
+ app.screen.BoldPrintAt(0, app.screen.Height()-1, app.tiwsbt.TotalRowContent())
+}
+
+// show actual I/O latency values
+func (app App) displayIO() {
+ app.screen.BoldPrintAt(0, 2, app.fsbi.Headings())
+
+ // print out the data
+ max_rows := app.screen.Height() - 3
+ row_content := app.fsbi.RowContent(max_rows)
+
+ // print out rows
+ for k := range row_content {
+ y := 3 + k
+ app.screen.PrintAt(0, y, row_content[k])
+ }
+ // print out empty rows
+ for k := len(row_content); k < (app.screen.Height() - 3); k++ {
+ y := 3 + k
+ if y < app.screen.Height()-1 {
+ app.screen.PrintAt(0, y, app.fsbi.EmptyRowContent())
+ }
+ }
+
+ // print out the totals at the bottom
+ app.screen.BoldPrintAt(0, app.screen.Height()-1, app.fsbi.TotalRowContent())
+}
+
+func (app *App) displayLocks() {
+ app.screen.BoldPrintAt(0, 2, app.tlwsbt.Headings())
+
+ // print out the data
+ max_rows := app.screen.Height() - 3
+ row_content := app.tlwsbt.RowContent(max_rows)
+
+ // print out rows
+ for k := range row_content {
+ y := 3 + k
+ app.screen.PrintAt(0, y, row_content[k])
+ }
+ // print out empty rows
+ for k := len(row_content); k < (app.screen.Height() - 3); k++ {
+ y := 3 + k
+ if y < app.screen.Height()-1 {
+ app.screen.PrintAt(0, y, app.tlwsbt.EmptyRowContent())
+ }
+ }
+
+ // print out the totals at the bottom
+ app.screen.BoldPrintAt(0, app.screen.Height()-1, app.tlwsbt.TotalRowContent())
+}
+
+func (app *App) displayUsers() {
+ app.screen.BoldPrintAt(0, 2, app.users.Headings())
+
+ // print out the data
+ max_rows := app.screen.Height() - 3
+ row_content := app.users.RowContent(max_rows)
+
+ // print out rows
+ for k := range row_content {
+ y := 3 + k
+ app.screen.PrintAt(0, y, row_content[k])
+ }
+ // print out empty rows
+ for k := len(row_content); k < (app.screen.Height() - 3); k++ {
+ y := 3 + k
+ if y < app.screen.Height()-1 {
+ app.screen.PrintAt(0, y, app.users.EmptyRowContent())
+ }
+ }
+
+ // print out the totals at the bottom
+ app.screen.BoldPrintAt(0, app.screen.Height()-1, app.users.TotalRowContent())
+}
+
+func (app *App) displayMutex() {
+ app.screen.BoldPrintAt(0, 2, app.ewsgben.Headings())
+
+ // print out the data
+ max_rows := app.screen.Height() - 3
+ row_content := app.ewsgben.RowContent(max_rows)
+
+ // print out rows
+ for k := range row_content {
+ y := 3 + k
+ app.screen.PrintAt(0, y, row_content[k])
+ }
+ // print out empty rows
+ for k := len(row_content); k < (app.screen.Height() - 3); k++ {
+ y := 3 + k
+ if y < app.screen.Height()-1 {
+ app.screen.PrintAt(0, y, app.ewsgben.EmptyRowContent())
+ }
+ }
+
+ // print out the totals at the bottom
+ app.screen.BoldPrintAt(0, app.screen.Height()-1, app.ewsgben.TotalRowContent())
+}
+
+func (app *App) displayStages() {
+ app.screen.BoldPrintAt(0, 2, app.essgben.Headings())
+
+ // print out the data
+ max_rows := app.screen.Height() - 3
+ row_content := app.essgben.RowContent(max_rows)
+
+ // print out rows
+ for k := range row_content {
+ y := 3 + k
+ app.screen.PrintAt(0, y, row_content[k])
+ }
+ // print out empty rows
+ for k := len(row_content); k < (app.screen.Height() - 3); k++ {
+ y := 3 + k
+ if y < app.screen.Height()-1 {
+ app.screen.PrintAt(0, y, app.essgben.EmptyRowContent())
+ }
+ }
+
+ // print out the totals at the bottom
+ app.screen.BoldPrintAt(0, app.screen.Height()-1, app.essgben.TotalRowContent())
+}
+
+// do we want to show all p_s data?
+func (app App) WantRelativeStats() bool {
+ return app.want_relative_stats
+}
+
+// set if we want data from when we started/reset stats.
+func (app *App) SetWantRelativeStats(want_relative_stats bool) {
+ app.want_relative_stats = want_relative_stats
+
+ app.fsbi.SetWantRelativeStats(want_relative_stats)
+ app.tlwsbt.SetWantRelativeStats(app.want_relative_stats)
+ app.tiwsbt.SetWantRelativeStats(app.want_relative_stats)
+ app.ewsgben.SetWantRelativeStats(app.want_relative_stats)
+ app.essgben.SetWantRelativeStats(app.want_relative_stats)
+}
+
+// if there's a better way of doing this do it better ...
+func now_hhmmss() string {
+ t := time.Now()
+ return fmt.Sprintf("%2d:%02d:%02d", t.Hour(), t.Minute(), t.Second())
+}
+
+// record the latest screen size
+func (app *App) ScreenSetSize(width, height int) {
+ app.screen.SetSize(width, height)
+}
+
+// clean up screen and disconnect database
+func (app *App) Cleanup() {
+ app.screen.Close()
+ if app.dbh != nil {
+ app.setup_instruments.RestoreConfiguration()
+ _ = app.dbh.Close()
+ }
+}
+
+// make chan for termbox events and run a poller to send events to the channel
+// - return the channel
+func new_tb_chan() chan termbox.Event {
+ termboxChan := make(chan termbox.Event)
+ go func() {
+ for {
+ termboxChan <- termbox.PollEvent()
+ }
+ }()
+ return termboxChan
+}
+
+// get into a run loop
+func (app *App) Run() {
+ done := make(chan struct{})
+ defer close(done)
+
+ sigChan := make(chan os.Signal, 1)
+ signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
+
+ var wi wait_info.WaitInfo
+ wi.SetWaitInterval(time.Second)
+
+ termboxChan := new_tb_chan()
+
+ for !app.Finished() {
+ select {
+ case <-done:
+ fmt.Println("exiting")
+ app.SetFinished()
+ case sig := <-sigChan:
+ fmt.Println("Caught a signal", sig)
+ done <- struct{}{}
+ case <-wi.WaitNextPeriod():
+ app.Collect()
+ wi.CollectedNow()
+ app.Display()
+ case event := <-termboxChan:
+ // switch on event type
+ switch event.Type {
+ case termbox.EventKey: // actions depend on key
+ switch event.Key {
+ case termbox.KeyCtrlZ, termbox.KeyCtrlC, termbox.KeyEsc:
+ app.SetFinished()
+ case termbox.KeyArrowLeft: // left arrow change to previous display mode
+ app.DisplayPrevious()
+ app.Display()
+ case termbox.KeyTab, termbox.KeyArrowRight: // tab or right arrow - change to next display mode
+ app.DisplayNext()
+ app.Display()
+ }
+ switch event.Ch {
+ case '-': // decrease the interval if > 1
+ if wi.WaitInterval() > time.Second {
+ wi.SetWaitInterval(wi.WaitInterval() - time.Second)
+ }
+ case '+': // increase interval by creating a new ticker
+ wi.SetWaitInterval(wi.WaitInterval() + time.Second)
+ case 'h', '?': // help
+ app.SetHelp(!app.Help())
+ case 'q': // quit
+ app.SetFinished()
+ case 't': // toggle between absolute/relative statistics
+ app.SetWantRelativeStats(!app.WantRelativeStats())
+ app.Display()
+ case 'z': // reset the statistics to now by taking a query of current values
+ app.ResetDBStatistics()
+ app.Display()
+ }
+ case termbox.EventResize: // set sizes
+ app.ScreenSetSize(event.Width, event.Height)
+ app.Display()
+ case termbox.EventError: // quit
+ log.Fatalf("Quitting because of termbox error: \n%s\n", event.Err)
+ }
+ }
+ }
+}
+
+// pstop requires MySQL 5.6+ or MariaDB 10.0+. Check the version
+// rather than giving an error message if the requires P_S tables can't
+// be found.
+func (app *App) validate_mysql_version() error {
+ var tables = [...]string{
+ "performance_schema.events_waits_summary_global_by_event_name",
+ "performance_schema.file_summary_by_instance",
+ "performance_schema.table_io_waits_summary_by_table",
+ "performance_schema.table_lock_waits_summary_by_table",
+ }
+
+ lib.Logger.Println("validate_mysql_version()")
+
+ lib.Logger.Println("- Getting MySQL version")
+ err, mysql_version := lib.SelectGlobalVariableByVariableName(app.dbh, "VERSION")
+ if err != nil {
+ return err
+ }
+ lib.Logger.Println("- mysql_version: '" + mysql_version + "'")
+
+ if !re_valid_version.MatchString(mysql_version) {
+ return errors.New(lib.MyName() + " does not work with MySQL version " + mysql_version)
+ }
+ lib.Logger.Println("OK: MySQL version is valid, continuing")
+
+ lib.Logger.Println("Checking access to required tables:")
+ for i := range tables {
+ if err := lib.CheckTableAccess(app.dbh, tables[i]); err == nil {
+ lib.Logger.Println("OK: " + tables[i] + " found")
+ } else {
+ return err
+ }
+ }
+ lib.Logger.Println("OK: all table checks passed")
+
+ return nil
+}
+
--- /dev/null
+// Use Connector to specify how to connect to MySQL.
+// Then get a sql.*DB from it which is returned to the app..
+package connector
+
+import (
+ "database/sql"
+ "log"
+
+ _ "github.com/go-sql-driver/mysql"
+
+ "github.com/sjmudd/mysql_defaults_file"
+ "github.com/sjmudd/pstop/lib"
+)
+
+const (
+ sql_driver = "mysql"
+ db = "performance_schema"
+
+ DEFAULTS_FILE = iota
+ COMPONENTS = iota
+)
+
+// connector struct
+type Connector struct {
+ connectBy int
+ components map[string] string
+ defaults_file string
+ dbh *sql.DB
+}
+
+// return the database handle
+func (c Connector) Handle() *sql.DB {
+ return c.dbh
+}
+
+// return the defaults file
+func (c Connector) DefaultsFile() string {
+ return c.defaults_file
+}
+
+// set the defaults file
+func (c *Connector) SetDefaultsFile( defaults_file string ) {
+ c.defaults_file = defaults_file
+}
+
+// set the components
+func (c *Connector) SetComponents(components map[string]string) {
+ c.components = components
+}
+
+// things to do after connecting
+func (c *Connector) postConnectStuff() {
+ if err := c.dbh.Ping(); err != nil {
+ log.Fatal(err)
+ }
+
+ // deliberately limit the pool size to 5 to avoid "problems" if any queries hang.
+ c.dbh.SetMaxOpenConns(5) // hard-coded value!
+}
+
+// determine how we want to connect
+func (c *Connector) SetConnectBy( connectHow int ) {
+ c.connectBy = connectHow
+}
+
+// make the database connection
+func (c *Connector) Connect() {
+ var err error
+
+ switch {
+ case c.connectBy == DEFAULTS_FILE:
+ lib.Logger.Println("connect_by_defaults_file() connecting to database")
+
+ c.dbh, err = mysql_defaults_file.OpenUsingDefaultsFile(sql_driver, c.defaults_file, db)
+ if err != nil {
+ log.Fatal(err)
+ }
+ case c.connectBy == COMPONENTS:
+ lib.Logger.Println("connect_by_components() connecting to database")
+
+ new_dsn := mysql_defaults_file.BuildDSN(c.components, db)
+ c.dbh, err = sql.Open(sql_driver, new_dsn)
+ if err != nil {
+ log.Fatal(err)
+ }
+ default:
+ log.Fatal("Connector.Connect() c.connectBy not DEFAULTS_FILE/COMPONENTS")
+ }
+ c.postConnectStuff()
+}
+
+// Connect to MySQL using various component parts needed to make the dsn.
+func (c *Connector) ConnectByComponents(components map[string]string) {
+ c.SetComponents(components)
+ c.SetConnectBy(COMPONENTS)
+ c.Connect()
+}
+
+// Connect to the database with the given defaults-file, or ~/.my.cnf if not provided.
+func (c *Connector) ConnectByDefaultsFile(defaults_file string) {
+ c.SetDefaultsFile(defaults_file)
+ c.SetConnectBy(DEFAULTS_FILE)
+ c.Connect()
+}
type Object struct {
p_s.RelativeStats
p_s.InitialTime
- current table_rows // processlist
- results pl_by_user_rows // results by user
- totals pl_by_user_row // totals of results
+ current table_rows // processlist
+ results pl_by_user_rows // results by user
+ totals pl_by_user_row // totals of results
}
// Collect() collects data from the db, updating initial
func NewKeyValueCache() KeyValueCache {
lib.Logger.Println("KeyValueCache()")
- return KeyValueCache {}
+ return KeyValueCache{}
}
// Given a lookup key return the value if found.
}
// like Amount but tigher in space
-func FormatCounter( counter int, width int ) string {
+func FormatCounter(counter int, width int) string {
if counter == 0 {
pattern := "%" + fmt.Sprintf("%d", width) + "s"
- return fmt.Sprintf( pattern, " " )
+ return fmt.Sprintf(pattern, " ")
} else {
pattern := "%" + fmt.Sprintf("%d", width) + "d"
- return fmt.Sprintf( pattern, counter )
+ return fmt.Sprintf(pattern, counter)
}
}
package main
import (
- "database/sql"
- "errors"
"flag"
"fmt"
"log"
"os"
- "os/signal"
- "regexp"
"runtime/pprof"
- "syscall"
- "time"
_ "github.com/go-sql-driver/mysql"
- "github.com/nsf/termbox-go"
- "github.com/sjmudd/mysql_defaults_file"
+ "github.com/sjmudd/pstop/app"
+ "github.com/sjmudd/pstop/connector"
"github.com/sjmudd/pstop/lib"
- "github.com/sjmudd/pstop/state"
"github.com/sjmudd/pstop/version"
- "github.com/sjmudd/pstop/wait_info"
)
const (
flag_defaults_file = flag.String("defaults-file", "", "Provide a defaults-file to use to connect to MySQL")
flag_help = flag.Bool("help", false, "Provide some help for "+lib.MyName())
flag_host = flag.String("host", "", "Provide the hostname of the MySQL to connect to")
- flag_port = flag.Int("port", 0 , "Provide the port number of the MySQL to connect to (default: 3306)") /* deliberately 0 here, defaults to 3306 elsewhere */
+ flag_port = flag.Int("port", 0, "Provide the port number of the MySQL to connect to (default: 3306)") /* deliberately 0 here, defaults to 3306 elsewhere */
flag_socket = flag.String("socket", "", "Provide the path to the local MySQL server to connect to")
flag_password = flag.String("password", "", "Provide the password when connecting to the MySQL server")
flag_user = flag.String("user", "", "Provide the username to connect with to MySQL (default: $USER)")
flag_version = flag.Bool("version", false, "Show the version of "+lib.MyName())
cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")
-
- re_valid_version = regexp.MustCompile(`^(5\.[67]\.|10\.[01])`)
)
-// Connect to the database with the given defaults-file, or ~/.my.cnf if not provided.
-func connect_by_defaults_file( defaults_file string ) *sql.DB {
- var err error
- var dbh *sql.DB
- lib.Logger.Println("connect_by_defaults_file() connecting to database")
-
- dbh, err = mysql_defaults_file.OpenUsingDefaultsFile(sql_driver, defaults_file, "performance_schema")
- if err != nil {
- log.Fatal(err)
- }
- if err = dbh.Ping(); err != nil {
- log.Fatal(err)
- }
-
- // deliberately limit the pool size to 5 to avoid "problems" if any queries hang.
- dbh.SetMaxOpenConns(5) // hard-coded value!
-
- return dbh
-}
-
-// connect to MySQL using various component parts needed to make the dsn
-func connect_by_components( components map[string]string ) *sql.DB {
- var err error
- var dbh *sql.DB
- lib.Logger.Println("connect_by_components() connecting to database")
-
- new_dsn := mysql_defaults_file.BuildDSN(components, "performance_schema")
- dbh, err = sql.Open(sql_driver, new_dsn)
- if err != nil {
- log.Fatal(err)
- }
- if err = dbh.Ping(); err != nil {
- log.Fatal(err)
- }
-
- return dbh
-}
-
-// make chan for termbox events and run a poller to send events to the channel
-// - return the channel
-func new_tb_chan() chan termbox.Event {
- termboxChan := make(chan termbox.Event)
- go func() {
- for {
- termboxChan <- termbox.PollEvent()
- }
- }()
- return termboxChan
-}
-
func usage() {
fmt.Println(lib.MyName() + " - " + lib.Copyright())
fmt.Println("")
fmt.Println("Usage: " + lib.MyName() + " <options>")
fmt.Println("")
fmt.Println("Options:")
- fmt.Println("--defaults-file=/path/to/defaults.file Connect to MySQL using given defaults-file" )
+ fmt.Println("--defaults-file=/path/to/defaults.file Connect to MySQL using given defaults-file")
fmt.Println("--help Show this help message")
fmt.Println("--version Show the version")
fmt.Println("--host=<hostname> MySQL host to connect to")
fmt.Println("--password=<password> Password to use when connecting")
}
-// pstop requires MySQL 5.6+ or MariaDB 10.0+. Check the version
-// rather than giving an error message if the requires P_S tables can't
-// be found.
-func validate_mysql_version(dbh *sql.DB) error {
- var tables = [...]string{
- "performance_schema.events_waits_summary_global_by_event_name",
- "performance_schema.file_summary_by_instance",
- "performance_schema.table_io_waits_summary_by_table",
- "performance_schema.table_lock_waits_summary_by_table",
- }
-
- lib.Logger.Println("validate_mysql_version()")
-
- lib.Logger.Println("- Getting MySQL version")
- err, mysql_version := lib.SelectGlobalVariableByVariableName(dbh, "VERSION")
- if err != nil {
- return err
- }
- lib.Logger.Println("- mysql_version: '" + mysql_version + "'")
-
- if !re_valid_version.MatchString(mysql_version) {
- err := errors.New(lib.MyName() + " does not work with MySQL version " + mysql_version)
- return err
- }
- lib.Logger.Println("OK: MySQL version is valid, continuing")
-
- lib.Logger.Println("Checking access to required tables:")
- for i := range tables {
- if err := lib.CheckTableAccess(dbh, tables[i]); err == nil {
- lib.Logger.Println("OK: " + tables[i] + " found")
- } else {
- return err
- }
- }
- lib.Logger.Println("OK: all table checks passed")
-
- return nil
-}
-
func main() {
var defaults_file string = ""
flag.Parse()
lib.Logger.Println("Starting " + lib.MyName())
- var dbh *sql.DB
+ var connector connector.Connector
if *flag_host != "" || *flag_socket != "" {
lib.Logger.Println("--host= or --socket= defined")
var components = make(map[string]string)
if *flag_host != "" && *flag_socket != "" {
- fmt.Println(lib.MyName() + ": Do not specify --host and --socket together" )
+ fmt.Println(lib.MyName() + ": Do not specify --host and --socket together")
os.Exit(1)
}
if *flag_host != "" {
if *flag_socket == "" {
components["port"] = fmt.Sprintf("%d", *flag_port)
} else {
- fmt.Println(lib.MyName() + ": Do not specify --socket and --port together" )
+ fmt.Println(lib.MyName() + ": Do not specify --socket and --port together")
os.Exit(1)
}
}
if *flag_password != "" {
components["password"] = *flag_password
}
- dbh = connect_by_components( components )
+ connector.ConnectByComponents(components)
} else {
- if flag_defaults_file != nil && *flag_defaults_file != "" {
+ if flag_defaults_file != nil && *flag_defaults_file != "" {
lib.Logger.Println("--defaults-file defined")
defaults_file = *flag_defaults_file
} else {
lib.Logger.Println("connecting by implicit defaults file")
}
- dbh = connect_by_defaults_file( defaults_file )
+ connector.ConnectByDefaultsFile(defaults_file)
}
- if err := validate_mysql_version(dbh); err != nil {
- log.Fatal(err)
- }
-
- var state state.State
- var wi wait_info.WaitInfo
- wi.SetWaitInterval(time.Second)
-
- sigChan := make(chan os.Signal, 1)
- done := make(chan struct{})
- defer close(done)
- termboxChan := new_tb_chan()
+ var app app.App
- signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
-
- state.Setup(dbh)
- for !state.Finished() {
- select {
- case <-done:
- fmt.Println("exiting")
- state.SetFinished()
- case sig := <-sigChan:
- fmt.Println("Caught a signal", sig)
- done <- struct{}{}
- case <-wi.WaitNextPeriod():
- state.Collect()
- wi.CollectedNow()
- state.Display()
- case event := <-termboxChan:
- // switch on event type
- switch event.Type {
- case termbox.EventKey: // actions depend on key
- switch event.Key {
- case termbox.KeyCtrlZ, termbox.KeyCtrlC, termbox.KeyEsc:
- state.SetFinished()
- case termbox.KeyArrowLeft: // left arrow change to previous display mode
- state.DisplayPrevious()
- state.Display()
- case termbox.KeyTab, termbox.KeyArrowRight: // tab or right arrow - change to next display mode
- state.DisplayNext()
- state.Display()
- }
- switch event.Ch {
- case '-': // decrease the interval if > 1
- if wi.WaitInterval() > time.Second {
- wi.SetWaitInterval(wi.WaitInterval() - time.Second)
- }
- case '+': // increase interval by creating a new ticker
- wi.SetWaitInterval(wi.WaitInterval() + time.Second)
- case 'h', '?': // help
- state.SetHelp(!state.Help())
- case 'q': // quit
- state.SetFinished()
- case 't': // toggle between absolute/relative statistics
- state.SetWantRelativeStats(!state.WantRelativeStats())
- state.Display()
- case 'z': // reset the statistics to now by taking a query of current values
- state.ResetDBStatistics()
- state.Display()
- }
- case termbox.EventResize: // set sizes
- state.ScreenSetSize(event.Width, event.Height)
- state.Display()
- case termbox.EventError: // quit
- log.Fatalf("Quitting because of termbox error: \n%s\n", event.Err)
- }
- }
- }
- state.Cleanup()
+ app.Setup(connector.Handle())
+ app.Run()
+ app.Cleanup()
lib.Logger.Println("Terminating " + lib.MyName())
}
// Return a newly initialised SetupInstruments structure with a handle to the database.
// Better to return a pointer ?
func NewSetupInstruments(dbh *sql.DB) SetupInstruments {
- return SetupInstruments{ dbh: dbh }
+ return SetupInstruments{dbh: dbh}
}
// enable mutex and stage monitoring
count = 0
for i := range si.rows {
lib.Logger.Println("- changing row:", si.rows[i].NAME)
- lib.Logger.Println("stmt.Exec", "YES", "YES", si.rows[i].NAME )
+ lib.Logger.Println("stmt.Exec", "YES", "YES", si.rows[i].NAME)
if res, err := stmt.Exec("YES", "YES", si.rows[i].NAME); err == nil {
lib.Logger.Println("update succeeded")
si.update_succeeded = true
}
stmt.Close()
}
- lib.Logger.Println( "Configure() returns update_tried", si.update_tried, ", update_succeeded", si.update_succeeded)
+ lib.Logger.Println("Configure() returns update_tried", si.update_tried, ", update_succeeded", si.update_succeeded)
}
// restore setup_instruments rows to their previous settings
// update the rows which need to be set - do multiple updates but I don't care
update_sql := "UPDATE setup_instruments SET enabled = ?, TIMED = ? WHERE NAME = ?"
- lib.Logger.Println("dbh.Prepare(",update_sql,")")
+ lib.Logger.Println("dbh.Prepare(", update_sql, ")")
stmt, err := si.dbh.Prepare(update_sql)
if err != nil {
log.Fatal(err)
}
count := 0
for i := range si.rows {
- lib.Logger.Println("stmt.Exec(",si.rows[i].ENABLED, si.rows[i].TIMED, si.rows[i].NAME,")")
+ lib.Logger.Println("stmt.Exec(", si.rows[i].ENABLED, si.rows[i].TIMED, si.rows[i].NAME, ")")
if _, err := stmt.Exec(si.rows[i].ENABLED, si.rows[i].TIMED, si.rows[i].NAME); err != nil {
log.Fatal(err)
}
offset := 0
for c := range text {
if (x + offset) < s.width {
- termbox.SetCell(x+offset, y, rune(text[c]), s.fg | termbox.AttrBold, s.bg)
+ termbox.SetCell(x+offset, y, rune(text[c]), s.fg|termbox.AttrBold, s.bg)
offset++
}
}
+++ /dev/null
-// lib - library routines for pstop.
-//
-// this file contains the library routines related to the stored state in pstop.
-package state
-
-import (
- "database/sql"
- "fmt"
- "strings"
- "time"
-
- "github.com/sjmudd/pstop/i_s/processlist"
- "github.com/sjmudd/pstop/lib"
- ewsgben "github.com/sjmudd/pstop/p_s/events_waits_summary_global_by_event_name"
- essgben "github.com/sjmudd/pstop/p_s/events_stages_summary_global_by_event_name"
- fsbi "github.com/sjmudd/pstop/p_s/file_summary_by_instance"
- "github.com/sjmudd/pstop/p_s/ps_table"
- "github.com/sjmudd/pstop/p_s/setup_instruments"
- tiwsbt "github.com/sjmudd/pstop/p_s/table_io_waits_summary_by_table"
- tlwsbt "github.com/sjmudd/pstop/p_s/table_lock_waits_summary_by_table"
- "github.com/sjmudd/pstop/screen"
- "github.com/sjmudd/pstop/version"
- "github.com/sjmudd/pstop/wait_info"
-)
-
-// what information to show
-type Show int
-
-const (
- showLatency = iota
- showOps = iota
- showIO = iota
- showLocks = iota
- showUsers = iota
- showMutex = iota
- showStages = iota
-)
-
-type State struct {
- finished bool
- datadir string
- dbh *sql.DB
- help bool
- hostname string
- fsbi ps_table.Tabler // ufsbi.File_summary_by_instance
- tiwsbt tiwsbt.Object
- tlwsbt ps_table.Tabler // tlwsbt.Table_lock_waits_summary_by_table
- ewsgben ps_table.Tabler // ewsgben.Events_waits_summary_global_by_event_name
- essgben ps_table.Tabler // essgben.Events_stages_summary_global_by_event_name
- users processlist.Object
- screen screen.TermboxScreen
- show Show
- mysql_version string
- want_relative_stats bool
- wait_info.WaitInfo // embedded
- setup_instruments setup_instruments.SetupInstruments
-}
-
-func (state *State) Setup(dbh *sql.DB) {
- state.dbh = dbh
- state.finished = false
-
- state.screen.Initialise()
-
- state.setup_instruments = setup_instruments.NewSetupInstruments(dbh)
- state.setup_instruments.EnableMonitoring()
-
- _, variables := lib.SelectAllGlobalVariablesByVariableName(state.dbh)
- // setup to their initial types/values
- state.fsbi = fsbi.NewFileSummaryByInstance(variables)
- state.tlwsbt = new(tlwsbt.Object)
- state.ewsgben = new(ewsgben.Object)
- state.essgben = new(essgben.Object)
-
- state.want_relative_stats = true // we show info from the point we start collecting data
- state.fsbi.SetWantRelativeStats(state.want_relative_stats)
- state.fsbi.SetNow()
- state.tlwsbt.SetWantRelativeStats(state.want_relative_stats)
- state.tlwsbt.SetNow()
- state.tiwsbt.SetWantRelativeStats(state.want_relative_stats)
- state.tiwsbt.SetNow()
- state.users.SetWantRelativeStats(state.want_relative_stats) // ignored
- state.users.SetNow() // ignored
- state.essgben.SetWantRelativeStats(state.want_relative_stats)
- state.essgben.SetNow()
- state.ewsgben.SetWantRelativeStats(state.want_relative_stats) // ignored
- state.ewsgben.SetNow() // ignored
-
- state.ResetDBStatistics()
-
- state.SetHelp(false)
- state.show = showLatency
- state.tiwsbt.SetWantsLatency(true)
-
- // get short name (to save space)
- _, hostname := lib.SelectGlobalVariableByVariableName(state.dbh, "HOSTNAME")
- if index := strings.Index(hostname, "."); index >= 0 {
- hostname = hostname[0:index]
- }
- _, mysql_version := lib.SelectGlobalVariableByVariableName(state.dbh, "VERSION")
- _, datadir := lib.SelectGlobalVariableByVariableName(state.dbh, "DATADIR")
- state.SetHostname(hostname)
- state.SetMySQLVersion(mysql_version)
- state.SetDatadir(datadir)
-}
-
-// have we finished ?
-func (state State) Finished() bool {
- return state.finished
-}
-
-// indicate we have finished
-func (state *State) SetFinished() {
- state.finished = true
-}
-
-// do a fresh collection of data and then update the initial values based on that.
-func (state *State) ResetDBStatistics() {
- state.CollectAll()
- state.SyncReferenceValues()
-}
-
-func (state *State) SyncReferenceValues() {
- start := time.Now()
- state.fsbi.SyncReferenceValues()
- state.tlwsbt.SyncReferenceValues()
- state.tiwsbt.SyncReferenceValues()
- state.essgben.SyncReferenceValues()
- lib.Logger.Println("state.SyncReferenceValues() took", time.Duration(time.Since(start)).String())
-}
-
-// collect all initial values on startup / reset
-func (state *State) CollectAll() {
- state.fsbi.Collect(state.dbh)
- state.tlwsbt.Collect(state.dbh)
- state.tiwsbt.Collect(state.dbh)
-}
-
-// Only collect the data we are looking at.
-func (state *State) Collect() {
- start := time.Now()
-
- switch state.show {
- case showLatency, showOps:
- state.tiwsbt.Collect(state.dbh)
- case showIO:
- state.fsbi.Collect(state.dbh)
- case showLocks:
- state.tlwsbt.Collect(state.dbh)
- case showUsers:
- state.users.Collect(state.dbh)
- case showMutex:
- state.ewsgben.Collect(state.dbh)
- case showStages:
- state.essgben.Collect(state.dbh)
- }
- lib.Logger.Println("state.Collect() took", time.Duration(time.Since(start)).String())
-}
-
-func (state State) MySQLVersion() string {
- return state.mysql_version
-}
-
-func (state State) Datadir() string {
- return state.datadir
-}
-
-func (state *State) SetHelp(newHelp bool) {
- state.help = newHelp
-
- state.screen.Clear()
- state.screen.Flush()
-}
-
-func (state *State) SetDatadir(datadir string) {
- state.datadir = datadir
-}
-
-func (state *State) SetMySQLVersion(mysql_version string) {
- state.mysql_version = mysql_version
-}
-
-func (state *State) SetHostname(hostname string) {
- state.hostname = hostname
-}
-
-func (state State) Help() bool {
- return state.help
-}
-
-// states go: showLatency -> showOps -> showIO -> showLocks -> showUsers -> showMutex -> showStages
-
-// display the output according to the mode we are in
-func (state *State) Display() {
- if state.help {
- state.screen.DisplayHelp()
- } else {
- state.displayHeading()
- switch state.show {
- case showLatency, showOps:
- state.displayOpsOrLatency()
- case showIO:
- state.displayIO()
- case showLocks:
- state.displayLocks()
- case showUsers:
- state.displayUsers()
- case showMutex:
- state.displayMutex()
- case showStages:
- state.displayStages()
- }
- }
-}
-
-// fix_latency_setting() ensures the SetWantsLatency() value is
-// correct. This needs to be done more cleanly.
-func (state *State) fix_latency_setting() {
- if state.show == showLatency {
- state.tiwsbt.SetWantsLatency(true)
- }
- if state.show == showOps {
- state.tiwsbt.SetWantsLatency(false)
- }
-}
-
-// change to the previous display mode
-func (state *State) DisplayPrevious() {
- if state.show == showLatency {
- state.show = showStages
- } else {
- state.show--
- }
- state.fix_latency_setting()
- state.screen.Clear()
- state.screen.Flush()
-}
-
-// change to the next display mode
-func (state *State) DisplayNext() {
- if state.show == showStages {
- state.show = showLatency
- } else {
- state.show++
- }
- state.fix_latency_setting()
- state.screen.Clear()
- state.screen.Flush()
-}
-
-func (state State) displayHeading() {
- state.displayLine0()
- state.displayDescription()
-}
-
-func (state State) displayLine0() {
- _, uptime := lib.SelectGlobalStatusByVariableName(state.dbh, "UPTIME")
- top_line := lib.MyName() + " " + version.Version() + " - " + now_hhmmss() + " " + state.hostname + " / " + state.mysql_version + ", up " + fmt.Sprintf("%-16s", lib.Uptime(uptime))
- if state.want_relative_stats {
- now := time.Now()
-
- var initial time.Time
-
- switch state.show {
- case showLatency, showOps:
- initial = state.tiwsbt.Last()
- case showIO:
- initial = state.fsbi.Last()
- case showLocks:
- initial = state.tlwsbt.Last()
- case showUsers:
- initial = state.users.Last()
- case showStages:
- initial = state.essgben.Last()
- case showMutex:
- initial = state.ewsgben.Last()
- default:
- // should not get here !
- }
-
- d := now.Sub(initial)
-
- top_line = top_line + " [REL] " + fmt.Sprintf("%.0f seconds", d.Seconds())
- } else {
- top_line = top_line + " [ABS] "
- }
- state.screen.PrintAt(0, 0, top_line)
-}
-
-func (state State) displayDescription() {
- description := "UNKNOWN"
-
- switch state.show {
- case showLatency, showOps:
- description = state.tiwsbt.Description()
- case showIO:
- description = state.fsbi.Description()
- case showLocks:
- description = state.tlwsbt.Description()
- case showUsers:
- description = state.users.Description()
- case showMutex:
- description = state.ewsgben.Description()
- case showStages:
- description = state.essgben.Description()
- }
-
- state.screen.PrintAt(0, 1, description)
-}
-
-func (state *State) displayOpsOrLatency() {
- state.screen.BoldPrintAt(0, 2, state.tiwsbt.Headings())
-
- max_rows := state.screen.Height() - 3
- row_content := state.tiwsbt.RowContent(max_rows)
-
- // print out rows
- for k := range row_content {
- y := 3 + k
- state.screen.PrintAt(0, y, row_content[k])
- }
- // print out empty rows
- for k := len(row_content); k < (state.screen.Height() - 3); k++ {
- y := 3 + k
- if y < state.screen.Height()-1 {
- state.screen.PrintAt(0, y, state.tiwsbt.EmptyRowContent())
- }
- }
-
- // print out the totals at the bottom
- state.screen.BoldPrintAt(0, state.screen.Height()-1, state.tiwsbt.TotalRowContent())
-}
-
-// show actual I/O latency values
-func (state State) displayIO() {
- state.screen.BoldPrintAt(0, 2, state.fsbi.Headings())
-
- // print out the data
- max_rows := state.screen.Height() - 3
- row_content := state.fsbi.RowContent(max_rows)
-
- // print out rows
- for k := range row_content {
- y := 3 + k
- state.screen.PrintAt(0, y, row_content[k])
- }
- // print out empty rows
- for k := len(row_content); k < (state.screen.Height() - 3); k++ {
- y := 3 + k
- if y < state.screen.Height()-1 {
- state.screen.PrintAt(0, y, state.fsbi.EmptyRowContent())
- }
- }
-
- // print out the totals at the bottom
- state.screen.BoldPrintAt(0, state.screen.Height()-1, state.fsbi.TotalRowContent())
-}
-
-func (state *State) displayLocks() {
- state.screen.BoldPrintAt(0, 2, state.tlwsbt.Headings())
-
- // print out the data
- max_rows := state.screen.Height() - 3
- row_content := state.tlwsbt.RowContent(max_rows)
-
- // print out rows
- for k := range row_content {
- y := 3 + k
- state.screen.PrintAt(0, y, row_content[k])
- }
- // print out empty rows
- for k := len(row_content); k < (state.screen.Height() - 3); k++ {
- y := 3 + k
- if y < state.screen.Height()-1 {
- state.screen.PrintAt(0, y, state.tlwsbt.EmptyRowContent())
- }
- }
-
- // print out the totals at the bottom
- state.screen.BoldPrintAt(0, state.screen.Height()-1, state.tlwsbt.TotalRowContent())
-}
-
-func (state *State) displayUsers() {
- state.screen.BoldPrintAt(0, 2, state.users.Headings())
-
- // print out the data
- max_rows := state.screen.Height() - 3
- row_content := state.users.RowContent(max_rows)
-
- // print out rows
- for k := range row_content {
- y := 3 + k
- state.screen.PrintAt(0, y, row_content[k])
- }
- // print out empty rows
- for k := len(row_content); k < (state.screen.Height() - 3); k++ {
- y := 3 + k
- if y < state.screen.Height()-1 {
- state.screen.PrintAt(0, y, state.users.EmptyRowContent())
- }
- }
-
- // print out the totals at the bottom
- state.screen.BoldPrintAt(0, state.screen.Height()-1, state.users.TotalRowContent())
-}
-
-func (state *State) displayMutex() {
- state.screen.BoldPrintAt(0, 2, state.ewsgben.Headings())
-
- // print out the data
- max_rows := state.screen.Height() - 3
- row_content := state.ewsgben.RowContent(max_rows)
-
- // print out rows
- for k := range row_content {
- y := 3 + k
- state.screen.PrintAt(0, y, row_content[k])
- }
- // print out empty rows
- for k := len(row_content); k < (state.screen.Height() - 3); k++ {
- y := 3 + k
- if y < state.screen.Height()-1 {
- state.screen.PrintAt(0, y, state.ewsgben.EmptyRowContent())
- }
- }
-
- // print out the totals at the bottom
- state.screen.BoldPrintAt(0, state.screen.Height()-1, state.ewsgben.TotalRowContent())
-}
-
-func (state *State) displayStages() {
- state.screen.BoldPrintAt(0, 2, state.essgben.Headings())
-
- // print out the data
- max_rows := state.screen.Height() - 3
- row_content := state.essgben.RowContent(max_rows)
-
- // print out rows
- for k := range row_content {
- y := 3 + k
- state.screen.PrintAt(0, y, row_content[k])
- }
- // print out empty rows
- for k := len(row_content); k < (state.screen.Height() - 3); k++ {
- y := 3 + k
- if y < state.screen.Height()-1 {
- state.screen.PrintAt(0, y, state.essgben.EmptyRowContent())
- }
- }
-
- // print out the totals at the bottom
- state.screen.BoldPrintAt(0, state.screen.Height()-1, state.essgben.TotalRowContent())
-}
-
-
-// do we want to show all p_s data?
-func (state State) WantRelativeStats() bool {
- return state.want_relative_stats
-}
-
-// set if we want data from when we started/reset stats.
-func (state *State) SetWantRelativeStats(want_relative_stats bool) {
- state.want_relative_stats = want_relative_stats
-
- state.fsbi.SetWantRelativeStats(want_relative_stats)
- state.tlwsbt.SetWantRelativeStats(state.want_relative_stats)
- state.tiwsbt.SetWantRelativeStats(state.want_relative_stats)
- state.ewsgben.SetWantRelativeStats(state.want_relative_stats)
- state.essgben.SetWantRelativeStats(state.want_relative_stats)
-}
-
-// if there's a better way of doing this do it better ...
-func now_hhmmss() string {
- t := time.Now()
- return fmt.Sprintf("%2d:%02d:%02d", t.Hour(), t.Minute(), t.Second())
-}
-
-// record the latest screen size
-func (state *State) ScreenSetSize(width, height int) {
- state.screen.SetSize(width, height)
-}
-
-// clean up screen and disconnect database
-func (state *State) Cleanup() {
- state.screen.Close()
- if state.dbh != nil {
- state.setup_instruments.RestoreConfiguration()
- _ = state.dbh.Close()
- }
-}
package version
const (
- version = "0.3.2"
+ version = "0.3.3"
)
// return the current application version