Refresh rate is honoured exactly everywhere: no list floor, sampler up to 10 s, server caches reduced to coalescing only

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-18 13:32:36 -07:00
co-authored by Claude Fable 5.1
parent bbf0f2580f
commit 4b8a2b15ed
7 changed files with 12 additions and 12 deletions
+1 -1
View File
@@ -390,7 +390,7 @@ var cronLogRe = regexp.MustCompile(`^\((\S+)\) CMD \((.*)\)$`)
// lastRunsFromJournal maps user+command to the most recent "(user) CMD (command)" line. // lastRunsFromJournal maps user+command to the most recent "(user) CMD (command)" line.
func (m *Manager) lastRunsFromJournal(ctx context.Context) map[string]int64 { func (m *Manager) lastRunsFromJournal(ctx context.Context) map[string]int64 {
m.mu.Lock() m.mu.Lock()
if time.Since(m.lastAt) < 60*time.Second && m.lastRuns != nil { if time.Since(m.lastAt) < 5*time.Second && m.lastRuns != nil {
r := m.lastRuns r := m.lastRuns
m.mu.Unlock() m.mu.Unlock()
return r return r
+2 -2
View File
@@ -29,7 +29,7 @@ import (
const ( const (
DefaultInterval = 2 * time.Second DefaultInterval = 2 * time.Second
MinInterval = 100 * time.Millisecond MinInterval = 100 * time.Millisecond
MaxInterval = 2 * time.Second 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 // 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 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 upsEvery = 10 * time.Second // UPS daemons are an exec each; don't hammer them at fast intervals
@@ -982,7 +982,7 @@ func (c *Collector) Processes() ProcTable {
c.prevProc = map[int]procTimes{} c.prevProc = map[int]procTimes{}
c.procInfo = map[int]procStatic{} c.procInfo = map[int]procStatic{}
} }
if now.Sub(c.procAt) < 500*time.Millisecond && c.lastTable != nil { // coalesce bursts of clients if now.Sub(c.procAt) < 100*time.Millisecond && c.lastTable != nil { // coalesce simultaneous clients only
return *c.lastTable return *c.lastTable
} }
ents, err := os.ReadDir("/proc") ents, err := os.ReadDir("/proc")
+1 -1
View File
@@ -107,7 +107,7 @@ func run(ctx context.Context, args ...string) (string, error) {
// Cached for a few seconds because the UI polls it. // Cached for a few seconds because the UI polls it.
func (m *Manager) List(ctx context.Context) ([]Unit, error) { func (m *Manager) List(ctx context.Context) ([]Unit, error) {
m.mu.Lock() m.mu.Lock()
if time.Since(m.cacheAt) < 4*time.Second && m.cache != nil { if time.Since(m.cacheAt) < 200*time.Millisecond && m.cache != nil { // coalesce simultaneous clients only
c := m.cache c := m.cache
m.mu.Unlock() m.mu.Unlock()
return c, nil return c, nil
+1 -1
View File
@@ -25,7 +25,7 @@ const MODULES: Module[] = [
const byId = (id: string) => MODULES.find((m) => m.id === id) const byId = (id: string) => MODULES.find((m) => m.id === id)
const DEFAULT_INTERVAL = 2000 const DEFAULT_INTERVAL = 2000
const clampSampler = (ms: number) => Math.min(2000, Math.max(100, ms)) const clampSampler = (ms: number) => Math.min(10000, Math.max(100, ms))
interface Layout { order: string[]; hidden: Set<string>; hiddenItems: Set<string>; intervalMs: number } interface Layout { order: string[]; hidden: Set<string>; hiddenItems: Set<string>; intervalMs: number }
+3 -3
View File
@@ -2,7 +2,7 @@ import { FormEvent, useCallback, useEffect, useState } from 'react'
import { api, Me } from './api' import { api, Me } from './api'
import { applyTheme, currentThemeId, THEMES } from './themes' import { applyTheme, currentThemeId, THEMES } from './themes'
import { Btn, Loading, toast } from './ui' import { Btn, Loading, toast } from './ui'
import { fmtMs, getRefreshMs, REFRESH_OPTIONS, setRefreshMs, LIST_FLOOR } from './prefs' import { fmtMs, getRefreshMs, REFRESH_OPTIONS, setRefreshMs } from './prefs'
const fmtUptime = (s: number) => { const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600), m = Math.floor((s % 3600) / 60); return d > 0 ? `${d}d ${h}h` : h > 0 ? `${h}h ${m}m` : `${m}m` } const fmtUptime = (s: number) => { const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600), m = Math.floor((s % 3600) / 60); return d > 0 ? `${d}d ${h}h` : h > 0 ? `${h}h ${m}m` : `${m}m` }
@@ -81,8 +81,8 @@ export default function Settings({ me }: { me: Me }) {
{REFRESH_OPTIONS.map((ms) => <button key={ms} className={'chip' + (refresh === ms ? ' on' : '')} onClick={() => pickRefresh(ms)}>{fmtMs(ms)}</button>)} {REFRESH_OPTIONS.map((ms) => <button key={ms} className={'chip' + (refresh === ms ? ' on' : '')} onClick={() => pickRefresh(ms)}>{fmtMs(ms)}</button>)}
</div> </div>
<div className="muted small" style={{ marginTop: 8 }}> <div className="muted small" style={{ marginTop: 8 }}>
Dashboard charts, the Processes tab, disk activity and the machine list all poll at this rate. The dashboard's sampler on each machine follows it too (fastest 100 ms, slowest 2 s; charts keep 300 points, so the window is 300 × rate). Every page polls at exactly this rate: dashboard, processes, disks, services, cron, updates, machines. The sampler on each machine follows it too, so charts keep 300 points and cover 300 × rate.
Lists that shell out on the host — services, cron, updates — never go faster than {fmtMs(LIST_FLOOR)}. Fast rates cost CPU on the host, especially on the list pages, which run commands to refresh. Nothing is throttled behind your back.
</div> </div>
</section> </section>
+2 -2
View File
@@ -74,7 +74,7 @@ export default function Shell({ me: hubMe, onLoggedOut }: { me: Me; onLoggedOut:
// Machine list for the switcher (hub-local), refreshed while the console is open. // Machine list for the switcher (hub-local), refreshed while the console is open.
const loadMachines = useCallback(async () => { try { setMachines((await api.machines()).nodes) } catch { /* not a hub or offline */ } }, []) const loadMachines = useCallback(async () => { try { setMachines((await api.machines()).nodes) } catch { /* not a hub or offline */ } }, [])
useEffect(() => { loadMachines(); const t = setInterval(loadMachines, Math.max(listRate, 5000)); return () => clearInterval(t) }, [loadMachines, listRate]) useEffect(() => { loadMachines(); const t = setInterval(loadMachines, listRate); return () => clearInterval(t) }, [loadMachines, listRate])
// Switching machines: reset everything that belongs to a host, then re-fetch its identity. // Switching machines: reset everything that belongs to a host, then re-fetch its identity.
const switchMachine = (name: string | null) => { const switchMachine = (name: string | null) => {
@@ -88,7 +88,7 @@ export default function Shell({ me: hubMe, onLoggedOut }: { me: Me; onLoggedOut:
let stop = false let stop = false
const tick = async () => { try { const m = await api.me(); if (!stop) { setRemoteMe(m); setRemoteErr('') } } catch (e) { if (!stop) setRemoteErr(e instanceof Error ? e.message : 'unreachable') } } const tick = async () => { try { const m = await api.me(); if (!stop) { setRemoteMe(m); setRemoteErr('') } } catch (e) { if (!stop) setRemoteErr(e instanceof Error ? e.message : 'unreachable') } }
tick() tick()
const t = setInterval(tick, Math.max(listRate, 5000)) const t = setInterval(tick, listRate)
return () => { stop = true; clearInterval(t) } return () => { stop = true; clearInterval(t) }
}, [machine, listRate]) }, [machine, listRate])
+2 -2
View File
@@ -4,8 +4,8 @@ import { useEffect, useState } from 'react'
export const REFRESH_OPTIONS = [250, 500, 1000, 2000, 5000] as const export const REFRESH_OPTIONS = [250, 500, 1000, 2000, 5000] as const
export const DEFAULT_REFRESH = 2000 export const DEFAULT_REFRESH = 2000
// Lists that shell out on the host (services, cron, updates, machines) never poll faster than this. // No floors: the rate means exactly what it says on every page, at the host's expense.
export const LIST_FLOOR = 2000 export const LIST_FLOOR = 0
const KEY = 'hamlab.refresh' const KEY = 'hamlab.refresh'
export function getRefreshMs(): number { export function getRefreshMs(): number {