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
96 changes: 96 additions & 0 deletions model/base.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package model

import (
"time"

"github.com/sjmudd/ps-top/config"
)

// BaseCollector encapsulates the common collection state and logic for all models.
// T is the row type (e.g., tableio.Row, memoryusage.Row)
// R is a slice of T (e.g., []Row, or a named type like Rows)
type BaseCollector[T any, R ~[]T] struct {
config *config.Config
db QueryExecutor

FirstCollected time.Time
LastCollected time.Time
First R // baseline snapshot
Last R // most recent raw data collection
Results R // processed results (after subtraction, etc.)
Totals T // totals row computed from results
process ProcessFunc[T, R]
}

// NewBaseCollector creates a new BaseCollector with the given config, database, and process function.
func NewBaseCollector[T any, R ~[]T](cfg *config.Config, db QueryExecutor, process ProcessFunc[T, R]) *BaseCollector[T, R] {
return &BaseCollector[T, R]{
config: cfg,
db: db,
process: process,
}
}

// ProcessFunc defines the transformation from raw data to displayable results.
// It receives the last collected data and the baseline (first) data,
// and returns the processed results along with their totals.
type ProcessFunc[T any, R ~[]T] func(last, first R) (results R, totals T)

// WantRefreshFunc determines whether the baseline should be refreshed
// (i.e., copy last to first) based on current state.
type WantRefreshFunc = func() bool

// FetchFunc retrieves raw data from the database.
type FetchFunc[R any] func() (R, error)

// Collect orchestrates a full collection cycle:
// 1. Fetch raw data via fetchFunc
// 2. Optionally refresh baseline if wantRefresh returns true
// 3. Process data via the stored process function to produce results and totals
func (bc *BaseCollector[T, R]) Collect(
fetch FetchFunc[R],
wantRefresh WantRefreshFunc,
) {
// Fetch the latest data
last, err := fetch()
if err != nil {
// TODO: log error? For now, skip this collection cycle.
return
}

// Update last snapshot and timestamp
bc.Last = last
bc.LastCollected = time.Now()
if bc.FirstCollected.IsZero() {
bc.FirstCollected = bc.LastCollected
}

// Refresh baseline if needed (e.g., on first collection or wrap-around)
if wantRefresh() {
bc.First = make(R, len(last))
copy(bc.First, last)
bc.FirstCollected = bc.LastCollected
}

// Process results and compute totals using the stored process function
bc.Results, bc.Totals = bc.process(bc.Last, bc.First)
}

// ResetStatistics sets the baseline to the last collected values.
// This is used when the user requests a manual reset.
func (bc *BaseCollector[T, R]) ResetStatistics() {
bc.First = make(R, len(bc.Last))
copy(bc.First, bc.Last)
bc.FirstCollected = bc.LastCollected
bc.Results, bc.Totals = bc.process(bc.Last, bc.First)
}

// Config returns the collector's configuration
func (bc *BaseCollector[T, R]) Config() *config.Config {
return bc.config
}

// DB returns the QueryExecutor (for use in fetch functions)
func (bc *BaseCollector[T, R]) DB() QueryExecutor {
return bc.db
}
85 changes: 28 additions & 57 deletions model/fileinfo/fileinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,86 +2,57 @@
package fileinfo

