diff --git a/model/base.go b/model/base.go index bcab6c1..269b3b4 100644 --- a/model/base.go +++ b/model/base.go @@ -94,3 +94,38 @@ func (bc *BaseCollector[T, R]) Config() *config.Config { func (bc *BaseCollector[T, R]) DB() QueryExecutor { return bc.db } + +// Model is the interface that a data model must satisfy to be used with +// wrapper.BaseWrapper. It includes collection control, statistics queries, +// and data accessors. +type Model[T any] interface { + Collect() + ResetStatistics() + HaveRelativeStats() bool + WantRelativeStats() bool + GetFirstCollected() time.Time + GetLastCollected() time.Time + GetResults() []T + GetTotals() T +} + +// GetResults returns the results slice as a []T. +// This provides interface-accessible access to the Results field. +func (bc *BaseCollector[T, R]) GetResults() []T { + return []T(bc.Results) +} + +// GetTotals returns the totals row. +func (bc *BaseCollector[T, R]) GetTotals() T { + return bc.Totals +} + +// GetFirstCollected returns the time of the first collection. +func (bc *BaseCollector[T, R]) GetFirstCollected() time.Time { + return bc.FirstCollected +} + +// GetLastCollected returns the time of the last collection. +func (bc *BaseCollector[T, R]) GetLastCollected() time.Time { + return bc.LastCollected +} diff --git a/wrapper/base.go b/wrapper/base.go new file mode 100644 index 0000000..08b04a9 --- /dev/null +++ b/wrapper/base.go @@ -0,0 +1,105 @@ +package wrapper + +import ( + "fmt" + "time" + + "github.com/sjmudd/ps-top/model" +) + +// BaseWrapper[T, M] implements Tabler for any model M that satisfies +// model.Model[T]. It delegates to a content function provided at construction. +type BaseWrapper[T any, M model.Model[T]] struct { + model M + name string + sortFn func([]T) // optional sorting function; nil = no sort + hasData func(T) bool // predicate for counting rows with data; nil = count all rows + contentFn func(T, T) string // formats a single row +} + +// NewBaseWrapper creates a new BaseWrapper with the given model and options. +func NewBaseWrapper[T any, M model.Model[T]]( + model M, + name string, + sortFn func([]T), + hasData func(T) bool, + contentFn func(T, T) string, +) *BaseWrapper[T, M] { + return &BaseWrapper[T, M]{ + model: model, + name: name, + sortFn: sortFn, + hasData: hasData, + contentFn: contentFn, + } +} + +// Collect implements Tabler. +func (bw *BaseWrapper[T, M]) Collect() { + bw.model.Collect() + if bw.sortFn != nil { + results := bw.model.GetResults() + bw.sortFn(results) + } +} + +// ResetStatistics implements Tabler. +func (bw *BaseWrapper[T, M]) ResetStatistics() { + bw.model.ResetStatistics() +} + +// HaveRelativeStats implements Tabler. +func (bw *BaseWrapper[T, M]) HaveRelativeStats() bool { + return bw.model.HaveRelativeStats() +} + +// FirstCollectTime implements Tabler. +func (bw *BaseWrapper[T, M]) FirstCollectTime() time.Time { + return bw.model.GetFirstCollected() +} + +// LastCollectTime implements Tabler. +func (bw *BaseWrapper[T, M]) LastCollectTime() time.Time { + return bw.model.GetLastCollected() +} + +// WantRelativeStats implements Tabler. +func (bw *BaseWrapper[T, M]) WantRelativeStats() bool { + return bw.model.WantRelativeStats() +} + +// RowContent implements Tabler. +func (bw *BaseWrapper[T, M]) RowContent() []string { + results := bw.model.GetResults() + n := len(results) + return RowsFromGetter(n, func(i int) string { + return bw.contentFn(results[i], bw.model.GetTotals()) + }) +} + +// TotalRowContent implements Tabler. +func (bw *BaseWrapper[T, M]) TotalRowContent() string { + totals := bw.model.GetTotals() + return TotalRowContent(totals, bw.contentFn) +} + +// EmptyRowContent implements Tabler. +func (bw *BaseWrapper[T, M]) EmptyRowContent() string { + return EmptyRowContent(bw.contentFn) +} + +// Description implements Tabler. +func (bw *BaseWrapper[T, M]) Description() string { + results := bw.model.GetResults() + n := len(results) + count := n + if bw.hasData != nil { + count = CountIf(n, func(i int) bool { return bw.hasData(results[i]) }) + } + return fmt.Sprintf("%s %d rows", bw.name, count) +} + +// GetModel returns the embedded model. Used by special wrappers like tableioops. +func (bw *BaseWrapper[T, M]) GetModel() M { + return bw.model +} diff --git a/wrapper/common.go b/wrapper/common.go index a2c507b..d89e6ba 100644 --- a/wrapper/common.go +++ b/wrapper/common.go @@ -3,7 +3,6 @@ package wrapper import ( "fmt" - "github.com/sjmudd/ps-top/model/tableio" "github.com/sjmudd/ps-top/utils" ) @@ -61,11 +60,6 @@ func MakeTableIOHeadings(kind string) string { "Table Name") } -// MakeTableIODescription builds the description string for table IO wrappers. -func MakeTableIODescription(kind string, count int) string { - return fmt.Sprintf("Table %s (table_io_waits_summary_by_table) %d rows", kind, count) -} - // TimePct returns the formatted time and percentage strings for a row's // SumTimerWait and the total SumTimerWait. This small helper centralizes // the common prefix used by several wrapper content formatters. @@ -84,32 +78,3 @@ func PctStrings(total uint64, values ...uint64) []string { } return out } - -// RowsFromSlice builds a slice of strings by applying the provided content -// function to each element of `slice` along with the provided totals value. -// This consolidates the common RowContent pattern used by tableio wrappers. -func RowsFromSlice[T any](slice []T, totals T, content func(T, T) string) []string { - n := len(slice) - return RowsFromGetter(n, func(i int) string { return content(slice[i], totals) }) -} - -// TableIO helpers: small delegating helpers specialized for tableio.Row so -// wrappers can call a single helper instead of reimplementing the same -// RowContent/TotalRowContent/EmptyRowContent/Description trio. -func TableIORowContent(slice []tableio.Row, totals tableio.Row, content func(tableio.Row, tableio.Row) string) []string { - return RowsFromSlice(slice, totals, content) -} - -func TableIOTotalRowContent(totals tableio.Row, content func(tableio.Row, tableio.Row) string) string { - return TotalRowContent(totals, content) -} - -func TableIOEmptyRowContent(content func(tableio.Row, tableio.Row) string) string { - return EmptyRowContent(content) -} - -func TableIODescription(kind string, slice []tableio.Row, hasData func(tableio.Row) bool) string { - n := len(slice) - count := CountIf(n, func(i int) bool { return hasData(slice[i]) }) - return MakeTableIODescription(kind, count) -} diff --git a/wrapper/fileinfolatency/wrapper.go b/wrapper/fileinfolatency/wrapper.go index f3f05ed..de5e1be 100644 --- a/wrapper/fileinfolatency/wrapper.go +++ b/wrapper/fileinfolatency/wrapper.go @@ -5,7 +5,6 @@ import ( "database/sql" "fmt" "slices" - "time" "github.com/sjmudd/ps-top/config" "github.com/sjmudd/ps-top/model/fileinfo" @@ -13,38 +12,67 @@ import ( "github.com/sjmudd/ps-top/wrapper" ) -// Wrapper wraps a FileIoLatency struct representing the contents of the data collected from file_summary_by_instance, but adding formatting for presentation in the terminal -type Wrapper struct { - fiol *fileinfo.FileIoLatency -} +var ( + defaultSort = func(rows []fileinfo.Row) { + slices.SortFunc(rows, func(a, b fileinfo.Row) int { + return utils.SumTimerWaitNameOrdering( + utils.NewSumTimerWaitName(a.Name, a.SumTimerWait), + utils.NewSumTimerWaitName(b.Name, b.SumTimerWait), + ) + }) + } -// NewFileSummaryByInstance creates a wrapper around FileIoLatency -func NewFileSummaryByInstance(cfg *config.Config, db *sql.DB) *Wrapper { - return &Wrapper{ - fiol: fileinfo.NewFileSummaryByInstance(cfg, db), + defaultHasData = func(r fileinfo.Row) bool { return r.HasData() } + + defaultContent = func(row, totals fileinfo.Row) string { + var name = row.Name + + // We assume that if CountStar = 0 then there's no data at all... + // when we have no data we really don't want to show the name either. + if (row.SumTimerWait == 0 && row.CountStar == 0 && row.SumNumberOfBytesRead == 0 && row.SumNumberOfBytesWrite == 0) && name != "Totals" { + name = "" + } + + timeStr, pctStr := wrapper.TimePct(row.SumTimerWait, totals.SumTimerWait) + pct := wrapper.PctStrings(row.SumTimerWait, row.SumTimerRead, row.SumTimerWrite, row.SumTimerMisc) + opsPct := wrapper.PctStrings(row.CountStar, row.CountRead, row.CountWrite, row.CountMisc) + + return fmt.Sprintf("%10s %6s|%6s %6s %6s|%8s %8s|%8s %6s %6s %6s|%s", + timeStr, + pctStr, + pct[0], + pct[1], + pct[2], + utils.FormatAmount(row.SumNumberOfBytesRead), + utils.FormatAmount(row.SumNumberOfBytesWrite), + utils.FormatAmount(row.CountStar), + opsPct[0], + opsPct[1], + opsPct[2], + name) } -} +) -// ResetStatistics resets the statistics to last values -func (fiolw *Wrapper) ResetStatistics() { - fiolw.fiol.ResetStatistics() +// Wrapper wraps a FileIoLatency struct. +type Wrapper struct { + *wrapper.BaseWrapper[fileinfo.Row, *fileinfo.FileIoLatency] } -// Collect data from the db, then merge it in. -func (fiolw *Wrapper) Collect() { - fiolw.fiol.Collect() - - // order data by SumTimerWait (descending), Name - slices.SortFunc(fiolw.fiol.Results, func(a, b fileinfo.Row) int { - return utils.SumTimerWaitNameOrdering( - utils.NewSumTimerWaitName(a.Name, a.SumTimerWait), - utils.NewSumTimerWaitName(b.Name, b.SumTimerWait), - ) - }) +// NewFileSummaryByInstance creates a wrapper around FileIoLatency. +func NewFileSummaryByInstance(cfg *config.Config, db *sql.DB) *Wrapper { + fiol := fileinfo.NewFileSummaryByInstance(cfg, db) + bw := wrapper.NewBaseWrapper( + fiol, + "File I/O Latency (file_summary_by_instance)", + defaultSort, + defaultHasData, + defaultContent, + ) + return &Wrapper{BaseWrapper: bw} } -// Headings returns the headings for a table -func (fiolw Wrapper) Headings() string { +// Headings returns the headings for a table. +func (w *Wrapper) Headings() string { return fmt.Sprintf("%10s %6s|%6s %6s %6s|%8s %8s|%8s %6s %6s %6s|%s", "Latency", "%", @@ -59,77 +87,3 @@ func (fiolw Wrapper) Headings() string { "M Ops", "Table Name") } - -// RowContent returns the rows we need for displaying -func (fiolw Wrapper) RowContent() []string { - n := len(fiolw.fiol.Results) - return wrapper.RowsFromGetter(n, func(i int) string { - return fiolw.content(fiolw.fiol.Results[i], fiolw.fiol.Totals) - }) -} - -// TotalRowContent returns all the totals -func (fiolw Wrapper) TotalRowContent() string { - return wrapper.TotalRowContent(fiolw.fiol.Totals, fiolw.content) -} - -// EmptyRowContent returns an empty string of data (for filling in) -func (fiolw Wrapper) EmptyRowContent() string { - return wrapper.EmptyRowContent(fiolw.content) -} - -// Description returns a description of the table -func (fiolw Wrapper) Description() string { - n := len(fiolw.fiol.Results) - count := wrapper.CountIf(n, func(i int) bool { return fiolw.fiol.Results[i].HasData() }) - return fmt.Sprintf("File I/O Latency (file_summary_by_instance) %d rows", count) -} - -// HaveRelativeStats is true for this object -func (fiolw Wrapper) HaveRelativeStats() bool { - return fiolw.fiol.HaveRelativeStats() -} - -// FirstCollectTime returns the time the first value was collected -func (fiolw Wrapper) FirstCollectTime() time.Time { - return fiolw.fiol.FirstCollected -} - -// LastCollectTime returns the time the last value was collected -func (fiolw Wrapper) LastCollectTime() time.Time { - return fiolw.fiol.LastCollected -} - -// WantRelativeStats indicates if we want relative statistics -func (fiolw Wrapper) WantRelativeStats() bool { - return fiolw.fiol.WantRelativeStats() -} - -// content generate a printable result for a row, given the totals -func (fiolw Wrapper) content(row, totals fileinfo.Row) string { - var name = row.Name - - // We assume that if CountStar = 0 then there's no data at all... - // when we have no data we really don't want to show the name either. - if (row.SumTimerWait == 0 && row.CountStar == 0 && row.SumNumberOfBytesRead == 0 && row.SumNumberOfBytesWrite == 0) && name != "Totals" { - name = "" - } - - timeStr, pctStr := wrapper.TimePct(row.SumTimerWait, totals.SumTimerWait) - pct := wrapper.PctStrings(row.SumTimerWait, row.SumTimerRead, row.SumTimerWrite, row.SumTimerMisc) - opsPct := wrapper.PctStrings(row.CountStar, row.CountRead, row.CountWrite, row.CountMisc) - - return fmt.Sprintf("%10s %6s|%6s %6s %6s|%8s %8s|%8s %6s %6s %6s|%s", - timeStr, - pctStr, - pct[0], - pct[1], - pct[2], - utils.FormatAmount(row.SumNumberOfBytesRead), - utils.FormatAmount(row.SumNumberOfBytesWrite), - utils.FormatAmount(row.CountStar), - opsPct[0], - opsPct[1], - opsPct[2], - name) -} diff --git a/wrapper/memoryusage/wrapper.go b/wrapper/memoryusage/wrapper.go index 031b84c..1523183 100644 --- a/wrapper/memoryusage/wrapper.go +++ b/wrapper/memoryusage/wrapper.go @@ -1,11 +1,10 @@ -// Package memoryusage holds the routines which manage the file_summary_by_instance table. +// Package memoryusage holds the routines which manage the memory usage table. package memoryusage import ( "database/sql" "fmt" "slices" - "time" "github.com/sjmudd/ps-top/config" "github.com/sjmudd/ps-top/model/memoryusage" @@ -13,118 +12,69 @@ import ( "github.com/sjmudd/ps-top/wrapper" ) -// Wrapper wraps a FileIoLatency struct representing the contents of the data collected from file_summary_by_instance, but adding formatting for presentation in the terminal +// Wrapper wraps a MemoryUsage struct and implements the Tabler interface +// via embedded BaseWrapper. type Wrapper struct { - mu *memoryusage.MemoryUsage + *wrapper.BaseWrapper[memoryusage.Row, *memoryusage.MemoryUsage] } -// NewMemoryUsage creates a wrapper around MemoryUsage +// NewMemoryUsage creates a wrapper around MemoryUsage. func NewMemoryUsage(cfg *config.Config, db *sql.DB) *Wrapper { - return &Wrapper{ - mu: memoryusage.NewMemoryUsage(cfg, db), + mu := memoryusage.NewMemoryUsage(cfg, db) + + // Sort by CurrentBytesUsed descending, then Name ascending. + sortFn := func(rows []memoryusage.Row) { + slices.SortFunc(rows, func(a, b memoryusage.Row) int { + if a.CurrentBytesUsed > b.CurrentBytesUsed { + return -1 + } + if a.CurrentBytesUsed < b.CurrentBytesUsed { + return 1 + } + if a.Name < b.Name { + return -1 + } + if a.Name > b.Name { + return 1 + } + return 0 + }) } -} - -// ResetStatistics resets the statistics to last values -func (muw *Wrapper) ResetStatistics() { - muw.mu.ResetStatistics() -} -// Collect data from the db, then merge it in. -func (muw *Wrapper) Collect() { - muw.mu.Collect() + // Count rows with meaningful data. + hasData := func(r memoryusage.Row) bool { return r.HasData() } - // order data by CurrentBytesUsed (descending), Name - slices.SortFunc(muw.mu.Results, func(a, b memoryusage.Row) int { - if a.CurrentBytesUsed > b.CurrentBytesUsed { - return -1 - } - if a.CurrentBytesUsed < b.CurrentBytesUsed { - return 1 + // Format a single row. + contentFn := func(row, totals memoryusage.Row) string { + // assume the data is empty so hide it. + name := row.Name + if row.TotalMemoryOps == 0 && name != "Totals" { + name = "" } - if a.Name < b.Name { - return -1 - } - if a.Name > b.Name { - return 1 - } - return 0 - }) -} - -// Headings returns the headings for a table -func (muw Wrapper) Headings() string { - return "CurBytes % High Bytes|MemOps %|CurAlloc % HiAlloc|Memory Area" - // 1234567890 100.0% 1234567890|123456789 100.0%|12345678 100.0% 12345678|Some memory name -} - -// RowContent returns the rows we need for displaying -func (muw Wrapper) RowContent() []string { - n := len(muw.mu.Results) - return wrapper.RowsFromGetter(n, func(i int) string { - return muw.content(muw.mu.Results[i], muw.mu.Totals) - }) -} - -// TotalRowContent returns all the totals -func (muw Wrapper) TotalRowContent() string { - return wrapper.TotalRowContent(muw.mu.Totals, muw.content) -} -// EmptyRowContent returns an empty string of data (for filling in) -func (muw Wrapper) EmptyRowContent() string { - return wrapper.EmptyRowContent(muw.content) -} - -// Description returns a description of the table -func (muw Wrapper) Description() string { - var count int - - for row := range muw.mu.Results { - if muw.mu.Results[row].HasData() { - count++ - } + return fmt.Sprintf("%10s %6s %10s|%10s %6s|%8s %6s %8s|%s", + utils.SignedFormatAmount(row.CurrentBytesUsed), + utils.FormatPct(utils.SignedDivide(row.CurrentBytesUsed, totals.CurrentBytesUsed)), + utils.SignedFormatAmount(row.HighBytesUsed), + utils.SignedFormatAmount(row.TotalMemoryOps), + utils.FormatPct(utils.SignedDivide(row.TotalMemoryOps, totals.TotalMemoryOps)), + utils.SignedFormatAmount(row.CurrentCountUsed), + utils.FormatPct(utils.SignedDivide(row.CurrentCountUsed, totals.CurrentCountUsed)), + utils.SignedFormatAmount(row.HighCountUsed), + name) } - return fmt.Sprintf("Memory Usage (memory_summary_global_by_event_name) %d rows", count) -} - -// HaveRelativeStats is true for this object -func (muw Wrapper) HaveRelativeStats() bool { - return muw.mu.HaveRelativeStats() -} - -// FirstCollectTime returns the time the first value was collected -func (muw Wrapper) FirstCollectTime() time.Time { - return muw.mu.FirstCollected + bw := wrapper.NewBaseWrapper(mu, + "Memory Usage (memory_summary_global_by_event_name)", + sortFn, + hasData, + contentFn, + ) + return &Wrapper{BaseWrapper: bw} } -// LastCollectTime returns the time the last value was collected -func (muw Wrapper) LastCollectTime() time.Time { - return muw.mu.LastCollected -} - -// WantRelativeStats indicates if we want relative statistics -func (muw Wrapper) WantRelativeStats() bool { - return muw.mu.WantRelativeStats() -} - -// content generate a printable result for a row, given the totals -func (muw Wrapper) content(row, totals memoryusage.Row) string { - // assume the data is empty so hide it. - name := row.Name - if row.TotalMemoryOps == 0 && name != "Totals" { - name = "" - } - - return fmt.Sprintf("%10s %6s %10s|%10s %6s|%8s %6s %8s|%s", - utils.SignedFormatAmount(row.CurrentBytesUsed), - utils.FormatPct(utils.SignedDivide(row.CurrentBytesUsed, totals.CurrentBytesUsed)), - utils.SignedFormatAmount(row.HighBytesUsed), - utils.SignedFormatAmount(row.TotalMemoryOps), - utils.FormatPct(utils.SignedDivide(row.TotalMemoryOps, totals.TotalMemoryOps)), - utils.SignedFormatAmount(row.CurrentCountUsed), - utils.FormatPct(utils.SignedDivide(row.CurrentCountUsed, totals.CurrentCountUsed)), - utils.SignedFormatAmount(row.HighCountUsed), - name) +// Headings returns the headings for a table. +func (w *Wrapper) Headings() string { + return "CurBytes % High Bytes|MemOps %|CurAlloc % HiAlloc|Memory Area" + // 1234567890 100.0% 1234567890|123456789 100.0%|12345678 100.0% 12345678|Some memory name } diff --git a/wrapper/mutexlatency/wrapper.go b/wrapper/mutexlatency/wrapper.go index 47ebf04..28fd78d 100644 --- a/wrapper/mutexlatency/wrapper.go +++ b/wrapper/mutexlatency/wrapper.go @@ -1,11 +1,10 @@ -// Package mutexlatency holds the routines which manage the server mutexes +// Package mutexlatency holds the routines which manage the server mutexes. package mutexlatency import ( "database/sql" "fmt" "slices" - "time" "github.com/sjmudd/ps-top/config" "github.com/sjmudd/ps-top/model/mutexlatency" @@ -13,96 +12,50 @@ import ( "github.com/sjmudd/ps-top/wrapper" ) -// Wrapper wraps a MutexLatency struct -type Wrapper struct { - ml *mutexlatency.MutexLatency -} - -// NewMutexLatency creates a wrapper around mutexlatency.MutexLatency -func NewMutexLatency(cfg *config.Config, db *sql.DB) *Wrapper { - return &Wrapper{ - ml: mutexlatency.NewMutexLatency(cfg, db), +var ( + defaultSort = func(rows []mutexlatency.Row) { + slices.SortFunc(rows, func(a, b mutexlatency.Row) int { + return utils.SumTimerWaitNameOrdering( + utils.NewSumTimerWaitName(a.Name, a.SumTimerWait), + utils.NewSumTimerWaitName(b.Name, b.SumTimerWait), + ) + }) } -} - -// ResetStatistics resets the statistics to last values -func (mlw *Wrapper) ResetStatistics() { - mlw.ml.ResetStatistics() -} - -// Collect data from the db, then merge it in. -func (mlw *Wrapper) Collect() { - mlw.ml.Collect() - - // order data by SumTimerWait (descending), Name - slices.SortFunc(mlw.ml.Results, func(a, b mutexlatency.Row) int { - return utils.SumTimerWaitNameOrdering( - utils.NewSumTimerWaitName(a.Name, a.SumTimerWait), - utils.NewSumTimerWaitName(b.Name, b.SumTimerWait), - ) - }) -} - -// RowContent returns the rows we need for displaying -func (mlw Wrapper) RowContent() []string { - n := len(mlw.ml.Results) - return wrapper.RowsFromGetter(n, func(i int) string { - return mlw.content(mlw.ml.Results[i], mlw.ml.Totals) - }) -} - -// TotalRowContent returns all the totals -func (mlw Wrapper) TotalRowContent() string { - return wrapper.TotalRowContent(mlw.ml.Totals, mlw.content) -} - -// EmptyRowContent returns an empty string of data (for filling in) -func (mlw Wrapper) EmptyRowContent() string { - return wrapper.EmptyRowContent(mlw.content) -} -// HaveRelativeStats is true for this object -func (mlw Wrapper) HaveRelativeStats() bool { - return mlw.ml.HaveRelativeStats() -} - -// FirstCollectTime returns the time the first value was collected -func (mlw Wrapper) FirstCollectTime() time.Time { - return mlw.ml.FirstCollected -} - -// LastCollectTime returns the time the last value was collected -func (mlw Wrapper) LastCollectTime() time.Time { - return mlw.ml.LastCollected -} - -// WantRelativeStats indicates if we want relative statistics -func (mlw Wrapper) WantRelativeStats() bool { - return mlw.ml.WantRelativeStats() -} + defaultHasData = func(r mutexlatency.Row) bool { return r.SumTimerWait > 0 } + + defaultContent = func(row, totals mutexlatency.Row) string { + name := row.Name + if row.CountStar == 0 && name != "Totals" { + name = "" + } + return fmt.Sprintf("%10s %8s %8s|%s", + utils.FormatTime(row.SumTimerWait), + utils.FormatAmount(row.CountStar), + utils.FormatPct(utils.Divide(row.SumTimerWait, totals.SumTimerWait)), + name) + } +) -// Description returns a description of the table -func (mlw Wrapper) Description() string { - n := len(mlw.ml.Results) - count := wrapper.CountIf(n, func(i int) bool { return mlw.ml.Results[i].SumTimerWait > 0 }) - return fmt.Sprintf("Mutex Latency (events_waits_summary_global_by_event_name) %d rows", count) +// Wrapper wraps a MutexLatency struct. +type Wrapper struct { + *wrapper.BaseWrapper[mutexlatency.Row, *mutexlatency.MutexLatency] } -// Headings returns the headings for a table -func (mlw Wrapper) Headings() string { +// NewMutexLatency creates a wrapper around mutexlatency. +func NewMutexLatency(cfg *config.Config, db *sql.DB) *Wrapper { + ml := mutexlatency.NewMutexLatency(cfg, db) + bw := wrapper.NewBaseWrapper( + ml, + "Mutex Latency (events_waits_summary_global_by_event_name)", + defaultSort, + defaultHasData, + defaultContent, + ) + return &Wrapper{BaseWrapper: bw} +} + +// Headings returns the headings for a table. +func (w *Wrapper) Headings() string { return fmt.Sprintf("%10s %8s %8s|%s", "Latency", "MtxCnt", "%", "Mutex Name") } - -// content generate a printable result for a row, given the totals -func (mlw Wrapper) content(row, totals mutexlatency.Row) string { - name := row.Name - if row.CountStar == 0 && name != "Totals" { - name = "" - } - - return fmt.Sprintf("%10s %8s %8s|%s", - utils.FormatTime(row.SumTimerWait), - utils.FormatAmount(row.CountStar), - utils.FormatPct(utils.Divide(row.SumTimerWait, totals.SumTimerWait)), - name) -} diff --git a/wrapper/stageslatency/wrapper.go b/wrapper/stageslatency/wrapper.go index ea4b7e8..c578a47 100644 --- a/wrapper/stageslatency/wrapper.go +++ b/wrapper/stageslatency/wrapper.go @@ -5,7 +5,6 @@ import ( "database/sql" "fmt" "slices" - "time" "github.com/sjmudd/ps-top/config" "github.com/sjmudd/ps-top/model/stageslatency" @@ -13,97 +12,50 @@ import ( "github.com/sjmudd/ps-top/wrapper" ) -// Wrapper wraps a Stages struct -type Wrapper struct { - sl *stageslatency.StagesLatency -} - -// NewStagesLatency creates a wrapper around stageslatency -func NewStagesLatency(cfg *config.Config, db *sql.DB) *Wrapper { - return &Wrapper{ - sl: stageslatency.NewStagesLatency(cfg, db), +var ( + defaultSort = func(rows []stageslatency.Row) { + slices.SortFunc(rows, func(a, b stageslatency.Row) int { + return utils.SumTimerWaitNameOrdering( + utils.NewSumTimerWaitName(a.Name, a.SumTimerWait), + utils.NewSumTimerWaitName(b.Name, b.SumTimerWait), + ) + }) } -} - -// ResetStatistics resets the statistics to last values -func (slw *Wrapper) ResetStatistics() { - slw.sl.ResetStatistics() -} -// Collect data from the db, then merge it in. -func (slw *Wrapper) Collect() { - slw.sl.Collect() + defaultHasData = func(r stageslatency.Row) bool { return r.SumTimerWait > 0 } + + defaultContent = func(row, totals stageslatency.Row) string { + name := row.Name + if row.CountStar == 0 && name != "Totals" { + name = "" + } + return fmt.Sprintf("%10s %6s %8s|%s", + utils.FormatTime(row.SumTimerWait), + utils.FormatPct(utils.Divide(row.SumTimerWait, totals.SumTimerWait)), + utils.FormatAmount(row.CountStar), + name) + } +) - // order by SumeTimerWait (desc), Name - slices.SortFunc(slw.sl.Results, func(a, b stageslatency.Row) int { - return utils.SumTimerWaitNameOrdering( - utils.NewSumTimerWaitName(a.Name, a.SumTimerWait), - utils.NewSumTimerWaitName(b.Name, b.SumTimerWait), - ) - }) +// Wrapper wraps a Stages struct. +type Wrapper struct { + *wrapper.BaseWrapper[stageslatency.Row, *stageslatency.StagesLatency] } -// Headings returns the headings for a table -func (slw Wrapper) Headings() string { +// NewStagesLatency creates a wrapper around stageslatency. +func NewStagesLatency(cfg *config.Config, db *sql.DB) *Wrapper { + sl := stageslatency.NewStagesLatency(cfg, db) + bw := wrapper.NewBaseWrapper( + sl, + "SQL Stage Latency (events_stages_summary_global_by_event_name)", + defaultSort, + defaultHasData, + defaultContent, + ) + return &Wrapper{BaseWrapper: bw} +} + +// Headings returns the headings for a table. +func (w *Wrapper) Headings() string { return fmt.Sprintf("%10s %6s %8s|%s", "Latency", "%", "Counter", "Stage Name") - -} - -// RowContent returns the rows we need for displaying -func (slw Wrapper) RowContent() []string { - n := len(slw.sl.Results) - return wrapper.RowsFromGetter(n, func(i int) string { - return slw.content(slw.sl.Results[i], slw.sl.Totals) - }) -} - -// TotalRowContent returns all the totals -func (slw Wrapper) TotalRowContent() string { - return wrapper.TotalRowContent(slw.sl.Totals, slw.content) -} - -// EmptyRowContent returns an empty string of data (for filling in) -func (slw Wrapper) EmptyRowContent() string { - return wrapper.EmptyRowContent(slw.content) -} - -// Description describe the stages -func (slw Wrapper) Description() string { - n := len(slw.sl.Results) - count := wrapper.CountIf(n, func(i int) bool { return slw.sl.Results[i].SumTimerWait > 0 }) - return fmt.Sprintf("SQL Stage Latency (events_stages_summary_global_by_event_name) %d rows", count) -} - -// HaveRelativeStats is true for this object -func (slw Wrapper) HaveRelativeStats() bool { - return slw.sl.HaveRelativeStats() -} - -// FirstCollectTime returns the time the first value was collected -func (slw Wrapper) FirstCollectTime() time.Time { - return slw.sl.FirstCollected -} - -// LastCollectTime returns the time the last value was collected -func (slw Wrapper) LastCollectTime() time.Time { - return slw.sl.LastCollected -} - -// WantRelativeStats indicates if we want relative statistics -func (slw Wrapper) WantRelativeStats() bool { - return slw.sl.WantRelativeStats() -} - -// generate a printable result -func (slw Wrapper) content(row, totals stageslatency.Row) string { - name := row.Name - if row.CountStar == 0 && name != "Totals" { - name = "" - } - - return fmt.Sprintf("%10s %6s %8s|%s", - utils.FormatTime(row.SumTimerWait), - utils.FormatPct(utils.Divide(row.SumTimerWait, totals.SumTimerWait)), - utils.FormatAmount(row.CountStar), - name) } diff --git a/wrapper/tableiolatency/wrapper.go b/wrapper/tableiolatency/wrapper.go index 3cd5c1b..f0e3fc6 100644 --- a/wrapper/tableiolatency/wrapper.go +++ b/wrapper/tableiolatency/wrapper.go @@ -1,11 +1,10 @@ -// Package tableiolatency holds the routines which manage the tableio statisticss. +// Package tableiolatency holds the routines which manage table IO latency statistics. package tableiolatency import ( "database/sql" "fmt" "slices" - "time" "github.com/sjmudd/ps-top/config" "github.com/sjmudd/ps-top/model/tableio" @@ -13,102 +12,57 @@ import ( "github.com/sjmudd/ps-top/wrapper" ) -// Wrapper represents the contents of the data collected related to tableio statistics -type Wrapper struct { - tiol *tableio.TableIo -} - -// NewTableIoLatency creates a wrapper around tableio statistics -func NewTableIoLatency(cfg *config.Config, db *sql.DB) *Wrapper { - return &Wrapper{ - tiol: tableio.NewTableIo(cfg, db), +// Default functions for BaseWrapper, shared with tests. +var ( + defaultSort = func(rows []tableio.Row) { + slices.SortFunc(rows, func(a, b tableio.Row) int { + return utils.SumTimerWaitNameOrdering( + utils.NewSumTimerWaitName(a.Name, a.SumTimerWait), + utils.NewSumTimerWaitName(b.Name, b.SumTimerWait), + ) + }) } -} -// Tiol returns the a TableIo value -func (tiolw *Wrapper) Tiol() *tableio.TableIo { - return tiolw.tiol -} + defaultHasData = func(r tableio.Row) bool { return r.HasData() } + + defaultContent = func(row, totals tableio.Row) string { + // assume the data is empty so hide it. + name := row.Name + if row.CountStar == 0 && name != "Totals" { + name = "" + } + return fmt.Sprintf("%10s %6s|%6s %6s|%6s %6s %6s %6s|%s", + utils.FormatTime(row.SumTimerWait), + utils.FormatPct(utils.Divide(row.SumTimerWait, totals.SumTimerWait)), + utils.FormatPct(utils.Divide(row.SumTimerRead, row.SumTimerWait)), + utils.FormatPct(utils.Divide(row.SumTimerWrite, row.SumTimerWait)), + utils.FormatPct(utils.Divide(row.SumTimerFetch, row.SumTimerWait)), + utils.FormatPct(utils.Divide(row.SumTimerInsert, row.SumTimerWait)), + utils.FormatPct(utils.Divide(row.SumTimerUpdate, row.SumTimerWait)), + utils.FormatPct(utils.Divide(row.SumTimerDelete, row.SumTimerWait)), + name) + } +) -// ResetStatistics resets the statistics to last values -func (tiolw *Wrapper) ResetStatistics() { - tiolw.tiol.ResetStatistics() +// Wrapper wraps a TableIo struct and implements Tabler via BaseWrapper. +type Wrapper struct { + *wrapper.BaseWrapper[tableio.Row, *tableio.TableIo] } -// Collect data from the db, then merge it in. -func (tiolw *Wrapper) Collect() { - tiolw.tiol.Collect() - - // order by latency (SumTimerWait) descending, Name - slices.SortFunc(tiolw.tiol.Results, func(a, b tableio.Row) int { - return utils.SumTimerWaitNameOrdering( - utils.NewSumTimerWaitName(a.Name, a.SumTimerWait), - utils.NewSumTimerWaitName(b.Name, b.SumTimerWait), - ) - }) +// NewTableIoLatency creates a wrapper around tableio statistics. +func NewTableIoLatency(cfg *config.Config, db *sql.DB) *Wrapper { + tiol := tableio.NewTableIo(cfg, db) + bw := wrapper.NewBaseWrapper( + tiol, + "Table I/O Latency (table_io_waits_summary_by_table)", + defaultSort, + defaultHasData, + defaultContent, + ) + return &Wrapper{BaseWrapper: bw} } -// Headings returns the latency headings as a string -func (tiolw Wrapper) Headings() string { +// Headings returns the latency headings as a string. +func (w *Wrapper) Headings() string { return wrapper.MakeTableIOHeadings("Latency") } - -// RowContent returns the rows we need for displaying -func (tiolw Wrapper) RowContent() []string { - return wrapper.TableIORowContent(tiolw.tiol.Results, tiolw.tiol.Totals, tiolw.content) -} - -// TotalRowContent returns all the totals -func (tiolw Wrapper) TotalRowContent() string { - return wrapper.TableIOTotalRowContent(tiolw.tiol.Totals, tiolw.content) -} - -// EmptyRowContent returns an empty string of data (for filling in) -func (tiolw Wrapper) EmptyRowContent() string { - return wrapper.TableIOEmptyRowContent(tiolw.content) -} - -// Description returns a description of the table -func (tiolw Wrapper) Description() string { - return wrapper.TableIODescription("Latency", tiolw.tiol.Results, func(r tableio.Row) bool { return r.HasData() }) -} - -// HaveRelativeStats is true for this object -func (tiolw Wrapper) HaveRelativeStats() bool { - return tiolw.tiol.HaveRelativeStats() -} - -// FirstCollectTime returns the time of the first collection -func (tiolw Wrapper) FirstCollectTime() time.Time { - return tiolw.tiol.FirstCollected -} - -// LastCollectTime returns the time of the last collection -func (tiolw Wrapper) LastCollectTime() time.Time { - return tiolw.tiol.LastCollected -} - -// WantRelativeStats returns if we want to see relative stats -func (tiolw Wrapper) WantRelativeStats() bool { - return tiolw.tiol.WantRelativeStats() -} - -// latencyRowContents returns the printable result -func (tiolw Wrapper) content(row, totals tableio.Row) string { - // assume the data is empty so hide it. - name := row.Name - if row.CountStar == 0 && name != "Totals" { - name = "" - } - - return fmt.Sprintf("%10s %6s|%6s %6s|%6s %6s %6s %6s|%s", - utils.FormatTime(row.SumTimerWait), - utils.FormatPct(utils.Divide(row.SumTimerWait, totals.SumTimerWait)), - utils.FormatPct(utils.Divide(row.SumTimerRead, row.SumTimerWait)), - utils.FormatPct(utils.Divide(row.SumTimerWrite, row.SumTimerWait)), - utils.FormatPct(utils.Divide(row.SumTimerFetch, row.SumTimerWait)), - utils.FormatPct(utils.Divide(row.SumTimerInsert, row.SumTimerWait)), - utils.FormatPct(utils.Divide(row.SumTimerUpdate, row.SumTimerWait)), - utils.FormatPct(utils.Divide(row.SumTimerDelete, row.SumTimerWait)), - name) -} diff --git a/wrapper/tableiolatency/wrapper_test.go b/wrapper/tableiolatency/wrapper_test.go index 1bb23f1..dc0174b 100644 --- a/wrapper/tableiolatency/wrapper_test.go +++ b/wrapper/tableiolatency/wrapper_test.go @@ -6,55 +6,63 @@ import ( "github.com/sjmudd/ps-top/model" "github.com/sjmudd/ps-top/model/tableio" + "github.com/sjmudd/ps-top/wrapper" ) -func newTableIo(rows []tableio.Row, totals tableio.Row) *tableio.TableIo { +// newTableIo creates a Wrapper for testing with the given rows and totals. +// This helper constructs a TableIo model with injected data and wraps it +// using BaseWrapper with the default functions defined in this package. +func newTableIo(rows []tableio.Row, totals tableio.Row) *Wrapper { + // Create a TableIo with a BaseCollector, manually set Results/Totals. process := func(last, _ tableio.Rows) (tableio.Rows, tableio.Row) { - // Dummy process; not used in tests. + // Not used because we set Results manually. return last, tableio.Row{} } bc := model.NewBaseCollector[tableio.Row, tableio.Rows](nil, nil, process) tiol := &tableio.TableIo{BaseCollector: bc} bc.Results = rows bc.Totals = totals - return tiol + + // Wrap using BaseWrapper with the default functions from this package. + bw := wrapper.NewBaseWrapper( + tiol, + "Table I/O Latency (table_io_waits_summary_by_table)", + defaultSort, + defaultHasData, + defaultContent, + ) + return &Wrapper{BaseWrapper: bw} } -// TestRowContentUsesSumTimerWait verifies that RowContent produces output based on SumTimerWait. +// TestRowContentUsesSumTimerWait verifies that RowContent includes the expected +// SumTimerWait values and percentages for each row. func TestRowContentUsesSumTimerWait(t *testing.T) { - // Create multiple rows with SumTimerWait values rows := []tableio.Row{ - {Name: "db1.t1", CountStar: 1, SumTimerWait: 1000000}, // 1ms, should be 25% - {Name: "db2.t2", CountStar: 1, SumTimerWait: 3000000}, // 3ms, should be 75% + {Name: "db1.t1", CountStar: 1, SumTimerWait: 1000000}, + {Name: "db2.t2", CountStar: 1, SumTimerWait: 3000000}, } - // Sum = 4ms totals := tableio.Row{SumTimerWait: 4000000} - tiol := newTableIo(rows, totals) - w := &Wrapper{tiol: tiol} + w := newTableIo(rows, totals) lines := w.RowContent() if len(lines) != 2 { t.Fatalf("RowContent returned %d rows, want 2", len(lines)) } - // Combine all lines to search for expected values regardless of order. all := strings.Join(lines, " ") - // Should contain 1ms time and 25% for the smaller row. if !strings.Contains(all, "1.00") || !strings.Contains(all, "25.0%") { t.Errorf("output missing 1ms/25%%: %q", all) } - // Should contain 3ms time and 75% for the larger row. if !strings.Contains(all, "3.00") || !strings.Contains(all, "75.0%") { t.Errorf("output missing 3ms/75%%: %q", all) } - // Both table names present. if !strings.Contains(all, "db1.t1") || !strings.Contains(all, "db2.t2") { t.Errorf("missing table names: %q", all) } } -// TestHeadings checks that headings contain "Latency". +// TestHeadings checks that the Headings output contains "Latency". func TestHeadings(t *testing.T) { w := &Wrapper{} h := w.Headings() @@ -63,53 +71,40 @@ func TestHeadings(t *testing.T) { } } -// TestDescription checks that description contains "Latency". +// TestDescription verifies that Description includes the expected latency label. func TestDescription(t *testing.T) { rows := []tableio.Row{{Name: "db.t", SumTimerWait: 1000}} - tiol := newTableIo(rows, tableio.Row{}) - w := &Wrapper{tiol: tiol} + w := newTableIo(rows, tableio.Row{}) d := w.Description() if !strings.Contains(d, "Latency") { t.Errorf("Description missing 'Latency': %q", d) } } -// TestRowContentOperationPercentages verifies that Fetch/Insert/Update/Delete percentages -// are calculated from SumTimer* fields divided by row.SumTimerWait. -// It uses realistic values satisfying MySQL constraints: -// - SumTimerWait = SumTimerRead + SumTimerWrite -// - SumTimerRead >= SumTimerFetch -// - SumTimerWrite >= SumTimerInsert + SumTimerUpdate + SumTimerDelete +// TestRowContentOperationPercentages checks that Fetch/Insert/Update/Delete +// percentages are calculated correctly from SumTimer* fields. func TestRowContentOperationPercentages(t *testing.T) { - // Realistic distribution: - // Fetch=250 (part of read), other reads=50 -> SumTimerRead=300 (>= fetch) - // Insert=100, Update=50, Delete=50 -> sum=200, plus write overhead=50 -> SumTimerWrite=250 - // SumTimerWait = 300+250 = 550 row := tableio.Row{ Name: "db.t", CountStar: 1, SumTimerWait: 550, - SumTimerFetch: 250, // 250/550 ≈ 45.5% - SumTimerInsert: 100, // 18.2% - SumTimerUpdate: 50, // 9.1% - SumTimerDelete: 50, // 9.1% - SumTimerRead: 300, // read total ≥ fetch - SumTimerWrite: 250, // write total ≥ insert+update+delete (200) + SumTimerFetch: 250, + SumTimerInsert: 100, + SumTimerUpdate: 50, + SumTimerDelete: 50, + SumTimerRead: 300, + SumTimerWrite: 250, } totals := tableio.Row{SumTimerWait: 550} - tiol := newTableIo([]tableio.Row{row}, totals) - w := &Wrapper{tiol: tiol} + w := newTableIo([]tableio.Row{row}, totals) line := w.RowContent()[0] parts := strings.Split(line, "|") if len(parts) != 4 { t.Fatalf("expected 4 parts, got %d: %q", len(parts), line) } - // parts[1] contains read%, write%; parts[2] contains fetch%, insert%, update%, delete% mid := parts[2] - // Expected percentages (rounded to 1 decimal): - // fetch=45.5%, insert=18.2%, update=9.1%, delete=9.1% expPcts := []string{"45.5%", "18.2%", "9.1%", "9.1%"} for _, exp := range expPcts { if !strings.Contains(mid, exp) { diff --git a/wrapper/tableioops/wrapper.go b/wrapper/tableioops/wrapper.go index 1a72846..e8bd208 100644 --- a/wrapper/tableioops/wrapper.go +++ b/wrapper/tableioops/wrapper.go @@ -1,10 +1,9 @@ -// Package tableioops holds the routines which manage the table ops +// Package tableioops holds the routines which manage table I/O operations statistics. package tableioops import ( "fmt" "slices" - "time" "github.com/sjmudd/ps-top/model/tableio" "github.com/sjmudd/ps-top/utils" @@ -12,110 +11,86 @@ import ( "github.com/sjmudd/ps-top/wrapper/tableiolatency" ) -// Wrapper represents a wrapper around tableiolatency -// - the latency wrapper is only to be used for common functionality between the 2 structs -type Wrapper struct { - tiol *tableio.TableIo - latency *tableiolatency.Wrapper -} - -// NewTableIoOps creates a wrapper around TableIo, sharing the same connection with the tableiolatency wrapper -func NewTableIoOps(latency *tableiolatency.Wrapper) *Wrapper { - return &Wrapper{ - tiol: latency.Tiol(), - latency: latency, +var ( + // sort by CountStar (ops) descending, Name ascending. + defaultSort = func(rows []tableio.Row) { + slices.SortFunc(rows, func(a, b tableio.Row) int { + if a.CountStar > b.CountStar { + return -1 + } + if a.CountStar < b.CountStar { + return 1 + } + if a.Name < b.Name { + return -1 + } + if a.Name > b.Name { + return 1 + } + return 0 + }) } -} - -// ResetStatistics resets the statistics to last values -func (tiolw *Wrapper) ResetStatistics() { - tiolw.tiol.ResetStatistics() -} -// Collect data from the db, then merge it in. -func (tiolw *Wrapper) Collect() { - tiolw.tiol.Collect() + defaultHasData = func(r tableio.Row) bool { return r.HasData() } - // sort the results by ops == CountStar (descending), Name - slices.SortFunc(tiolw.tiol.Results, func(a, b tableio.Row) int { - if a.CountStar > b.CountStar { - return -1 - } - if a.CountStar < b.CountStar { - return 1 - } - if a.Name < b.Name { - return -1 - } - if a.Name > b.Name { - return 1 + defaultContent = func(row, totals tableio.Row) string { + name := row.Name + if row.CountStar == 0 && name != "Totals" { + name = "" } - return 0 - }) -} - -// Headings returns the headings by operations as a string -func (tiolw Wrapper) Headings() string { - return wrapper.MakeTableIOHeadings("Ops") -} - -// content returns the printable content of a row given the totals details -func (tiolw Wrapper) content(row, totals tableio.Row) string { - // assume the data is empty so hide it. - name := row.Name - if row.CountStar == 0 && name != "Totals" { - name = "" + // Read/Write percentages placed before fetch/insert/update/delete with extra separator + return fmt.Sprintf("%10s %6s|%6s %6s|%6s %6s %6s %6s|%s", + utils.FormatCounterU(row.CountStar, 10), + utils.FormatPct(utils.Divide(row.CountStar, totals.CountStar)), + utils.FormatPct(utils.Divide(row.CountRead, row.CountStar)), + utils.FormatPct(utils.Divide(row.CountWrite, row.CountStar)), + utils.FormatPct(utils.Divide(row.CountFetch, row.CountStar)), + utils.FormatPct(utils.Divide(row.CountInsert, row.CountStar)), + utils.FormatPct(utils.Divide(row.CountUpdate, row.CountStar)), + utils.FormatPct(utils.Divide(row.CountDelete, row.CountStar)), + name) } +) - // Read/Write percentages placed before fetch/insert/update/delete with extra separator - return fmt.Sprintf("%10s %6s|%6s %6s|%6s %6s %6s %6s|%s", - utils.FormatCounterU(row.CountStar, 10), - utils.FormatPct(utils.Divide(row.CountStar, totals.CountStar)), - utils.FormatPct(utils.Divide(row.CountRead, row.CountStar)), - utils.FormatPct(utils.Divide(row.CountWrite, row.CountStar)), - utils.FormatPct(utils.Divide(row.CountFetch, row.CountStar)), - utils.FormatPct(utils.Divide(row.CountInsert, row.CountStar)), - utils.FormatPct(utils.Divide(row.CountUpdate, row.CountStar)), - utils.FormatPct(utils.Divide(row.CountDelete, row.CountStar)), - name) -} - -// RowContent returns the rows we need for displaying -func (tiolw Wrapper) RowContent() []string { - return wrapper.TableIORowContent(tiolw.tiol.Results, tiolw.tiol.Totals, tiolw.content) -} - -// TotalRowContent returns all the totals -func (tiolw Wrapper) TotalRowContent() string { - return tiolw.latency.TotalRowContent() -} - -// EmptyRowContent returns an empty string of data (for filling in) -func (tiolw Wrapper) EmptyRowContent() string { - return tiolw.latency.EmptyRowContent() +// Wrapper represents a wrapper around table I/O ops. It shares the same underlying +// TableIo model as the latency wrapper but presents different formatting and sorting. +type Wrapper struct { + *wrapper.BaseWrapper[tableio.Row, *tableio.TableIo] + latency *tableiolatency.Wrapper } -// Description returns a description of the table -func (tiolw Wrapper) Description() string { - return wrapper.TableIODescription("Ops", tiolw.tiol.Results, func(r tableio.Row) bool { return r.HasData() }) -} +// NewTableIoOps creates a new ops wrapper that shares a TableIo model with the +// latency wrapper. It uses the latency wrapper for TotalRowContent and EmptyRowContent. +func NewTableIoOps(latency *tableiolatency.Wrapper) *Wrapper { + // Get the shared TableIo model from the latency wrapper. + tiol := latency.GetModel() + + // Build our own BaseWrapper using ops-specific parameters. + bw := wrapper.NewBaseWrapper( + tiol, + "Table I/O Ops (table_io_waits_summary_by_table)", + defaultSort, + defaultHasData, + defaultContent, + ) -// HaveRelativeStats is true for this object -func (tiolw Wrapper) HaveRelativeStats() bool { - return true + return &Wrapper{ + BaseWrapper: bw, + latency: latency, + } } -// FirstCollectTime returns the time of the first collection of information -func (tiolw Wrapper) FirstCollectTime() time.Time { - return tiolw.tiol.FirstCollected +// TotalRowContent returns the total row content by delegating to the latency wrapper. +func (w *Wrapper) TotalRowContent() string { + return w.latency.TotalRowContent() } -// LastCollectTime returns the last time data was collected -func (tiolw Wrapper) LastCollectTime() time.Time { - return tiolw.tiol.LastCollected +// EmptyRowContent returns the empty row content by delegating to the latency wrapper. +func (w *Wrapper) EmptyRowContent() string { + return w.latency.EmptyRowContent() } -// WantRelativeStats returns whether we want to see relative or absolute stats -func (tiolw Wrapper) WantRelativeStats() bool { - return tiolw.tiol.WantRelativeStats() +// Headings returns the ops headings. +func (w *Wrapper) Headings() string { + return wrapper.MakeTableIOHeadings("Ops") } diff --git a/wrapper/tableioops/wrapper_test.go b/wrapper/tableioops/wrapper_test.go index f464ed6..0a5c817 100644 --- a/wrapper/tableioops/wrapper_test.go +++ b/wrapper/tableioops/wrapper_test.go @@ -4,38 +4,39 @@ import ( "strings" "testing" - "github.com/sjmudd/ps-top/model" "github.com/sjmudd/ps-top/model/tableio" + "github.com/sjmudd/ps-top/wrapper/tableiolatency" ) -func newTableIo(rows []tableio.Row, totals tableio.Row) *tableio.TableIo { - process := func(last, _ tableio.Rows) (tableio.Rows, tableio.Row) { - // Dummy process; not used in tests. - return last, tableio.Row{} - } - bc := model.NewBaseCollector[tableio.Row, tableio.Rows](nil, nil, process) - tiol := &tableio.TableIo{BaseCollector: bc} - bc.Results = rows - bc.Totals = totals - return tiol +// newTableIo creates a Wrapper for testing with the given rows and totals. +// It constructs a latency wrapper and derives the ops wrapper from it, +// injecting the test data into the shared underlying TableIo model. +func newTableIo(rows []tableio.Row, totals tableio.Row) *Wrapper { + // Create a latency wrapper with nil config/db (sufficient for tests). + latency := tableiolatency.NewTableIoLatency(nil, nil) + // Inject test data into the underlying TableIo model. + model := latency.GetModel() + model.Results = rows + model.Totals = totals + // Create ops wrapper using the latency wrapper. + return NewTableIoOps(latency) } -// TestRowContentUsesCounts verifies that RowContent uses CountStar and Count* fields. +// TestRowContentUsesCounts verifies that RowContent uses CountStar and Count* fields +// to display operations counts and percentages. func TestRowContentUsesCounts(t *testing.T) { rows := []tableio.Row{ {Name: "db1.t1", CountStar: 100, CountFetch: 30}, {Name: "db2.t2", CountStar: 100, CountFetch: 50}, } totals := tableio.Row{CountStar: 200} - tiol := newTableIo(rows, totals) - w := &Wrapper{tiol: tiol} + w := newTableIo(rows, totals) lines := w.RowContent() if len(lines) != 2 { t.Fatalf("RowContent returned %d rows, want 2", len(lines)) } - // Inspect each line's columns. for _, line := range lines { parts := strings.Split(line, "|") if len(parts) != 4 { @@ -43,70 +44,59 @@ func TestRowContentUsesCounts(t *testing.T) { } left := parts[0] - // Each row's CountStar is 100. if !strings.Contains(left, "100") { t.Errorf("missing count 100 in left: %q", left) } - // Total percentage = 50.0% if !strings.Contains(left, "50.0%") { t.Errorf("missing total %% 50.0%% in left: %q", left) } } } -// TestHeadings checks that headings contain "Ops". +// TestHeadings checks that the headings contain the "Ops" label. func TestHeadings(t *testing.T) { - h := (&Wrapper{}).Headings() + // Construct a wrapper using a latency wrapper (with nil args). + latency := tableiolatency.NewTableIoLatency(nil, nil) + w := NewTableIoOps(latency) + h := w.Headings() if !strings.Contains(h, "Ops") { t.Errorf("Headings missing 'Ops': %q", h) } } -// TestDescription checks that description contains "Ops". +// TestDescription verifies that Description includes the expected "Ops" label. func TestDescription(t *testing.T) { rows := []tableio.Row{{Name: "db.t", CountStar: 100}} - tiol := newTableIo(rows, tableio.Row{}) - w := &Wrapper{tiol: tiol} + w := newTableIo(rows, tableio.Row{}) d := w.Description() if !strings.Contains(d, "Ops") { t.Errorf("Description missing 'Ops': %q", d) } } -// TestRowContentOperationPercentages verifies that Fetch/Insert/Update/Delete percentages -// are calculated from Count* fields divided by row.CountStar. -// It uses realistic values satisfying MySQL constraints: -// - CountStar = CountRead + CountWrite -// - CountRead >= CountFetch -// - CountWrite >= CountInsert + CountUpdate + CountDelete +// TestRowContentOperationPercentages validates that Fetch/Insert/Update/Delete +// percentages are computed correctly from Count* fields divided by row CountStar. func TestRowContentOperationPercentages(t *testing.T) { - // Realistic distribution: - // CountFetch=100 (part of read), other reads=50 -> CountRead=150 (>= fetch) - // CountInsert=50, CountUpdate=30, CountDelete=20 -> sum=100, plus write overhead=20 -> CountWrite=120 - // CountStar = 150 + 120 = 270 row := tableio.Row{ Name: "db.t", CountStar: 270, - CountFetch: 100, // 100/270 ≈ 37.0% - CountInsert: 50, // 18.5% - CountUpdate: 30, // 11.1% - CountDelete: 20, // 7.4% - CountRead: 150, // ≥ CountFetch - CountWrite: 120, // ≥ insert+update+delete (100) + CountFetch: 100, + CountInsert: 50, + CountUpdate: 30, + CountDelete: 20, + CountRead: 150, + CountWrite: 120, } totals := tableio.Row{CountStar: 270} - tiol := newTableIo([]tableio.Row{row}, totals) - w := &Wrapper{tiol: tiol} + w := newTableIo([]tableio.Row{row}, totals) line := w.RowContent()[0] parts := strings.Split(line, "|") if len(parts) != 4 { t.Fatalf("expected 4 parts, got %d: %q", len(parts), line) } - // parts[1] contains read%, write%; parts[2] contains fetch%, insert%, update%, delete% mid := parts[2] - // Expected percentages (rounded): expPcts := []string{"37.0%", "18.5%", "11.1%", "7.4%"} for _, exp := range expPcts { if !strings.Contains(mid, exp) { diff --git a/wrapper/tablelocklatency/wrapper.go b/wrapper/tablelocklatency/wrapper.go index 11f6eec..1291da4 100644 --- a/wrapper/tablelocklatency/wrapper.go +++ b/wrapper/tablelocklatency/wrapper.go @@ -5,7 +5,6 @@ import ( "database/sql" "fmt" "slices" - "time" "github.com/sjmudd/ps-top/config" "github.com/sjmudd/ps-top/model/tablelocks" @@ -13,38 +12,81 @@ import ( "github.com/sjmudd/ps-top/wrapper" ) -// Wrapper wraps a TableLockLatency struct -type Wrapper struct { - tl *tablelocks.TableLocks -} +var ( + defaultSort = func(rows []tablelocks.Row) { + slices.SortFunc(rows, func(a, b tablelocks.Row) int { + return utils.SumTimerWaitNameOrdering( + utils.NewSumTimerWaitName(a.Name, a.SumTimerWait), + utils.NewSumTimerWaitName(b.Name, b.SumTimerWait), + ) + }) + } -// NewTableLockLatency creates a wrapper around TableLockLatency -func NewTableLockLatency(cfg *config.Config, db *sql.DB) *Wrapper { - return &Wrapper{ - tl: tablelocks.NewTableLocks(cfg, db), + // No hasData filter; count all rows. + // We'll not define a variable and pass nil directly. + + defaultContent = func(row, totals tablelocks.Row) string { + name := row.Name + if row.SumTimerWait == 0 && name != "Totals" { + name = "" + } + timeStr, pctStr := wrapper.TimePct(row.SumTimerWait, totals.SumTimerWait) + pct := wrapper.PctStrings(row.SumTimerWait, + row.SumTimerRead, + row.SumTimerWrite, + row.SumTimerReadWithSharedLocks, + row.SumTimerReadHighPriority, + row.SumTimerReadNoInsert, + row.SumTimerReadNormal, + row.SumTimerReadExternal, + row.SumTimerWriteAllowWrite, + row.SumTimerWriteConcurrentInsert, + row.SumTimerWriteLowPriority, + row.SumTimerWriteNormal, + row.SumTimerWriteExternal) + + return fmt.Sprintf("%10s %6s|%6s %6s|%6s %6s %6s %6s %6s|%6s %6s %6s %6s %6s|%s", + timeStr, + pctStr, + + pct[0], + pct[1], + + pct[2], + pct[3], + pct[4], + pct[5], + pct[6], + + pct[7], + pct[8], + pct[9], + pct[10], + pct[11], + name) } -} +) -// ResetStatistics resets the statistics to last values -func (tlw *Wrapper) ResetStatistics() { - tlw.tl.ResetStatistics() +// Wrapper wraps a TableLock struct. +type Wrapper struct { + *wrapper.BaseWrapper[tablelocks.Row, *tablelocks.TableLocks] } -// Collect data from the db, then merge it in. -func (tlw *Wrapper) Collect() { - tlw.tl.Collect() - - // order data by SumTimerWait (descending), Name - slices.SortFunc(tlw.tl.Results, func(a, b tablelocks.Row) int { - return utils.SumTimerWaitNameOrdering( - utils.NewSumTimerWaitName(a.Name, a.SumTimerWait), - utils.NewSumTimerWaitName(b.Name, b.SumTimerWait), - ) - }) +// NewTableLockLatency creates a wrapper around TableLockLatency. +func NewTableLockLatency(cfg *config.Config, db *sql.DB) *Wrapper { + tl := tablelocks.NewTableLocks(cfg, db) + bw := wrapper.NewBaseWrapper( + tl, + "Locks by Table Name (table_lock_waits_summary_by_table)", + defaultSort, + nil, // hasData: count all rows + defaultContent, + ) + return &Wrapper{BaseWrapper: bw} } -// Headings returns the headings for a table -func (tlw Wrapper) Headings() string { +// Headings returns the headings for a table. +func (w *Wrapper) Headings() string { return fmt.Sprintf("%10s %6s|%6s %6s|%6s %6s %6s %6s %6s|%6s %6s %6s %6s %6s|%-30s", "Latency", "%", "Read", "Write", @@ -52,91 +94,3 @@ func (tlw Wrapper) Headings() string { "AlloWr", "CncIns", "Low", "Normal", "Extrnl", "Table Name") } - -// RowContent returns the rows we need for displaying -func (tlw Wrapper) RowContent() []string { - n := len(tlw.tl.Results) - return wrapper.RowsFromGetter(n, func(i int) string { - return tlw.content(tlw.tl.Results[i], tlw.tl.Totals) - }) -} - -// TotalRowContent returns all the totals -func (tlw Wrapper) TotalRowContent() string { - return wrapper.TotalRowContent(tlw.tl.Totals, tlw.content) -} - -// EmptyRowContent returns an empty string of data (for filling in) -func (tlw Wrapper) EmptyRowContent() string { - return wrapper.EmptyRowContent(tlw.content) -} - -// Description returns a description of the table -func (tlw Wrapper) Description() string { - count := len(tlw.tl.Results) - - return fmt.Sprintf("Locks by Table Name (table_lock_waits_summary_by_table) %d rows", count) -} - -// HaveRelativeStats is true for this object -func (tlw Wrapper) HaveRelativeStats() bool { - return tlw.tl.HaveRelativeStats() -} - -// FirstCollectTime returns the time the first value was collected -func (tlw Wrapper) FirstCollectTime() time.Time { - return tlw.tl.FirstCollected -} - -// LastCollectTime returns the time the last value was collected -func (tlw Wrapper) LastCollectTime() time.Time { - return tlw.tl.LastCollected -} - -// WantRelativeStats indicates if we want relative statistics -func (tlw Wrapper) WantRelativeStats() bool { - return tlw.tl.WantRelativeStats() -} - -// content generate a printable result for a row, given the totals -func (tlw Wrapper) content(row, totals tablelocks.Row) string { - // assume the data is empty so hide it. - name := row.Name - if row.SumTimerWait == 0 && name != "Totals" { - name = "" - } - timeStr, pctStr := wrapper.TimePct(row.SumTimerWait, totals.SumTimerWait) - pct := wrapper.PctStrings(row.SumTimerWait, - row.SumTimerRead, - row.SumTimerWrite, - row.SumTimerReadWithSharedLocks, - row.SumTimerReadHighPriority, - row.SumTimerReadNoInsert, - row.SumTimerReadNormal, - row.SumTimerReadExternal, - row.SumTimerWriteAllowWrite, - row.SumTimerWriteConcurrentInsert, - row.SumTimerWriteLowPriority, - row.SumTimerWriteNormal, - row.SumTimerWriteExternal) - - return fmt.Sprintf("%10s %6s|%6s %6s|%6s %6s %6s %6s %6s|%6s %6s %6s %6s %6s|%s", - timeStr, - pctStr, - - pct[0], - pct[1], - - pct[2], - pct[3], - pct[4], - pct[5], - pct[6], - - pct[7], - pct[8], - pct[9], - pct[10], - pct[11], - name) -} diff --git a/wrapper/userlatency/wrapper.go b/wrapper/userlatency/wrapper.go index 43ed22a..cfbd2e6 100644 --- a/wrapper/userlatency/wrapper.go +++ b/wrapper/userlatency/wrapper.go @@ -1,11 +1,10 @@ -// Package userlatency holds the routines which manage the user latency information +// Package userlatency holds the routines which manage the user latency information. package userlatency import ( "database/sql" "fmt" "slices" - "time" "github.com/sjmudd/ps-top/config" "github.com/sjmudd/ps-top/model/userlatency" @@ -13,127 +12,8 @@ import ( "github.com/sjmudd/ps-top/wrapper" ) -// Wrapper wraps a UserLatency struct -type Wrapper struct { - ul *userlatency.UserLatency -} - -// NewUserLatency creates a wrapper around UserLatency -func NewUserLatency(cfg *config.Config, db *sql.DB) *Wrapper { - return &Wrapper{ - ul: userlatency.NewUserLatency(cfg, db), - } -} - -// ResetStatistics resets the statistics to last values -func (ulw *Wrapper) ResetStatistics() { - ulw.ul.ResetStatistics() -} - -// Collect data from the db, then sort the results. -func (ulw *Wrapper) Collect() { - ulw.ul.Collect() - - // order by TotalTime (descending), Connections (descending), Name - slices.SortFunc(ulw.ul.Results, func(a, b userlatency.Row) int { - if a.TotalTime() > b.TotalTime() { - return -1 - } - if a.TotalTime() < b.TotalTime() { - return 1 - } - if a.Connections > b.Connections { - return -1 - } - if a.Connections < b.Connections { - return 1 - } - if a.Username < b.Username { - return -1 - } - if a.Username > b.Username { - return 1 - } - return 0 - }) -} - -// RowContent returns the rows we need for displaying -func (ulw Wrapper) RowContent() []string { - n := len(ulw.ul.Results) - return wrapper.RowsFromGetter(n, func(i int) string { - return ulw.content(ulw.ul.Results[i], ulw.ul.Totals) - }) -} - -// TotalRowContent returns all the totals -func (ulw Wrapper) TotalRowContent() string { - return wrapper.TotalRowContent(ulw.ul.Totals, ulw.content) -} - -// EmptyRowContent returns an empty string of data (for filling in) -func (ulw Wrapper) EmptyRowContent() string { - return wrapper.EmptyRowContent(ulw.content) -} - -// HaveRelativeStats is true for this object -func (ulw Wrapper) HaveRelativeStats() bool { - return ulw.ul.HaveRelativeStats() -} - -// FirstCollectTime returns the time the first value was collected -func (ulw Wrapper) FirstCollectTime() time.Time { - return ulw.ul.FirstCollected -} - -// LastCollectTime returns the time the last value was collected -func (ulw Wrapper) LastCollectTime() time.Time { - return ulw.ul.LastCollected -} - -// WantRelativeStats indicates if we want relative statistics -func (ulw Wrapper) WantRelativeStats() bool { - return ulw.ul.WantRelativeStats() -} - -// Description returns a description of the table -func (ulw Wrapper) Description() string { - var count int - for row := range ulw.ul.Results { - if ulw.ul.Results[row].Username != "" { - count++ - } - } - return fmt.Sprintf("Activity by Username (processlist) %d rows", count) -} - -// Headings returns the headings for a table -func (ulw Wrapper) Headings() string { - return fmt.Sprintf("%-10s %6s|%-10s %6s|%4s %4s|%5s %3s|%3s %3s %3s %3s %3s|%s", - "Run Time", "%", "Sleeping", "%", "Conn", "Actv", "Hosts", "DBs", "Sel", "Ins", "Upd", "Del", "Oth", "User") -} - -// content generate a printable result for a row, given the totals -func (ulw Wrapper) content(row, totals userlatency.Row) string { - return fmt.Sprintf("%10s %6s|%10s %6s|%4s %4s|%5s %3s|%3s %3s %3s %3s %3s|%s", - formatSeconds(row.Runtime), - utils.FormatPct(utils.Divide(row.Runtime, totals.Runtime)), - formatSeconds(row.Sleeptime), - utils.FormatPct(utils.Divide(row.Sleeptime, totals.Sleeptime)), - utils.FormatCounterU(row.Connections, 4), - utils.FormatCounterU(row.Active, 4), - utils.FormatCounterU(row.Hosts, 5), - utils.FormatCounterU(row.Dbs, 3), - utils.FormatCounterU(row.Selects, 3), - utils.FormatCounterU(row.Inserts, 3), - utils.FormatCounterU(row.Updates, 3), - utils.FormatCounterU(row.Deletes, 3), - utils.FormatCounterU(row.Other, 3), - row.Username) -} - // formatSeconds formats the given seconds into xxh xxm xxs or xxd xxh xxm -// for periods longer than 24h. If seconds is 0 return an empty string. +// for periods longer than 24h. If seconds is 0 return an empty string. // Leading 0 values are omitted. // e.g. 0 -> "" // @@ -173,3 +53,73 @@ func formatSeconds(d uint64) string { return fmt.Sprintf("%ds", seconds) } + +var ( + defaultSort = func(rows []userlatency.Row) { + slices.SortFunc(rows, func(a, b userlatency.Row) int { + if a.TotalTime() > b.TotalTime() { + return -1 + } + if a.TotalTime() < b.TotalTime() { + return 1 + } + if a.Connections > b.Connections { + return -1 + } + if a.Connections < b.Connections { + return 1 + } + if a.Username < b.Username { + return -1 + } + if a.Username > b.Username { + return 1 + } + return 0 + }) + } + + defaultHasData = func(r userlatency.Row) bool { return r.Username != "" } + + defaultContent = func(row, totals userlatency.Row) string { + return fmt.Sprintf("%10s %6s|%10s %6s|%4s %4s|%5s %3s|%3s %3s %3s %3s %3s|%s", + formatSeconds(row.Runtime), + utils.FormatPct(utils.Divide(row.Runtime, totals.Runtime)), + formatSeconds(row.Sleeptime), + utils.FormatPct(utils.Divide(row.Sleeptime, totals.Sleeptime)), + utils.FormatCounterU(row.Connections, 4), + utils.FormatCounterU(row.Active, 4), + utils.FormatCounterU(row.Hosts, 5), + utils.FormatCounterU(row.Dbs, 3), + utils.FormatCounterU(row.Selects, 3), + utils.FormatCounterU(row.Inserts, 3), + utils.FormatCounterU(row.Updates, 3), + utils.FormatCounterU(row.Deletes, 3), + utils.FormatCounterU(row.Other, 3), + row.Username) + } +) + +// Wrapper wraps a UserLatency struct. +type Wrapper struct { + *wrapper.BaseWrapper[userlatency.Row, *userlatency.UserLatency] +} + +// NewUserLatency creates a wrapper around UserLatency. +func NewUserLatency(cfg *config.Config, db *sql.DB) *Wrapper { + ul := userlatency.NewUserLatency(cfg, db) + bw := wrapper.NewBaseWrapper( + ul, + "Activity by Username (processlist)", + defaultSort, + defaultHasData, + defaultContent, + ) + return &Wrapper{BaseWrapper: bw} +} + +// Headings returns the headings for a table. +func (w *Wrapper) Headings() string { + return fmt.Sprintf("%-10s %6s|%-10s %6s|%4s %4s|%5s %3s|%3s %3s %3s %3s %3s|%s", + "Run Time", "%", "Sleeping", "%", "Conn", "Actv", "Hosts", "DBs", "Sel", "Ins", "Upd", "Del", "Oth", "User") +} diff --git a/wrapper/userlatency/wrapper_test.go b/wrapper/userlatency/wrapper_test.go index c394607..51766d4 100644 --- a/wrapper/userlatency/wrapper_test.go +++ b/wrapper/userlatency/wrapper_test.go @@ -4,6 +4,8 @@ import ( "testing" ) +// TestFormatSeconds verifies the formatSeconds function produces the expected +// human-readable duration strings for various input values. func TestFormatSeconds(t *testing.T) { data := []struct { input uint64