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:
co-authored by
Claude Opus 5
parent
885b050d02
commit
04dd98e2cd
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user