refactor load average handling (#982)

- Transitioned from individual load average fields (LoadAvg1, LoadAvg5,
LoadAvg15) to a single array (LoadAvg)
- Ensure load is displayed when all zero values.
This commit is contained in:
henrygd
2025-07-25 18:07:11 -04:00
parent 6953edf59e
commit 91679b5cc0
10 changed files with 116 additions and 69 deletions

View File

@@ -80,10 +80,11 @@ func (a *Agent) getSystemStats() system.Stats {
// load average // load average
if avgstat, err := load.Avg(); err == nil { if avgstat, err := load.Avg(); err == nil {
systemStats.LoadAvg1 = twoDecimals(avgstat.Load1) // TODO: remove these in future release in favor of load avg array
systemStats.LoadAvg5 = twoDecimals(avgstat.Load5) systemStats.LoadAvg[0] = avgstat.Load1
systemStats.LoadAvg15 = twoDecimals(avgstat.Load15) systemStats.LoadAvg[1] = avgstat.Load5
slog.Debug("Load average", "5m", systemStats.LoadAvg5, "15m", systemStats.LoadAvg15) systemStats.LoadAvg[2] = avgstat.Load15
slog.Debug("Load average", "5m", avgstat.Load5, "15m", avgstat.Load15)
} else { } else {
slog.Error("Error getting load average", "err", err) slog.Error("Error getting load average", "err", err)
} }
@@ -255,9 +256,11 @@ func (a *Agent) getSystemStats() system.Stats {
// update base system info // update base system info
a.systemInfo.Cpu = systemStats.Cpu a.systemInfo.Cpu = systemStats.Cpu
a.systemInfo.LoadAvg1 = systemStats.LoadAvg1 a.systemInfo.LoadAvg = systemStats.LoadAvg
a.systemInfo.LoadAvg5 = systemStats.LoadAvg5 // TODO: remove these in future release in favor of load avg array
a.systemInfo.LoadAvg15 = systemStats.LoadAvg15 a.systemInfo.LoadAvg1 = systemStats.LoadAvg[0]
a.systemInfo.LoadAvg5 = systemStats.LoadAvg[1]
a.systemInfo.LoadAvg15 = systemStats.LoadAvg[2]
a.systemInfo.MemPct = systemStats.MemPct a.systemInfo.MemPct = systemStats.MemPct
a.systemInfo.DiskPct = systemStats.DiskPct a.systemInfo.DiskPct = systemStats.DiskPct
a.systemInfo.Uptime, _ = host.Uptime() a.systemInfo.Uptime, _ = host.Uptime()

View File

@@ -47,9 +47,7 @@ type SystemAlertStats struct {
NetSent float64 `json:"ns"` NetSent float64 `json:"ns"`
NetRecv float64 `json:"nr"` NetRecv float64 `json:"nr"`
Temperatures map[string]float32 `json:"t"` Temperatures map[string]float32 `json:"t"`
LoadAvg1 float64 `json:"l1"` LoadAvg [3]float64 `json:"la"`
LoadAvg5 float64 `json:"l5"`
LoadAvg15 float64 `json:"l15"`
} }
type SystemAlertData struct { type SystemAlertData struct {

View File

@@ -55,13 +55,13 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
val = data.Info.DashboardTemp val = data.Info.DashboardTemp
unit = "°C" unit = "°C"
case "LoadAvg1": case "LoadAvg1":
val = data.Info.LoadAvg1 val = data.Info.LoadAvg[0]
unit = "" unit = ""
case "LoadAvg5": case "LoadAvg5":
val = data.Info.LoadAvg5 val = data.Info.LoadAvg[1]
unit = "" unit = ""
case "LoadAvg15": case "LoadAvg15":
val = data.Info.LoadAvg15 val = data.Info.LoadAvg[2]
unit = "" unit = ""
} }
@@ -200,11 +200,11 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
alert.mapSums[key] += temp alert.mapSums[key] += temp
} }
case "LoadAvg1": case "LoadAvg1":
alert.val += stats.LoadAvg1 alert.val += stats.LoadAvg[0]
case "LoadAvg5": case "LoadAvg5":
alert.val += stats.LoadAvg5 alert.val += stats.LoadAvg[1]
case "LoadAvg15": case "LoadAvg15":
alert.val += stats.LoadAvg15 alert.val += stats.LoadAvg[2]
default: default:
continue continue
} }

