Sensibly format rows with table name on the right.
[pstop.git] / app / app.go
1 // app - pstop application package
2 //
3 // This file contains the library routines related to running the app.
4 package app
5
6 import (
7         "database/sql"
8         "errors"
9         "fmt"
10         "log"
11         "os"
12         "os/signal"
13         "regexp"
14         "strings"
15         "syscall"
16         "time"
17
18         "github.com/nsf/termbox-go"
19
20         "github.com/sjmudd/pstop/i_s/processlist"
21         "github.com/sjmudd/pstop/lib"
22         essgben "github.com/sjmudd/pstop/p_s/events_stages_summary_global_by_event_name"
23         ewsgben "github.com/sjmudd/pstop/p_s/events_waits_summary_global_by_event_name"
24         fsbi "github.com/sjmudd/pstop/p_s/file_summary_by_instance"
25         "github.com/sjmudd/pstop/p_s/ps_table"
26         "github.com/sjmudd/pstop/p_s/setup_instruments"
27         tiwsbt "github.com/sjmudd/pstop/p_s/table_io_waits_summary_by_table"
28         tlwsbt "github.com/sjmudd/pstop/p_s/table_lock_waits_summary_by_table"
29         "github.com/sjmudd/pstop/screen"
30         "github.com/sjmudd/pstop/version"
31         "github.com/sjmudd/pstop/wait_info"
32 )
33
34 // what information to show
35 type Show int
36
37 const (
38         showLatency = iota
39         showOps     = iota
40         showIO      = iota
41         showLocks   = iota
42         showUsers   = iota
43         showMutex   = iota
44         showStages  = iota
45 )
46
47 var (
48         re_valid_version = regexp.MustCompile(`^(5\.[67]\.|10\.[01])`)
49 )
50
51 type App struct {
52         done                chan struct{}
53         sigChan             chan os.Signal
54         wi                  wait_info.WaitInfo
55         finished            bool
56         dbh                 *sql.DB
57         help                bool
58         hostname            string
59         fsbi                ps_table.Tabler // ufsbi.File_summary_by_instance
60         tiwsbt              tiwsbt.Object
61         tlwsbt              ps_table.Tabler // tlwsbt.Table_lock_waits_summary_by_table
62         ewsgben             ps_table.Tabler // ewsgben.Events_waits_summary_global_by_event_name
63         essgben             ps_table.Tabler // essgben.Events_stages_summary_global_by_event_name
64         users               processlist.Object
65         screen              screen.TermboxScreen
66         show                Show
67         mysql_version       string
68         want_relative_stats bool
69         wait_info.WaitInfo  // embedded
70         setup_instruments   setup_instruments.SetupInstruments
71 }
72
73 func (app *App) Setup(dbh *sql.DB) {
74         app.dbh = dbh
75
76         if err := app.validate_mysql_version(); err != nil {
77                 log.Fatal(err)
78         }
79
80         app.finished = false
81         app.screen.Initialise()
82         app.setup_instruments = setup_instruments.NewSetupInstruments(dbh)
83         app.setup_instruments.EnableMonitoring()
84
85         _, variables := lib.SelectAllGlobalVariablesByVariableName(app.dbh)
86         // setup to their initial types/values
87         app.fsbi = fsbi.NewFileSummaryByInstance(variables)
88         app.tlwsbt = new(tlwsbt.Object)
89         app.ewsgben = new(ewsgben.Object)
90         app.essgben = new(essgben.Object)
91
92         app.want_relative_stats = true // we show info from the point we start collecting data
93         app.fsbi.SetWantRelativeStats(app.want_relative_stats)
94         app.fsbi.SetNow()
95         app.tlwsbt.SetWantRelativeStats(app.want_relative_stats)
96         app.tlwsbt.SetNow()
97         app.tiwsbt.SetWantRelativeStats(app.want_relative_stats)
98         app.tiwsbt.SetNow()
99         app.users.SetWantRelativeStats(app.want_relative_stats) // ignored
100         app.users.SetNow()                                      // ignored
101         app.essgben.SetWantRelativeStats(app.want_relative_stats)
102         app.essgben.SetNow()
103         app.ewsgben.SetWantRelativeStats(app.want_relative_stats) // ignored
104         app.ewsgben.SetNow()                                      // ignored
105
106         app.ResetDBStatistics()
107
108         app.SetHelp(false)
109         app.show = showLatency
110         app.tiwsbt.SetWantsLatency(true)
111
112         // get short name (to save space)
113         _, hostname := lib.SelectGlobalVariableByVariableName(app.dbh, "HOSTNAME")
114         if index := strings.Index(hostname, "."); index >= 0 {
115                 hostname = hostname[0:index]
116         }
117         _, mysql_version := lib.SelectGlobalVariableByVariableName(app.dbh, "VERSION")
118         app.SetHostname(hostname)
119         app.SetMySQLVersion(mysql_version)
120 }
121
122 // have we finished ?
123 func (app App) Finished() bool {
124         return app.finished
125 }
126
127 // indicate we have finished
128 func (app *App) SetFinished() {
129         app.finished = true
130 }
131
132 // do a fresh collection of data and then update the initial values based on that.
133 func (app *App) ResetDBStatistics() {
134         app.CollectAll()
135         app.SyncReferenceValues()
136 }
137
138 func (app *App) SyncReferenceValues() {
139         start := time.Now()
140         app.fsbi.SyncReferenceValues()
141         app.tlwsbt.SyncReferenceValues()
142         app.tiwsbt.SyncReferenceValues()
143         app.essgben.SyncReferenceValues()
144         lib.Logger.Println("app.SyncReferenceValues() took", time.Duration(time.Since(start)).String())
145 }
146
147 // collect all initial values on startup / reset
148 func (app *App) CollectAll() {
149         app.fsbi.Collect(app.dbh)
150         app.tlwsbt.Collect(app.dbh)
151         app.tiwsbt.Collect(app.dbh)
152 }
153
154 // Only collect the data we are looking at.
155 func (app *App) Collect() {
156         start := time.Now()
157
158         switch app.show {
159         case showLatency, showOps:
160                 app.tiwsbt.Collect(app.dbh)
161         case showIO:
162                 app.fsbi.Collect(app.dbh)
163         case showLocks:
164                 app.tlwsbt.Collect(app.dbh)
165         case showUsers:
166                 app.users.Collect(app.dbh)
167         case showMutex:
168                 app.ewsgben.Collect(app.dbh)
169         case showStages:
170                 app.essgben.Collect(app.dbh)
171         }
172         app.wi.CollectedNow()
173         lib.Logger.Println("app.Collect() took", time.Duration(time.Since(start)).String())
174 }
175
176 func (app App) MySQLVersion() string {
177         return app.mysql_version
178 }
179
180 func (app *App) SetHelp(newHelp bool) {
181         app.help = newHelp
182
183         app.screen.Clear()
184         app.screen.Flush()
185 }
186
187 func (app *App) SetMySQLVersion(mysql_version string) {
188         app.mysql_version = mysql_version
189 }
190
191 func (app *App) SetHostname(hostname string) {
192         app.hostname = hostname
193 }
194
195 func (app App) Help() bool {
196         return app.help
197 }
198
199 // apps go: showLatency -> showOps -> showIO -> showLocks -> showUsers -> showMutex -> showStages
200
201 // display the output according to the mode we are in
202 func (app *App) Display() {
203         if app.help {
204                 app.screen.DisplayHelp()
205         } else {
206                 app.displayHeading()
207                 switch app.show {
208                 case showLatency, showOps:
209                         app.displayOpsOrLatency()
210                 case showIO:
211                         app.displayIO()
212                 case showLocks:
213                         app.displayLocks()
214                 case showUsers:
215                         app.displayUsers()
216                 case showMutex:
217                         app.displayMutex()
218                 case showStages:
219                         app.displayStages()
220                 }
221         }
222 }
223
224 // fix_latency_setting() ensures the SetWantsLatency() value is
225 // correct. This needs to be done more cleanly.
226 func (app *App) fix_latency_setting() {
227         if app.show == showLatency {
228                 app.tiwsbt.SetWantsLatency(true)
229         }
230         if app.show == showOps {
231                 app.tiwsbt.SetWantsLatency(false)
232         }
233 }
234
235 // change to the previous display mode
236 func (app *App) DisplayPrevious() {
237         if app.show == showLatency {
238                 app.show = showStages
239         } else {
240                 app.show--
241         }
242         app.fix_latency_setting()
243         app.screen.Clear()
244         app.screen.Flush()
245 }
246
247 // change to the next display mode
248 func (app *App) DisplayNext() {
249         if app.show == showStages {
250                 app.show = showLatency
251         } else {
252                 app.show++
253         }
254         app.fix_latency_setting()
255         app.screen.Clear()
256         app.screen.Flush()
257 }
258
259 func (app App) displayHeading() {
260         app.displayLine0()
261         app.displayDescription()
262 }
263
264 func (app App) displayLine0() {
265         _, uptime := lib.SelectGlobalStatusByVariableName(app.dbh, "UPTIME")
266         top_line := lib.MyName() + " " + version.Version() + " - " + now_hhmmss() + " " + app.hostname + " / " + app.mysql_version + ", up " + fmt.Sprintf("%-16s", lib.Uptime(uptime))
267         if app.want_relative_stats {
268                 now := time.Now()
269
270                 var initial time.Time
271
272                 switch app.show {
273                 case showLatency, showOps:
274                         initial = app.tiwsbt.Last()
275                 case showIO:
276                         initial = app.fsbi.Last()
277                 case showLocks:
278                         initial = app.tlwsbt.Last()
279                 case showUsers:
280                         initial = app.users.Last()
281                 case showStages:
282                         initial = app.essgben.Last()
283                 case showMutex:
284                         initial = app.ewsgben.Last()
285                 default:
286                         // should not get here !
287                 }
288
289                 d := now.Sub(initial)
290
291                 top_line = top_line + " [REL] " + fmt.Sprintf("%.0f seconds", d.Seconds())
292         } else {
293                 top_line = top_line + " [ABS]             "
294         }
295         app.screen.PrintAt(0, 0, top_line)
296 }
297
298 func (app App) displayDescription() {
299         description := "UNKNOWN"
300
301         switch app.show {
302         case showLatency, showOps:
303                 description = app.tiwsbt.Description()
304         case showIO:
305                 description = app.fsbi.Description()
306         case showLocks:
307                 description = app.tlwsbt.Description()
308         case showUsers:
309                 description = app.users.Description()
310         case showMutex:
311                 description = app.ewsgben.Description()
312         case showStages:
313                 description = app.essgben.Description()
314         }
315
316         app.screen.PrintAt(0, 1, description)
317 }
318
319 func (app *App) displayOpsOrLatency() {
320         app.screen.BoldPrintAt(0, 2, app.tiwsbt.Headings())
321
322         max_rows := app.screen.Height() - 3
323         last_row := app.screen.Height() - 1
324         row_content := app.tiwsbt.RowContent(max_rows)
325
326         // print out rows
327         for k := range row_content {
328                 y := 3 + k
329                 app.screen.PrintAt(0, y, row_content[k])
330                 app.screen.ClearLine(len(row_content[k]), y)
331         }
332         // print out empty rows
333         for k := len(row_content); k < max_rows; k++ {
334                 y := 3 + k
335                 if y < max_rows - 1 {
336                         app.screen.PrintAt(0, y, app.tiwsbt.EmptyRowContent())
337                 }
338         }
339
340         // print out the totals at the bottom
341         total := app.tiwsbt.TotalRowContent()
342         app.screen.BoldPrintAt(0, last_row, total)
343         app.screen.ClearLine(len(total), last_row)
344 }
345
346 // show actual I/O latency values
347 func (app App) displayIO() {
348         app.screen.BoldPrintAt(0, 2, app.fsbi.Headings())
349
350         // print out the data
351         max_rows := app.screen.Height() - 4
352         last_row := app.screen.Height() - 1
353         row_content := app.fsbi.RowContent(max_rows)
354
355         // print out rows
356         for k := range row_content {
357                 y := 3 + k
358                 app.screen.PrintAt(0, y, row_content[k])
359                 app.screen.ClearLine(len(row_content[k]), y)
360         }
361         // print out empty rows
362         for k := len(row_content); k < max_rows; k++ {
363                 y := 3 + k
364                 if y < last_row {
365                         app.screen.PrintAt(0, y, app.fsbi.EmptyRowContent())
366                 }
367         }
368
369         // print out the totals at the bottom
370         total := app.fsbi.TotalRowContent()
371         app.screen.BoldPrintAt(0, last_row, total)
372         app.screen.ClearLine(len(total), last_row)
373 }
374
375 func (app *App) displayLocks() {
376         app.screen.BoldPrintAt(0, 2, app.tlwsbt.Headings())
377
378         // print out the data
379         max_rows := app.screen.Height() - 4
380         last_row := app.screen.Height() - 1
381         row_content := app.tlwsbt.RowContent(max_rows)
382
383         // print out rows
384         for k := range row_content {
385                 y := 3 + k
386                 app.screen.PrintAt(0, y, row_content[k])
387                 app.screen.ClearLine(len(row_content[k]), y)
388         }
389         // print out empty rows
390         for k := len(row_content); k < (app.screen.Height() - 3); k++ {
391                 y := 3 + k
392                 if y < last_row {
393                         app.screen.PrintAt(0, y, app.tlwsbt.EmptyRowContent())
394                 }
395         }
396
397         // print out the totals at the bottom
398         total := app.tlwsbt.TotalRowContent()
399         app.screen.BoldPrintAt(0, last_row, total)
400         app.screen.ClearLine(len(total), last_row)
401 }
402
403 func (app *App) displayUsers() {
404         app.screen.BoldPrintAt(0, 2, app.users.Headings())
405
406         // print out the data
407         max_rows := app.screen.Height() - 4
408         last_row := app.screen.Height() - 1
409         row_content := app.users.RowContent(max_rows)
410
411         // print out rows
412         for k := range row_content {
413                 y := 3 + k
414                 app.screen.PrintAt(0, y, row_content[k])
415                 app.screen.ClearLine(len(row_content[k]), y)
416         }
417         // print out empty rows
418         for k := len(row_content); k < max_rows; k++ {
419                 y := 3 + k
420                 if y < last_row {
421                         app.screen.PrintAt(0, y, app.users.EmptyRowContent())
422                 }
423         }
424
425         // print out the totals at the bottom
426         total := app.users.TotalRowContent()
427         app.screen.BoldPrintAt(0, last_row, total)
428         app.screen.ClearLine(len(total), last_row)
429 }
430
431 func (app *App) displayMutex() {
432         app.screen.BoldPrintAt(0, 2, app.ewsgben.Headings())
433
434         // print out the data
435         max_rows := app.screen.Height() - 4
436         last_row := app.screen.Height() - 1
437         row_content := app.ewsgben.RowContent(max_rows)
438
439         // print out rows
440         for k := range row_content {
441                 y := 3 + k
442                 app.screen.PrintAt(0, y, row_content[k])
443                 app.screen.ClearLine(len(row_content[k]), y)
444         }
445         // print out empty rows
446         for k := len(row_content); k < max_rows; k++ {
447                 y := 3 + k
448                 if y < last_row {
449                         app.screen.PrintAt(0, y, app.ewsgben.EmptyRowContent())
450                 }
451         }
452
453         // print out the totals at the bottom
454         total := app.ewsgben.TotalRowContent()
455         app.screen.BoldPrintAt(0, last_row, total)
456         app.screen.ClearLine(len(total), last_row)
457 }
458
459 func (app *App) displayStages() {
460         app.screen.BoldPrintAt(0, 2, app.essgben.Headings())
461
462         // print out the data
463         max_rows := app.screen.Height() - 4
464         last_row := app.screen.Height() - 1
465         row_content := app.essgben.RowContent(max_rows)
466
467         // print out rows
468         for k := range row_content {
469                 y := 3 + k
470                 app.screen.PrintAt(0, y, row_content[k])
471                 app.screen.ClearLine(len(row_content[k]), y)
472         }
473         // print out empty rows
474         for k := len(row_content); k < max_rows; k++ {
475                 y := 3 + k
476                 if y < last_row {
477                         app.screen.PrintAt(0, y, app.essgben.EmptyRowContent())
478                 }
479         }
480
481         // print out the totals at the bottom
482         total := app.essgben.TotalRowContent()
483         app.screen.BoldPrintAt(0, last_row, total)
484         app.screen.ClearLine(len(total), last_row)
485 }
486
487 // do we want to show all p_s data?
488 func (app App) WantRelativeStats() bool {
489         return app.want_relative_stats
490 }
491
492 // set if we want data from when we started/reset stats.
493 func (app *App) SetWantRelativeStats(want_relative_stats bool) {
494         app.want_relative_stats = want_relative_stats
495
496         app.fsbi.SetWantRelativeStats(want_relative_stats)
497         app.tlwsbt.SetWantRelativeStats(app.want_relative_stats)
498         app.tiwsbt.SetWantRelativeStats(app.want_relative_stats)
499         app.ewsgben.SetWantRelativeStats(app.want_relative_stats)
500         app.essgben.SetWantRelativeStats(app.want_relative_stats)
501 }
502
503 // if there's a better way of doing this do it better ...
504 func now_hhmmss() string {
505         t := time.Now()
506         return fmt.Sprintf("%2d:%02d:%02d", t.Hour(), t.Minute(), t.Second())
507 }
508
509 // record the latest screen size
510 func (app *App) ScreenSetSize(width, height int) {
511         app.screen.SetSize(width, height)
512 }
513
514 // clean up screen and disconnect database
515 func (app *App) Cleanup() {
516         app.screen.Close()
517         if app.dbh != nil {
518                 app.setup_instruments.RestoreConfiguration()
519                 _ = app.dbh.Close()
520         }
521 }
522
523 // get into a run loop
524 func (app *App) Run() {
525         app.done = make(chan struct{})
526         defer close(app.done)
527
528         app.sigChan = make(chan os.Signal, 1)
529         signal.Notify(app.sigChan, syscall.SIGINT, syscall.SIGTERM)
530
531         app.wi.SetWaitInterval(time.Second)
532
533         termboxChan := app.screen.TermBoxChan()
534
535         for !app.Finished() {
536                 select {
537                 case <-app.done:
538                         fmt.Println("app.done(): exiting")
539                         app.SetFinished()
540                 case sig := <-app.sigChan:
541                         fmt.Println("Caught a signal", sig)
542                         app.done <- struct{}{}
543                 case <-app.wi.WaitNextPeriod():
544                         app.Collect()
545                         app.Display()
546                 case event := <-termboxChan:
547                         // switch on event type
548                         switch event.Type {
549                         case termbox.EventKey: // actions depend on key
550                                 switch event.Key {
551                                 case termbox.KeyCtrlZ, termbox.KeyCtrlC, termbox.KeyEsc:
552                                         app.SetFinished()
553                                 case termbox.KeyArrowLeft: // left arrow change to previous display mode
554                                         app.DisplayPrevious()
555                                         app.Display()
556                                 case termbox.KeyTab, termbox.KeyArrowRight: // tab or right arrow - change to next display mode
557                                         app.DisplayNext()
558                                         app.Display()
559                                 }
560                                 switch event.Ch {
561                                 case '-': // decrease the interval if > 1
562                                         if app.wi.WaitInterval() > time.Second {
563                                                 app.wi.SetWaitInterval(app.wi.WaitInterval() - time.Second)
564                                         }
565                                 case '+': // increase interval by creating a new ticker
566                                         app.wi.SetWaitInterval(app.wi.WaitInterval() + time.Second)
567                                 case 'h', '?': // help
568                                         app.SetHelp(!app.Help())
569                                 case 'q': // quit
570                                         app.SetFinished()
571                                 case 't': // toggle between absolute/relative statistics
572                                         app.SetWantRelativeStats(!app.WantRelativeStats())
573                                         app.Display()
574                                 case 'z': // reset the statistics to now by taking a query of current values
575                                         app.ResetDBStatistics()
576                                         app.Display()
577                                 }
578                         case termbox.EventResize: // set sizes
579                                 app.ScreenSetSize(event.Width, event.Height)
580                                 app.Display()
581                         case termbox.EventError: // quit
582                                 log.Fatalf("Quitting because of termbox error: \n%s\n", event.Err)
583                         }
584                 }
585         }
586 }
587
588 // pstop requires MySQL 5.6+ or MariaDB 10.0+. Check the version
589 // rather than giving an error message if the requires P_S tables can't
590 // be found.
591 func (app *App) validate_mysql_version() error {
592         var tables = [...]string{
593                 "performance_schema.events_stages_summary_global_by_event_name",
594                 "performance_schema.events_waits_summary_global_by_event_name",
595                 "performance_schema.file_summary_by_instance",
596                 "performance_schema.table_io_waits_summary_by_table",
597                 "performance_schema.table_lock_waits_summary_by_table",
598         }
599
600         lib.Logger.Println("validate_mysql_version()")
601
602         lib.Logger.Println("- Getting MySQL version")
603         err, mysql_version := lib.SelectGlobalVariableByVariableName(app.dbh, "VERSION")
604         if err != nil {
605                 return err
606         }
607         lib.Logger.Println("- mysql_version: '" + mysql_version + "'")
608
609         if !re_valid_version.MatchString(mysql_version) {
610                 return errors.New(lib.MyName() + " does not work with MySQL version " + mysql_version)
611         }
612         lib.Logger.Println("OK: MySQL version is valid, continuing")
613
614         lib.Logger.Println("Checking access to required tables:")
615         for i := range tables {
616                 if err := lib.CheckTableAccess(app.dbh, tables[i]); err == nil {
617                         lib.Logger.Println("OK: " + tables[i] + " found")
618                 } else {
619                         return err
620                 }
621         }
622         lib.Logger.Println("OK: all table checks passed")
623
624         return nil
625 }