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