View File

@@ -31,11 +31,13 @@ type Stats struct {
Temperatures map[string]float64 `json:"t,omitempty" cbor:"20,keyasint,omitempty"` Temperatures map[string]float64 `json:"t,omitempty" cbor:"20,keyasint,omitempty"`
ExtraFs map[string]*FsStats `json:"efs,omitempty" cbor:"21,keyasint,omitempty"` ExtraFs map[string]*FsStats `json:"efs,omitempty" cbor:"21,keyasint,omitempty"`
GPUData map[string]GPUData `json:"g,omitempty" cbor:"22,keyasint,omitempty"` GPUData map[string]GPUData `json:"g,omitempty" cbor:"22,keyasint,omitempty"`
LoadAvg1 float64 `json:"l1,omitempty" cbor:"23,keyasint,omitempty,omitzero"` LoadAvg1 float64 `json:"l1,omitempty" cbor:"23,keyasint,omitempty"`
LoadAvg5 float64 `json:"l5,omitempty" cbor:"24,keyasint,omitempty,omitzero"` LoadAvg5 float64 `json:"l5,omitempty" cbor:"24,keyasint,omitempty"`
LoadAvg15 float64 `json:"l15,omitempty" cbor:"25,keyasint,omitempty,omitzero"` LoadAvg15 float64 `json:"l15,omitempty" cbor:"25,keyasint,omitempty"`
Bandwidth [2]uint64 `json:"b,omitzero" cbor:"26,keyasint,omitzero"` // [sent bytes, recv bytes] Bandwidth [2]uint64 `json:"b,omitzero" cbor:"26,keyasint,omitzero"` // [sent bytes, recv bytes]
MaxBandwidth [2]uint64 `json:"bm,omitzero" cbor:"27,keyasint,omitzero"` // [sent bytes, recv bytes] MaxBandwidth [2]uint64 `json:"bm,omitzero" cbor:"27,keyasint,omitzero"` // [sent bytes, recv bytes]
LoadAvg [3]float64 `json:"la,omitempty" cbor:"28,keyasint"`
// TODO: remove other load fields in future release in favor of load avg array
} }
type GPUData struct { type GPUData struct {
@@ -98,6 +100,8 @@ type Info struct {
LoadAvg5 float64 `json:"l5,omitempty" cbor:"16,keyasint,omitempty"` LoadAvg5 float64 `json:"l5,omitempty" cbor:"16,keyasint,omitempty"`
LoadAvg15 float64 `json:"l15,omitempty" cbor:"17,keyasint,omitempty"` LoadAvg15 float64 `json:"l15,omitempty" cbor:"17,keyasint,omitempty"`
BandwidthBytes uint64 `json:"bb" cbor:"18,keyasint"` BandwidthBytes uint64 `json:"bb" cbor:"18,keyasint"`
LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"`
// TODO: remove load fields in future release in favor of load avg array
} }
// Final data structure to return to the hub // Final data structure to return to the hub

View File

@@ -203,9 +203,9 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
sum.DiskWritePs += stats.DiskWritePs sum.DiskWritePs += stats.DiskWritePs
sum.NetworkSent += stats.NetworkSent sum.NetworkSent += stats.NetworkSent
sum.NetworkRecv += stats.NetworkRecv sum.NetworkRecv += stats.NetworkRecv
sum.LoadAvg1 += stats.LoadAvg1 sum.LoadAvg[0] += stats.LoadAvg[0]
sum.LoadAvg5 += stats.LoadAvg5 sum.LoadAvg[1] += stats.LoadAvg[1]
sum.LoadAvg15 += stats.LoadAvg15 sum.LoadAvg[2] += stats.LoadAvg[2]
sum.Bandwidth[0] += stats.Bandwidth[0] sum.Bandwidth[0] += stats.Bandwidth[0]
sum.Bandwidth[1] += stats.Bandwidth[1] sum.Bandwidth[1] += stats.Bandwidth[1]
// Set peak values // Set peak values
@@ -285,9 +285,9 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
sum.DiskWritePs = twoDecimals(sum.DiskWritePs / count) sum.DiskWritePs = twoDecimals(sum.DiskWritePs / count)
sum.NetworkSent = twoDecimals(sum.NetworkSent / count) sum.NetworkSent = twoDecimals(sum.NetworkSent / count)
sum.NetworkRecv = twoDecimals(sum.NetworkRecv / count) sum.NetworkRecv = twoDecimals(sum.NetworkRecv / count)
sum.LoadAvg1 = twoDecimals(sum.LoadAvg1 / count) sum.LoadAvg[0] = twoDecimals(sum.LoadAvg[0] / count)
sum.LoadAvg5 = twoDecimals(sum.LoadAvg5 / count) sum.LoadAvg[1] = twoDecimals(sum.LoadAvg[1] / count)
sum.LoadAvg15 = twoDecimals(sum.LoadAvg15 / count) sum.LoadAvg[2] = twoDecimals(sum.LoadAvg[2] / count)
sum.Bandwidth[0] = sum.Bandwidth[0] / uint64(count) sum.Bandwidth[0] = sum.Bandwidth[0] / uint64(count)
sum.Bandwidth[1] = sum.Bandwidth[1] / uint64(count) sum.Bandwidth[1] = sum.Bandwidth[1] / uint64(count)
// Average temperatures // Average temperatures

View File

@@ -9,31 +9,30 @@ import {
xAxis, xAxis,
} from "@/components/ui/chart" } from "@/components/ui/chart"
import { useYAxisWidth, cn, formatShortDate, toFixedFloat, decimalString, chartMargin } from "@/lib/utils" import { useYAxisWidth, cn, formatShortDate, toFixedFloat, decimalString, chartMargin } from "@/lib/utils"
import { ChartData } from "@/types" import { ChartData, SystemStats } from "@/types"
import { memo } from "react" import { memo } from "react"
import { t } from "@lingui/core/macro" import { t } from "@lingui/core/macro"
export default memo(function LoadAverageChart({ chartData }: { chartData: ChartData }) { export default memo(function LoadAverageChart({ chartData }: { chartData: ChartData }) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth() const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
if (chartData.systemStats.length === 0) { const keys: { legacy: keyof SystemStats; color: string; label: string }[] = [
return null {
} legacy: "l1",
const keys = {
l1: {
color: "hsl(271, 81%, 60%)", // Purple color: "hsl(271, 81%, 60%)", // Purple
label: t({ message: `1 min`, comment: "Load average" }), label: t({ message: `1 min`, comment: "Load average" }),
}, },
l5: { {
legacy: "l5",
color: "hsl(217, 91%, 60%)", // Blue color: "hsl(217, 91%, 60%)", // Blue
label: t({ message: `5 min`, comment: "Load average" }), label: t({ message: `5 min`, comment: "Load average" }),
}, },
l15: { {
legacy: "l15",
color: "hsl(25, 95%, 53%)", // Orange color: "hsl(25, 95%, 53%)", // Orange
label: t({ message: `15 min`, comment: "Load average" }), label: t({ message: `15 min`, comment: "Load average" }),
}, },
} ]
return ( return (
<div> <div>
@@ -69,16 +68,22 @@ export default memo(function LoadAverageChart({ chartData }: { chartData: ChartD
/> />
} }
/> />
{Object.entries(keys).map(([key, value]: [string, { color: string; label: string }]) => { {keys.map(({ legacy, color, label }, i) => {
const dataKey = (value: { stats: SystemStats }) => {
if (chartData.agentVersion.patch < 1) {
return value.stats?.[legacy]
}
return value.stats?.la?.[i] ?? value.stats?.[legacy]
}
return ( return (
<Line <Line
key={key} key={i}
dataKey={`stats.${key}`} dataKey={dataKey}
name={value.label} name={label}
type="monotoneX" type="monotoneX"
dot={false} dot={false}
strokeWidth={1.5} strokeWidth={1.5}
stroke={value.color} stroke={color}
isAnimationActive={false} isAnimationActive={false}
/> />
) )

View File

@@ -26,6 +26,7 @@ import {
getHostDisplayValue, getHostDisplayValue,
getPbTimestamp, getPbTimestamp,
listen, listen,
parseSemVer,
toFixedFloat, toFixedFloat,
useLocalStorage, useLocalStorage,
} from "@/lib/utils" } from "@/lib/utils"
@@ -191,6 +192,7 @@ export default function SystemDetail({ name }: { name: string }) {
chartTime, chartTime,
orientation: direction === "rtl" ? "right" : "left", orientation: direction === "rtl" ? "right" : "left",
...getTimeData(chartTime, lastCreated), ...getTimeData(chartTime, lastCreated),
agentVersion: parseSemVer(system?.info?.v),
} }
}, [systemStats, containerData, direction]) }, [systemStats, containerData, direction])
@@ -642,7 +644,7 @@ export default function SystemDetail({ name }: { name: string }) {
)} )}
{/* Load Average chart */} {/* Load Average chart */}
{system.info.l1 !== undefined && ( {chartData.agentVersion?.minor >= 12 && (
<ChartCard <ChartCard
empty={dataEmpty} empty={dataEmpty}
grid={grid} grid={grid}

View File

@@ -73,6 +73,7 @@ import {
formatTemperature, formatTemperature,
decimalString, decimalString,
formatBytes, formatBytes,
parseSemVer,
} from "@/lib/utils" } from "@/lib/utils"
import AlertsButton from "../alerts/alert-button" import AlertsButton from "../alerts/alert-button"
import { $router, Link, navigate } from "../router" import { $router, Link, navigate } from "../router"
@@ -224,8 +225,12 @@ export default function SystemsTable() {
{ {
id: "loadAverage", id: "loadAverage",
accessorFn: ({ info }) => { accessorFn: ({ info }) => {
const { l1 = 0, l5 = 0, l15 = 0 } = info const sum = info.la?.reduce((acc, curr) => acc + curr, 0)
return l1 + l5 + l15 // TODO: remove this in future release in favor of la array
if (!sum) {
return (info.l1 ?? 0) + (info.l5 ?? 0) + (info.l15 ?? 0)
}
return sum
}, },
name: () => t({ message: "Load Avg", comment: "Short label for load average" }), name: () => t({ message: "Load Avg", comment: "Short label for load average" }),
size: 0, size: 0,
@@ -233,16 +238,22 @@ export default function SystemsTable() {
header: sortableHeader, header: sortableHeader,
cell(info: CellContext<SystemRecord, unknown>) { cell(info: CellContext<SystemRecord, unknown>) {
const { info: sysInfo, status } = info.row.original const { info: sysInfo, status } = info.row.original
if (sysInfo.l1 === undefined) { // agent version
const { minor, patch } = parseSemVer(sysInfo.v)
let loadAverages = sysInfo.la
// use legacy load averages if agent version is less than 12.1.0
if (!loadAverages || (minor === 12 && patch < 1)) {
loadAverages = [sysInfo.l1 ?? 0, sysInfo.l5 ?? 0, sysInfo.l15 ?? 0]
}
const max = Math.max(...loadAverages)
if (max === 0 && (status === "paused" || minor < 12)) {
return null return null
} }
const { l1 = 0, l5 = 0, l15 = 0, t: cpuThreads = 1 } = sysInfo
const loadAverages = [l1, l5, l15]
function getDotColor() { function getDotColor() {
const max = Math.max(...loadAverages) const normalized = max / (sysInfo.t ?? 1)
const normalized = max / cpuThreads
if (status !== "up") return "bg-primary/30" if (status !== "up") return "bg-primary/30"
if (normalized < 0.7) return "bg-green-500" if (normalized < 0.7) return "bg-green-500"
if (normalized < 1) return "bg-yellow-500" if (normalized < 1) return "bg-yellow-500"
@@ -252,7 +263,7 @@ export default function SystemsTable() {
return ( return (
<div className="flex items-center gap-[.35em] w-full tabular-nums tracking-tight"> <div className="flex items-center gap-[.35em] w-full tabular-nums tracking-tight">
<span className={cn("inline-block size-2 rounded-full me-0.5", getDotColor())} /> <span className={cn("inline-block size-2 rounded-full me-0.5", getDotColor())} />
{loadAverages.map((la, i) => ( {loadAverages?.map((la, i) => (
<span key={i}>{decimalString(la, la >= 10 ? 1 : 2)}</span> <span key={i}>{decimalString(la, la >= 10 ? 1 : 2)}</span>
))} ))}
</div> </div>

View File

@@ -9,6 +9,7 @@ import {
ChartTimeData, ChartTimeData,
ChartTimes, ChartTimes,
FingerprintRecord, FingerprintRecord,
SemVer,
SystemRecord, SystemRecord,
UserSettings, UserSettings,
} from "@/types" } from "@/types"
@@ -495,3 +496,14 @@ export function formatDuration(
.filter(Boolean) .filter(Boolean)
.join(" ") .join(" ")
} }
export const parseSemVer = (semVer = ""): SemVer => {
// if (semVer.startsWith("v")) {
// semVer = semVer.slice(1)
// }
if (semVer.includes("-")) {
semVer = semVer.slice(0, semVer.indexOf("-"))
}
const parts = semVer.split(".").map(Number)
return { major: parts?.[0] ?? 0, minor: parts?.[1] ?? 0, patch: parts?.[2] ?? 0 }
}

View File

@@ -50,6 +50,8 @@ export interface SystemInfo {
l5?: number l5?: number
/** load average 15 minutes */ /** load average 15 minutes */
l15?: number l15?: number
/** load average */
la?: [number, number, number]
/** operating system */ /** operating system */
o?: string o?: string
/** uptime */ /** uptime */
@@ -79,12 +81,15 @@ export interface SystemStats {
cpu: number cpu: number
/** peak cpu */ /** peak cpu */
cpum?: number cpum?: number
// TODO: remove these in future release in favor of la
/** load average 1 minute */ /** load average 1 minute */
l1?: number l1?: number
/** load average 5 minutes */ /** load average 5 minutes */
l5?: number l5?: number
/** load average 15 minutes */ /** load average 15 minutes */
l15?: number l15?: number
/** load average */
la?: [number, number, number]
/** total memory (gb) */ /** total memory (gb) */
m: number m: number
/** memory used (gb) */ /** memory used (gb) */
@@ -234,7 +239,14 @@ type ChartDataContainer = {
[key: string]: key extends "created" ? never : ContainerStats [key: string]: key extends "created" ? never : ContainerStats
} }
export interface SemVer {
major: number
minor: number
patch: number
}
export interface ChartData { export interface ChartData {
agentVersion: SemVer
systemStats: SystemStatsRecord[] systemStats: SystemStatsRecord[]
containerData: ChartDataContainer[] containerData: ChartDataContainer[]
orientation: "right" | "left" orientation: "right" | "left"