Settings: one window for how far back every graph reaches, 1 minute to 24 hours

Each machine keeps 300 points per curve whatever the window, so memory doesn't follow it;
a wider window means wider points, each the average of its bucket rather than one instant
out of it. The hub passes the setting to every machine it manages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-20 19:07:55 -07:00
co-authored by Claude Opus 5
parent 885b050d02
commit 04dd98e2cd
13 changed files with 533 additions and 33 deletions
+82 -9
View File
@@ -32,7 +32,12 @@ const (
DefaultInterval = 2 * time.Second
MinInterval = 100 * time.Millisecond
MaxInterval = 10 * time.Second
History = 300 // samples kept; the window is History × interval (30 s at 100 ms, 10 min at 2 s)
History = 300 // points kept per curve; the window below sets how far apart they are
// How far back the kept history reaches. The point count is fixed, so this sets the spacing:
// at a minute every sample is its own point, at a day each point is the average of ~5 minutes.
DefaultWindow = 10 * time.Minute
MinWindow = 1 * time.Minute
MaxWindow = 24 * time.Hour
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
@@ -138,8 +143,11 @@ type Sample struct {
type Collector struct {
mu sync.RWMutex
ring []Sample
ring []Sample // the kept history: one point per stride, each the average of its bucket
last Sample // the newest raw sample, whatever the stride
bucket []Sample // samples since the last point was kept
interval time.Duration
window time.Duration
wake chan struct{} // nudges Run when the interval changes
prevCPU []cpuTimes
@@ -162,7 +170,7 @@ type Collector struct {
func NewCollector() *Collector {
c := &Collector{prevNet: map[string][2]uint64{}, prevDisk: map[string][3]uint64{},
interval: DefaultInterval, wake: make(chan struct{}, 1)}
interval: DefaultInterval, window: DefaultWindow, wake: make(chan struct{}, 1)}
switch {
case lookPath("pwrstat"):
c.upsProbe = upsPwrstat
@@ -223,13 +231,80 @@ func (c *Collector) Interval() time.Duration {
return c.interval
}
// SetWindow sets how far back the kept history reaches, clamped to [MinWindow, MaxWindow].
func (c *Collector) SetWindow(d time.Duration) {
if d < MinWindow {
d = MinWindow
}
if d > MaxWindow {
d = MaxWindow
}
c.mu.Lock()
defer c.mu.Unlock()
c.window = d
c.trim()
}
func (c *Collector) Window() time.Duration {
c.mu.RLock()
defer c.mu.RUnlock()
return c.window
}
// Stride is how far apart the kept points are: the sampling interval until the window is
// wider than History samples of it, then the window split History ways.
func (c *Collector) Stride() time.Duration {
c.mu.RLock()
defer c.mu.RUnlock()
return c.stride()
}
func (c *Collector) stride() time.Duration {
if s := c.window / History; s > c.interval {
return s
}
return c.interval
}
// keep folds samples into the history, one point per stride. Averaged, not sampled: a CPU
// graph of the last six hours should show the load through each five minutes, not whichever
// two-second instant happened to land on the mark.
func (c *Collector) keep(s Sample) {
c.bucket = append(c.bucket, s)
if s.T-c.bucket[0].T < c.stride().Milliseconds() && len(c.bucket) > 1 {
return
}
c.ring = append(c.ring, fold(c.bucket))
c.bucket = c.bucket[:0]
c.trim()
}
// trim drops points past the count budget and past the window. Caller holds c.mu.
func (c *Collector) trim() {
if len(c.ring) > History {
c.ring = c.ring[len(c.ring)-History:]
}
n := len(c.ring)
if n == 0 {
return
}
cut := c.ring[n-1].T - c.window.Milliseconds()
i := 0
for i < n && c.ring[i].T < cut {
i++
}
if i > 0 {
c.ring = c.ring[i:]
}
}
func (c *Collector) Latest() (Sample, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
if len(c.ring) == 0 {
if c.last.T == 0 {
return Sample{}, false
}
return c.ring[len(c.ring)-1], true
return c.last, true
}
// ---- sampling ----
@@ -265,10 +340,8 @@ func (c *Collector) sample() {
return // deltas are meaningless on the priming pass
}
c.mu.Lock()
c.ring = append(c.ring, s)
if len(c.ring) > History {
c.ring = c.ring[len(c.ring)-History:]
}
c.last = s
c.keep(s)
c.mu.Unlock()
c.subMu.Lock()