Configurable update interval (100 ms – 2 s): drives daemon sampling and UI polling, saved with the dashboard layout
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
eb70daf129
commit
1751afc974
+65
-31
@@ -25,11 +25,13 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
Interval = 2 * time.Second
|
||||
History = 300 // samples kept (10 min at 2 s)
|
||||
fsEvery = 15 // sample fs usage every N ticks
|
||||
upsEvery = 5 // sample UPS every N ticks
|
||||
sectorSize = 512 // /proc/diskstats always counts 512-byte sectors
|
||||
DefaultInterval = 2 * time.Second
|
||||
MinInterval = 100 * time.Millisecond
|
||||
MaxInterval = 2 * time.Second
|
||||
History = 300 // samples kept; the window is History × interval (30 s at 100 ms, 10 min at 2 s)
|
||||
fsEvery = 30 * time.Second // filesystem usage changes slowly and statfs on network mounts isn't free
|
||||
upsEvery = 10 * time.Second // UPS daemons are an exec each; don't hammer them at fast intervals
|
||||
sectorSize = 512 // /proc/diskstats always counts 512-byte sectors
|
||||
)
|
||||
|
||||
type Mem struct {
|
||||
@@ -94,19 +96,20 @@ type UPS struct {
|
||||
}
|
||||
|
||||
type Sample struct {
|
||||
T int64 `json:"t"` // unix ms
|
||||
CPU float64 `json:"cpu"`
|
||||
Cores []float64 `json:"cores"`
|
||||
Load [3]float64 `json:"load"`
|
||||
Mem Mem `json:"mem"`
|
||||
Swap Mem `json:"swap"`
|
||||
Temps []Temp `json:"temps"`
|
||||
Fans []Fan `json:"fans"`
|
||||
Net []Net `json:"net"`
|
||||
Disks []Disk `json:"disks"`
|
||||
FS []FS `json:"fs"`
|
||||
UPS *UPS `json:"ups"`
|
||||
Uptime float64 `json:"uptime"`
|
||||
T int64 `json:"t"` // unix ms
|
||||
Interval int64 `json:"interval_ms"` // sampling interval in effect
|
||||
CPU float64 `json:"cpu"`
|
||||
Cores []float64 `json:"cores"`
|
||||
Load [3]float64 `json:"load"`
|
||||
Mem Mem `json:"mem"`
|
||||
Swap Mem `json:"swap"`
|
||||
Temps []Temp `json:"temps"`
|
||||
Fans []Fan `json:"fans"`
|
||||
Net []Net `json:"net"`
|
||||
Disks []Disk `json:"disks"`
|
||||
FS []FS `json:"fs"`
|
||||
UPS *UPS `json:"ups"`
|
||||
Uptime float64 `json:"uptime"`
|
||||
}
|
||||
|
||||
// Series is the compact history the UI seeds its sparklines from.
|
||||
@@ -120,21 +123,25 @@ type Series struct {
|
||||
}
|
||||
|
||||
type Collector struct {
|
||||
mu sync.RWMutex
|
||||
ring []Sample
|
||||
tick int
|
||||
mu sync.RWMutex
|
||||
ring []Sample
|
||||
interval time.Duration
|
||||
wake chan struct{} // nudges Run when the interval changes
|
||||
|
||||
prevCPU []cpuTimes
|
||||
prevNet map[string][2]uint64
|
||||
prevDisk map[string][3]uint64 // sectors read, sectors written, io ticks ms
|
||||
prevT time.Time
|
||||
lastFS []FS
|
||||
fsAt time.Time
|
||||
lastUPS *UPS
|
||||
upsAt time.Time
|
||||
upsProbe func() *UPS // nil when no UPS daemon is installed
|
||||
}
|
||||
|
||||
func NewCollector() *Collector {
|
||||
c := &Collector{prevNet: map[string][2]uint64{}, prevDisk: map[string][3]uint64{}}
|
||||
c := &Collector{prevNet: map[string][2]uint64{}, prevDisk: map[string][3]uint64{},
|
||||
interval: DefaultInterval, wake: make(chan struct{}, 1)}
|
||||
switch {
|
||||
case lookPath("pwrstat"):
|
||||
c.upsProbe = upsPwrstat
|
||||
@@ -146,21 +153,49 @@ func NewCollector() *Collector {
|
||||
return c
|
||||
}
|
||||
|
||||
// Run samples until ctx is cancelled.
|
||||
// Run samples until ctx is cancelled, at whatever interval is current.
|
||||
func (c *Collector) Run(ctx context.Context) {
|
||||
c.sample() // prime deltas
|
||||
t := time.NewTicker(Interval)
|
||||
defer t.Stop()
|
||||
for {
|
||||
t := time.NewTimer(c.Interval())
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Stop()
|
||||
return
|
||||
case <-c.wake: // interval changed: restart the wait
|
||||
t.Stop()
|
||||
case <-t.C:
|
||||
c.sample()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetInterval changes the sampling period, clamped to [MinInterval, MaxInterval].
|
||||
func (c *Collector) SetInterval(d time.Duration) {
|
||||
if d < MinInterval {
|
||||
d = MinInterval
|
||||
}
|
||||
if d > MaxInterval {
|
||||
d = MaxInterval
|
||||
}
|
||||
c.mu.Lock()
|
||||
changed := d != c.interval
|
||||
c.interval = d
|
||||
c.mu.Unlock()
|
||||
if changed {
|
||||
select {
|
||||
case c.wake <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Collector) Interval() time.Duration {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.interval
|
||||
}
|
||||
|
||||
func (c *Collector) Latest() (Sample, bool) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
@@ -216,7 +251,7 @@ func (c *Collector) sample() {
|
||||
first := c.prevT.IsZero()
|
||||
c.prevT = now
|
||||
|
||||
s := Sample{T: now.UnixMilli(), Temps: []Temp{}, Fans: []Fan{}, Net: []Net{}, Disks: []Disk{}}
|
||||
s := Sample{T: now.UnixMilli(), Interval: c.Interval().Milliseconds(), Temps: []Temp{}, Fans: []Fan{}, Net: []Net{}, Disks: []Disk{}}
|
||||
s.CPU, s.Cores = c.cpu()
|
||||
s.Load = loadavg()
|
||||
s.Mem, s.Swap = meminfo()
|
||||
@@ -228,15 +263,14 @@ func (c *Collector) sample() {
|
||||
s.Net = c.net(dt)
|
||||
s.Disks = c.disks(dt)
|
||||
|
||||
if c.tick%fsEvery == 0 || c.lastFS == nil {
|
||||
c.lastFS = filesystems()
|
||||
if c.lastFS == nil || now.Sub(c.fsAt) >= fsEvery {
|
||||
c.lastFS, c.fsAt = filesystems(), now
|
||||
}
|
||||
s.FS = c.lastFS
|
||||
if c.upsProbe != nil && (c.tick%upsEvery == 0 || c.lastUPS == nil) {
|
||||
c.lastUPS = c.upsProbe()
|
||||
if c.upsProbe != nil && (c.upsAt.IsZero() || now.Sub(c.upsAt) >= upsEvery) {
|
||||
c.lastUPS, c.upsAt = c.upsProbe(), now
|
||||
}
|
||||
s.UPS = c.lastUPS
|
||||
c.tick++
|
||||
|
||||
if first {
|
||||
return // deltas are meaningless on the priming pass
|
||||
|
||||
Reference in New Issue
Block a user