Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions model/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
105 changes: 105 additions & 0 deletions wrapper/base.go
Original file line number Diff line number Diff line change
@@ -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
}
35 changes: 0 additions & 35 deletions wrapper/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package wrapper
import (
"fmt"

"github.com/sjmudd/ps-top/model/tableio"
"github.com/sjmudd/ps-top/utils"
)

Expand Down Expand Up @@ -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.
Expand All @@ -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)
}
154 changes: 54 additions & 100 deletions wrapper/fileinfolatency/wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,74 @@ import (
"database/sql"
"fmt"
"slices"
"time"

"github.com/sjmudd/ps-top/config"
"github.com/sjmudd/ps-top/model/fileinfo"
"github.com/sjmudd/ps-top/utils"
"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",
"%",
Expand All @@ -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)
}
Loading
Loading