ZFS tab: pools, caching tiers and writes full width; write and drain rates measured between real updates

A txg records its bytes only when it syncs and the mover reports every 2 s, so a per-sample delta
read 0 most of the time. Rates are now taken over the gap between syncs (or mover updates) and held
until the next one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-10-25 22:34:11 -07:00
co-authored by Claude Opus 5.5
parent 86cfcd2bad
commit 50768e2d94
3 changed files with 36 additions and 12 deletions
+32 -9
View File
@@ -109,10 +109,14 @@ type zfsWritePrev struct {
at time.Time
delay uint64
overMax uint64
txg map[string]uint64 // pool -> highest synced txg counted
logical map[string]uint64 // pool -> sum of objset nwritten
drained map[string]uint64 // landing branch -> moved + partial bytes
landing map[string]Landing // branch -> last walk
txg map[string]uint64 // pool -> highest synced txg counted
txgAt map[string]time.Time // pool -> when that txg was first seen
txgRate map[string]float64 // pool -> bytes/s over the last gap between syncs
logical map[string]uint64 // pool -> sum of objset nwritten
drained map[string]uint64 // landing branch -> moved + partial bytes, at the mover's last update
drainedAt map[string]int64 // landing branch -> that update's time, unix ms
drainRate map[string]float64 // landing branch -> bytes/s between its last two updates
landing map[string]Landing // branch -> last walk
landingAt time.Time
}
@@ -378,6 +382,7 @@ func (c *Collector) zfsWrites(now time.Time, arc *ARC, disks []Disk) *ZFSWrites
p.delay, p.overMax = tx["dmu_tx_dirty_delay"], tx["dmu_tx_dirty_over_max"]
if p.txg == nil {
p.txg, p.logical, p.drained, p.landing = map[string]uint64{}, map[string]uint64{}, map[string]uint64{}, map[string]Landing{}
p.txgAt, p.txgRate, p.drainedAt, p.drainRate = map[string]time.Time{}, map[string]float64{}, map[string]int64{}, map[string]float64{}
}
ms := readMounts()
@@ -394,11 +399,22 @@ func (c *Collector) zfsWrites(now time.Time, arc *ARC, disks []Disk) *ZFSWrites
for _, pool := range poolsWithKstats() {
pw := PoolWrites{Pool: pool}
// A txg records what it wrote only when it has synced — every few seconds, not every
// sample — so the rate is taken over the gap between syncs and held until the next one.
prevTop, had := p.txg[pool]
written, top := parseTxgs(readStr(zfsKstatDir+pool+"/txgs"), prevTop)
if had && dt > 0 {
pw.WriteBps = round1(float64(written) / dt)
switch {
case !had:
p.txgAt[pool] = now
case top > prevTop:
if gap := now.Sub(p.txgAt[pool]).Seconds(); gap > 0 {
p.txgRate[pool] = round1(float64(written) / gap)
}
p.txgAt[pool] = now
case now.Sub(p.txgAt[pool]) > time.Duration(2*max(w.TxgTimeoutSec, 5))*time.Second:
p.txgRate[pool] = 0 // no sync for two timeouts: nothing is being written
}
pw.WriteBps = p.txgRate[pool]
p.txg[pool] = top
lw := objsetWritten(pool)
if prev, ok := p.logical[pool]; ok && dt > 0 && lw >= prev {
@@ -418,11 +434,18 @@ func (c *Collector) zfsWrites(now time.Time, arc *ARC, disks []Disk) *ZFSWrites
l.InBps = diskW[l.Device]
if mv := movers[filepath.Clean(l.Branch)]; mv != nil {
l.Mover = mv
// the mover writes its status every couple of seconds: measure between its updates
done := mv.MovedBytes + mv.Partial
if prev, ok := p.drained[l.Branch]; ok && dt > 0 && done >= prev {
l.DrainBps = round1(float64(done-prev) / dt)
if at, ok := p.drainedAt[l.Branch]; !ok || mv.UpdatedMs != at {
if ok && mv.UpdatedMs > at && done >= p.drained[l.Branch] {
p.drainRate[l.Branch] = round1(float64(done-p.drained[l.Branch]) / (float64(mv.UpdatedMs-at) / 1000))
}
p.drained[l.Branch], p.drainedAt[l.Branch] = done, mv.UpdatedMs
}
p.drained[l.Branch] = done
if mv.State != "moving" {
p.drainRate[l.Branch] = 0
}
l.DrainBps = p.drainRate[l.Branch]
}
pw.Landing = &l
}