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