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