import (
"database/sql"
"log"
"time"

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

// FileIoLatency represents the contents of the data collected from file_summary_by_instance
type FileIoLatency struct {
config *config.Config
FirstCollected time.Time // the first collection time (for relative data)
LastCollected time.Time // the last collection time
first Rows
last Rows
Results Rows
Totals Row
db *sql.DB
*model.BaseCollector[Row, Rows]
}

// NewFileSummaryByInstance creates a new structure and include various variable values:
// - datadir, relay_log
// There's no checking that these are actually provided!
func NewFileSummaryByInstance(cfg *config.Config, db *sql.DB) *FileIoLatency {
fiol := &FileIoLatency{
db: db,
config: cfg,
func NewFileSummaryByInstance(cfg *config.Config, db model.QueryExecutor) *FileIoLatency {
process := func(last, first Rows) (Rows, Row) {
results := make(Rows, len(last))
copy(results, last)
if cfg.WantRelativeStats() {
results.subtract(first)
}
tot := totals(results)
return results, tot
}

return fiol
}

// ResetStatistics resets the statistics to last values
func (fiol *FileIoLatency) ResetStatistics() {
fiol.first = utils.DuplicateSlice(fiol.last)
fiol.FirstCollected = fiol.LastCollected

fiol.calculate()
bc := model.NewBaseCollector[Row, Rows](cfg, db, process)
return &FileIoLatency{BaseCollector: bc}
}

// Collect data from the db, then merge it in.
func (fiol *FileIoLatency) Collect() {
start := time.Now()
fiol.last = FileInfo2MySQLNames(
fiol.config.Variables().Get("datadir"),
fiol.config.Variables().Get("relaylog"),
collect(fiol.db),
)
fiol.LastCollected = time.Now()

// copy in first data if it was not there
// or check for reload initial characteristics
if (len(fiol.first) == 0 && len(fiol.last) > 0) || fiol.first.needsRefresh(fiol.last) {
fiol.first = utils.DuplicateSlice(fiol.last)
fiol.FirstCollected = fiol.LastCollected
bc := fiol.BaseCollector
fetch := func() (Rows, error) {
raw := collect(bc.DB())
// Apply transformation using config variables
transformed := FileInfo2MySQLNames(
bc.Config().Variables().Get("datadir"),
bc.Config().Variables().Get("relaylog"),
raw,
)
return transformed, nil
}

fiol.calculate()

log.Println("fiol.first.totals():", totals(fiol.first))
log.Println("fiol.last.totals():", totals(fiol.last))
log.Println("FileIoLatency.Collect() took:", time.Since(start))
}

func (fiol *FileIoLatency) calculate() {
fiol.Results = utils.DuplicateSlice(fiol.last)

if fiol.config.WantRelativeStats() {
fiol.Results.subtract(fiol.first)
wantRefresh := func() bool {
return (len(bc.First) == 0 && len(bc.Last) > 0) || totals(bc.First).SumTimerWait > totals(bc.Last).SumTimerWait
}

fiol.Totals = totals(fiol.Results)
bc.Collect(fetch, wantRefresh)
}

// HaveRelativeStats is true for this object
func (fiol FileIoLatency) HaveRelativeStats() bool {
return true
}

// WantRelativeStats
// WantRelativeStats returns the config setting.
func (fiol FileIoLatency) WantRelativeStats() bool {
return fiol.config.WantRelativeStats()
return fiol.Config().WantRelativeStats()
}
10 changes: 2 additions & 8 deletions model/fileinfo/rows.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
package fileinfo

import (
"database/sql"
"time"

"github.com/sjmudd/ps-top/log"
"github.com/sjmudd/ps-top/model"
)

// Config provides an interface for getting a configuration value from a key/value store
Expand Down Expand Up @@ -46,7 +46,7 @@ func (rows Rows) Valid() bool {
}

// Select the raw data from the database into Rows
func collect(db *sql.DB) Rows {
func collect(db model.QueryExecutor) Rows {
log.Println("collect() starts")
var t Rows
start := time.Now()
Expand Down Expand Up @@ -149,9 +149,3 @@ func (rows *Rows) subtract(initial Rows) {
log.Println("WARNING: END")
}
}

// if the data in t2 is "newer", "has more values" than t then it needs refreshing.
// check this by comparing totals.
func (rows Rows) needsRefresh(otherRows Rows) bool {
return totals(rows).SumTimerWait > totals(otherRows).SumTimerWait
}
74 changes: 25 additions & 49 deletions model/memoryusage/memoryusage.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,75 +3,51 @@
package memoryusage

import (
"database/sql"
"time"

_ "github.com/go-sql-driver/mysql" // keep golint happy

"github.com/sjmudd/ps-top/config"
"github.com/sjmudd/ps-top/model"
)

// MemoryUsage represents a table of rows
type MemoryUsage struct {
config *config.Config
FirstCollected time.Time // the first collection time (for relative data)
LastCollected time.Time // the last collection time
last []Row // last loaded values
Results []Row // results (maybe with subtraction)
Totals Row // totals of results
db *sql.DB
*model.BaseCollector[Row, []Row]
}

// NewMemoryUsage returns a pointer to a MemoryUsage struct
func NewMemoryUsage(cfg *config.Config, db *sql.DB) *MemoryUsage {
mu := &MemoryUsage{
db: db,
config: cfg,
func NewMemoryUsage(cfg *config.Config, db model.QueryExecutor) *MemoryUsage {
process := func(last, _ []Row) ([]Row, Row) {
results := make([]Row, len(last))
copy(results, last)
tot := totals(results)
return results, tot
}

return mu
bc := model.NewBaseCollector[Row, []Row](cfg, db, process)
return &MemoryUsage{BaseCollector: bc}
}

// Collect data from the db, no merging needed
// DEPRECATED
func (mu *MemoryUsage) Collect() {
mu.AddRows(collect(mu.db))
}

// AddRows takes an new set of rows to be added to the dataset
func (mu *MemoryUsage) AddRows(rows []Row) {
mu.last = rows
mu.LastCollected = time.Now()

mu.calculate()
}

// ResetStatistics resets the statistics to current values
func (mu *MemoryUsage) ResetStatistics() {

mu.calculate()
bc := mu.BaseCollector
fetch := func() ([]Row, error) {
return collect(bc.DB()), nil
}
wantRefresh := func() bool {
// MemoryUsage does not support relative stats, so always refresh baseline to current
return true
}
bc.Collect(fetch, wantRefresh)
}

// Rows returns the rows we have which are interesting
func (mu MemoryUsage) Rows() []Row {
rows := make([]Row, 0, len(mu.Results))
rows = append(rows, mu.Results...)

return rows
func (mu *MemoryUsage) Rows() []Row {
return mu.Results
}

// HaveRelativeStats returns if the values returned are relative to a previous collection
func (mu MemoryUsage) HaveRelativeStats() bool {
func (mu *MemoryUsage) HaveRelativeStats() bool {
return false
}

// WantRelativeStats
func (mu MemoryUsage) WantRelativeStats() bool {
return mu.config.WantRelativeStats()
}

func (mu *MemoryUsage) calculate() {
mu.Results = make([]Row, len(mu.last))
copy(mu.Results, mu.last)
mu.Totals = totals(mu.Results)
// WantRelativeStats returns whether relative stats are desired based on config
func (mu *MemoryUsage) WantRelativeStats() bool {
return mu.Config().WantRelativeStats()
}
4 changes: 2 additions & 2 deletions model/memoryusage/row.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
package memoryusage

import (
"database/sql"
"fmt"

"github.com/go-sql-driver/mysql"

"github.com/sjmudd/ps-top/log"
"github.com/sjmudd/ps-top/model"
)

/* This table does not exist in MySQL 5.6
Expand Down Expand Up @@ -103,7 +103,7 @@ func sqlErrorHandler(err error) bool {
}

// Select the raw data from the database
func collect(db *sql.DB) []Row {
func collect(db model.QueryExecutor) []Row {
var t []Row
var skip bool

Expand Down
Loading
Loading