Top wears the skin; the refresh rate reaches every machine; bug bash 4

The process table's columns fit the box they are in, the rate control saves, curves are
timed on the viewer's clock and keep to the stride, the power icon is drawn, and a young
history draws in from the right instead of being stretched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-21 10:12:45 -07:00
co-authored by Claude Fable 5.1
parent ef3dbdf543
commit 668e716601
13 changed files with 394 additions and 107 deletions
+30
View File
@@ -67,3 +67,33 @@ func TestStrideAndTrim(t *testing.T) {
t.Errorf("a window past the maximum is the maximum, got %v", c.Window())
}
}
// Points are one stride apart — not a stride and a sample — and jitter doesn't change that.
func TestKeepSpacing(t *testing.T) {
c := &Collector{interval: 2 * time.Second, window: time.Hour} // stride 12 s: six samples a point
at := int64(1_000_000)
for i := 0; i < 6*40; i++ {
jitter := int64((i*37)%41 - 20) // ±20 ms, deterministic
c.keep(Sample{T: at + jitter, CPU: float64(i % 6)})
at += 2000
}
if len(c.ring) != 40 {
t.Fatalf("240 samples at six a point are 40 points, got %d", len(c.ring))
}
for i := 1; i < len(c.ring); i++ {
if gap := c.ring[i].T - c.ring[i-1].T; gap < 11900 || gap > 12100 {
t.Fatalf("point %d is %d ms after the one before; a stride is 12000", i, gap)
}
}
if c.ring[3].CPU != 2.5 {
t.Errorf("each point is the mean of its own six samples (0…5 → 2.5), got %v", c.ring[3].CPU)
}
// and when the stride is the interval, every sample is a point
c = &Collector{interval: 2 * time.Second, window: DefaultWindow}
for i := 0; i < 50; i++ {
c.keep(Sample{T: int64(i)*2000 + int64(i%3) - 1})
}
if len(c.ring) != 50 {
t.Errorf("at the default window every sample is kept, got %d of 50", len(c.ring))
}
}
+17 -5
View File
@@ -32,15 +32,15 @@ const (
DefaultInterval = 2 * time.Second
MinInterval = 100 * time.Millisecond
MaxInterval = 10 * time.Second
History = 300 // points kept per curve; the window below sets how far apart they are
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
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 {
@@ -146,6 +146,7 @@ type Collector struct {
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
lastKept int64 // when that was, unix ms
interval time.Duration
window time.Duration
wake chan struct{} // nudges Run when the interval changes
@@ -271,9 +272,20 @@ func (c *Collector) stride() time.Duration {
// 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 {
// A point is due a stride after the last one (for the first, a stride after the interval its
// first sample stands for). Half an interval of slack, because a timer never fires exactly on
// time: without it a tick that lands 5 ms early is "not yet", and every point runs one sample
// long. Measured from the last point kept, not by counting samples, so a machine that stalled
// or slept simply keeps the next sample it gets.
iv := c.interval.Milliseconds()
since := c.lastKept
if since == 0 {
since = c.bucket[0].T - iv
}
if s.T-since < c.stride().Milliseconds()-iv/2 {
return
}
c.lastKept = s.T
c.ring = append(c.ring, fold(c.bucket))
c.bucket = c.bucket[:0]
c.trim()