CPU reductions (same charts/features)
CI / test (push) Successful in 1m11s
Release rolling / release (push) Successful in 7m35s

Cache cpuidle names (was re-reading name sysfs every tick × cores)
Cache CPU sysfs dir list + register cpufreq/throttle once
Map for per-core prev lookups (was O(n²) .find)
Dedupe meminfo/vmstat once per tick
Cache iface speed/duplex/mtu/qlen (30s)
Cache inotify limits (30s)
Gate registerChart for IRQs, cpuidle, peardock mapped charts
Remove leftover profile() stderr timers
Cache os.cpus().length in docker collector
Grow fd-cache buffer past 64KB for /proc/interrupts etc.
This commit is contained in:
Raven Scott
2026-07-22 13:40:39 -04:00
parent 9dd7e44380
commit af6609c2d3
9 changed files with 243 additions and 148 deletions
+48 -25
View File
@@ -42,6 +42,7 @@ import {
collectNetDeep,
parsePressureFull,
ifaceSpeedKbps,
ifaceLinkMeta,
} from './collectors/host-deep.js'
import { collectHostMore } from './collectors/host-more.js'
import { readFileCached, closeFdCache } from '../utils/fd-cache.js'
@@ -461,9 +462,11 @@ export class MetricsCollector extends EventEmitter {
const batch = []
if (IS_LINUX) {
const memSnap = parseMeminfo()
const vmSnap = parseVmstat()
this._safeCollect('cpu', () => this._collectCpuAndProcs(batch, ts, dtSec))
this._safeCollect('cpu_sysfs', () => collectCpuSysfs(batch, ts, dtSec, this.deepState))
this._safeCollect('memory', () => this._collectMemory(batch, ts, dtSec))
this._safeCollect('memory', () => this._collectMemory(batch, ts, dtSec, memSnap, vmSnap))
this._safeCollect('disk', () => this._collectDisk(batch, ts, dtSec))
this._safeCollect('net', () => this._collectNet(batch, ts, dtSec))
this._safeCollect('ip', () => this._collectIp(batch, ts, dtSec))
@@ -471,8 +474,8 @@ export class MetricsCollector extends EventEmitter {
this._safeCollect('diskspace', () => this._collectDiskSpace(batch, ts))
this._safeCollect('host_more', () => {
collectHostMore(batch, ts, dtSec, this.deepState, {
mem: parseMeminfo(),
vm: parseVmstat(),
mem: memSnap,
vm: vmSnap,
prevVm: prevVmSnapshot,
})
})
@@ -548,16 +551,21 @@ export class MetricsCollector extends EventEmitter {
const cpus = os.cpus()
const load = os.loadavg()
const agg = { user: 0, nice: 0, system: 0, idle: 0, irq: 0, iowait: 0, softirq: 0, steal: 0, guest: 0, guest_nice: 0 }
const prevById = this._lastStatCoresById || new Map()
/** @type {Map<string, object>} */
const nextById = new Map()
for (let i = 0; i < cpus.length; i++) {
const c = cpus[i]
const cur = { id: String(i), user: c.times.user, nice: c.times.nice, system: c.times.sys, idle: c.times.idle, irq: c.times.irq, iowait: 0, softirq: 0, steal: 0, guest: 0, guest_nice: 0 }
if (!this._chartsRegistered) registerChart(makeCpuCoreChart(i))
const prev = this.lastStat?.cores?.find((x) => x.id === String(i))
const prev = prevById.get(String(i))
batch.push({ chart: `cpu.cpu${i}`, context: 'cpu.cpu', ts, values: cpuDeltaPct(prev, cur) })
for (const k of Object.keys(agg)) agg[k] += cur[k] || 0
nextById.set(String(i), cur)
}
batch.push({ chart: 'system.cpu', context: 'system.cpu', ts, values: cpuDeltaPct(this.lastStat?.aggregate, agg) })
this.lastStat = { aggregate: agg, cores: cpus.map((c, i) => ({ id: String(i), ...c.times, system: c.times.sys })), intr: 0, ctxt: 0, processes: 0, procsRunning: 0, procsBlocked: 0 }
this.lastStat = { aggregate: agg, cores: [...nextById.values()], intr: 0, ctxt: 0, processes: 0, procsRunning: 0, procsBlocked: 0 }
this._lastStatCoresById = nextById
batch.push({ chart: 'system.processes', context: 'system.processes', ts, values: { running: load[0] < 0 ? 0 : Math.round(load[0]), blocked: 0 } })
batch.push({ chart: 'system.active_processes', context: 'system.active_processes', ts, values: { active: 0 } })
batch.push({ chart: 'system.load', context: 'system.load', ts, values: { load1: load[0], load5: load[1], load15: load[2] } })
@@ -578,15 +586,19 @@ export class MetricsCollector extends EventEmitter {
const cpuVals = cpuDeltaPct(this.lastStat?.aggregate, stat.aggregate)
batch.push({ chart: 'system.cpu', context: 'system.cpu', ts, values: cpuVals })
const prevById = this._lastStatCoresById || new Map()
/** @type {Map<string, object>} */
const nextById = new Map()
for (const core of stat.cores) {
if (!this._chartsRegistered) registerChart(makeCpuCoreChart(core.id))
const prev = this.lastStat?.cores?.find((c) => c.id === core.id)
const prev = prevById.get(core.id)
batch.push({
chart: `cpu.cpu${core.id}`,
context: 'cpu.cpu',
ts,
values: cpuDeltaPct(prev, core),
})
nextById.set(core.id, core)
}
const prev = this.lastStat
@@ -621,10 +633,14 @@ export class MetricsCollector extends EventEmitter {
values: { active: load.total || stat.procsRunning + stat.procsBlocked },
})
this.lastStat = stat
this._lastStatCoresById = nextById
} else {
// Node os fallback
const cpus = os.cpus()
const agg = { user: 0, nice: 0, system: 0, idle: 0, irq: 0, iowait: 0, softirq: 0, steal: 0, guest: 0, guest_nice: 0 }
const prevById = this._lastStatCoresById || new Map()
/** @type {Map<string, object>} */
const nextById = new Map()
cpus.forEach((c, i) => {
const cur = {
id: String(i),
@@ -639,8 +655,8 @@ export class MetricsCollector extends EventEmitter {
guest: 0,
guest_nice: 0,
}
registerChart(makeCpuCoreChart(i))
const prevCore = this.lastStat?.cores?.find((x) => x.id === String(i))
if (!this._chartsRegistered) registerChart(makeCpuCoreChart(i))
const prevCore = prevById.get(String(i))
batch.push({
chart: `cpu.cpu${i}`,
context: 'cpu.cpu',
@@ -648,6 +664,7 @@ export class MetricsCollector extends EventEmitter {
values: cpuDeltaPct(prevCore, cur),
})
for (const k of Object.keys(agg)) agg[k] += cur[k] || 0
nextById.set(String(i), cur)
})
batch.push({
chart: 'system.cpu',
@@ -655,7 +672,8 @@ export class MetricsCollector extends EventEmitter {
ts,
values: cpuDeltaPct(this.lastStat?.aggregate, agg),
})
this.lastStat = { aggregate: agg, cores: cpus.map((c, i) => ({ id: String(i), ...c.times, system: c.times.sys, iowait: 0, softirq: 0, steal: 0, guest: 0, guest_nice: 0 })), intr: 0, ctxt: 0, processes: 0, procsRunning: 0, procsBlocked: 0 }
this.lastStat = { aggregate: agg, cores: [...nextById.values()], intr: 0, ctxt: 0, processes: 0, procsRunning: 0, procsBlocked: 0 }
this._lastStatCoresById = nextById
batch.push({
chart: 'system.processes',
context: 'system.processes',
@@ -678,16 +696,20 @@ export class MetricsCollector extends EventEmitter {
})
}
_collectMemory(batch, ts, dtSec) {
const mem = parseMeminfo()
const vm = parseVmstat()
_collectMemory(batch, ts, dtSec, memArg, vmArg) {
const mem = memArg || parseMeminfo()
const vm = vmArg || parseVmstat()
const total = os.totalmem()
const freeOs = os.freemem()
if (mem) {
const freeB = mem.MemFree || freeOs
const buffers = mem.Buffers || 0
const cached = (mem.Cached || 0) + (mem.SReclaimable || 0)
// Shmem (tmpfs etc.) is already counted in Cached — subtract so used isn't understated
const cached = Math.max(
0,
(mem.Cached || 0) + (mem.SReclaimable || 0) - (mem.Shmem || 0)
)
const used = Math.max(0, (mem.MemTotal || total) - freeB - buffers - cached)
batch.push({
chart: 'system.ram',
@@ -829,7 +851,8 @@ export class MetricsCollector extends EventEmitter {
for (const [name, d] of Object.entries(disks)) {
if (name === '__agg') continue
if (!this._chartsRegistered) {
const registeredDisks = this._registeredDisks || (this._registeredDisks = new Set())
if (!registeredDisks.has(name)) {
registerChart(makeDiskIoChart(name))
registerChart(makeDiskOpsChart(name))
registerChart(makeDiskUtilChart(name))
@@ -842,6 +865,7 @@ export class MetricsCollector extends EventEmitter {
registerChart(makeDiskSvctmChart(name))
registerChart(makeDiskMergedChart(name))
registerChart(makeDiskFlushChart(name))
registeredDisks.add(name)
}
const prev = prevAll[name]
const readBytes = d.readSectors * 512
@@ -978,7 +1002,8 @@ export class MetricsCollector extends EventEmitter {
for (const [name, n] of Object.entries(ifaces)) {
if (name === '__agg') continue
if (!this._chartsRegistered) {
const registeredIfaces = this._registeredIfaces || (this._registeredIfaces = new Set())
if (!registeredIfaces.has(name)) {
registerChart(makeNetChart(name))
registerChart(makeNetPacketsChart(name))
registerChart(makeNetErrorsChart(name))
@@ -987,6 +1012,7 @@ export class MetricsCollector extends EventEmitter {
registerChart(makeNetDuplexChart(name))
registerChart(makeNetMtuChart(name))
registerChart(makeNetQueueChart(name))
registeredIfaces.add(name)
}
const prev = prevAll[name]
rx += n.rxBytes
@@ -1028,7 +1054,7 @@ export class MetricsCollector extends EventEmitter {
outbound: prev ? rate(prev.txDrop, n.txDrop, dtSec) : 0,
},
})
const speed = ifaceSpeedKbps(name)
const speed = ifaceSpeedKbps(name, this.deepState)
if (speed != null) {
batch.push({
chart: `net_speed.${name}`,
@@ -1037,31 +1063,28 @@ export class MetricsCollector extends EventEmitter {
values: { speed },
})
}
const duplex = readFile(`/sys/class/net/${name}/duplex`)
if (duplex) {
const d = duplex.trim().toLowerCase()
const link = ifaceLinkMeta(name, this.deepState)
if (link.duplex) {
batch.push({
chart: `net_duplex.${name}`,
context: 'net.duplex',
ts,
values: { full: d === 'full' ? 1 : 0, half: d === 'half' ? 1 : 0 },
values: { full: link.duplex === 'full' ? 1 : 0, half: link.duplex === 'half' ? 1 : 0 },
})
}
const mtu = Number(readFile(`/sys/class/net/${name}/mtu`) || 0)
if (mtu > 0) {
if (link.mtu > 0) {
batch.push({
chart: `net_mtu.${name}`,
context: 'net.mtu',
ts,
values: { mtu },
values: { mtu: link.mtu },
})
}
const qlen = Number(readFile(`/sys/class/net/${name}/tx_queue_len`) || 0)
batch.push({
chart: `net_queue.${name}`,
context: 'net.queue_length',
ts,
values: { tx_queue_len: qlen },
values: { tx_queue_len: link.qlen },
})
}
+5 -1
View File
@@ -67,12 +67,16 @@ function bytesToMiB(n) {
return n / (1024 * 1024)
}
function hostCpus() {
const HOST_CPUS = (() => {
try {
return os.cpus().length || 1
} catch {
return 1
}
})()
function hostCpus() {
return HOST_CPUS
}
/**
+2 -1
View File
@@ -295,7 +295,8 @@ export class EbpfCollector extends EventEmitter {
chart: 'ebpf.vfs',
context: 'ebpf.vfs',
ts,
values: { read_pages: rate('pgpgout'), write_pages: rate('pgpgin') },
// pgpgin = pages read from block devices; pgpgout = pages written out
values: { read_pages: rate('pgpgin'), write_pages: rate('pgpgout') },
},
{
chart: 'ebpf.socket',
+95 -59
View File
@@ -115,11 +115,16 @@ export function readKsm() {
export function collectCpuSysfs(batch, ts, dtSec, state) {
if (os.platform() !== 'linux') return
const cpuRoot = '/sys/devices/system/cpu'
let dirs
try {
dirs = fs.readdirSync(cpuRoot).filter((d) => /^cpu\d+$/.test(d))
} catch {
return
const now = Date.now()
let dirs = state._cpuSysfsDirs
if (!dirs || now - (state._cpuSysfsDirsTs || 0) > 30000) {
try {
dirs = fs.readdirSync(cpuRoot).filter((d) => /^cpu\d+$/.test(d))
} catch {
return
}
state._cpuSysfsDirs = dirs
state._cpuSysfsDirsTs = now
}
/** @type {Record<string, number>} */
@@ -157,58 +162,64 @@ export function collectCpuSysfs(batch, ts, dtSec, state) {
}
if (Object.keys(freqs).length) {
registerChart({
id: 'cpu.cpufreq',
name: 'cpu.cpufreq',
context: 'cpu.cpufreq',
title: 'CPU frequency',
units: 'MHz',
family: 'cpufreq',
chartType: 'line',
priority: 180,
plugin: 'proc',
dimensions: Object.keys(freqs).map((id) => ({
id,
name: id,
algorithm: 'absolute',
})),
})
if (!state._cpufreqRegistered) {
registerChart({
id: 'cpu.cpufreq',
name: 'cpu.cpufreq',
context: 'cpu.cpufreq',
title: 'CPU frequency',
units: 'MHz',
family: 'cpufreq',
chartType: 'line',
priority: 180,
plugin: 'proc',
dimensions: Object.keys(freqs).map((id) => ({
id,
name: id,
algorithm: 'absolute',
})),
})
state._cpufreqRegistered = true
}
batch.push({ chart: 'cpu.cpufreq', context: 'cpu.cpufreq', ts, values: freqs })
}
if (Object.keys(coreThrottleRates).length || Object.keys(nextThrottle).length) {
registerChart({
id: 'cpu.core_throttling',
name: 'cpu.core_throttling',
context: 'cpu.core_throttling',
title: 'CPU core thermal throttling',
units: 'events/s',
family: 'throttling',
chartType: 'line',
priority: 185,
plugin: 'proc',
dimensions: Object.keys(nextThrottle).map((id) => ({
id: `cpu${id}`,
name: `cpu${id}`,
algorithm: 'incremental',
})),
})
registerChart({
id: 'cpu.package_throttling',
name: 'cpu.package_throttling',
context: 'cpu.package_throttling',
title: 'CPU package thermal throttling',
units: 'events/s',
family: 'throttling',
chartType: 'line',
priority: 186,
plugin: 'proc',
dimensions: Object.keys(nextThrottle).map((id) => ({
id: `cpu${id}`,
name: `cpu${id}`,
algorithm: 'incremental',
})),
})
if (!state._throttleChartsRegistered) {
registerChart({
id: 'cpu.core_throttling',
name: 'cpu.core_throttling',
context: 'cpu.core_throttling',
title: 'CPU core thermal throttling',
units: 'events/s',
family: 'throttling',
chartType: 'line',
priority: 185,
plugin: 'proc',
dimensions: Object.keys(nextThrottle).map((id) => ({
id: `cpu${id}`,
name: `cpu${id}`,
algorithm: 'incremental',
})),
})
registerChart({
id: 'cpu.package_throttling',
name: 'cpu.package_throttling',
context: 'cpu.package_throttling',
title: 'CPU package thermal throttling',
units: 'events/s',
family: 'throttling',
chartType: 'line',
priority: 186,
plugin: 'proc',
dimensions: Object.keys(nextThrottle).map((id) => ({
id: `cpu${id}`,
name: `cpu${id}`,
algorithm: 'incremental',
})),
})
state._throttleChartsRegistered = true
}
if (Object.keys(coreThrottleRates).length) {
batch.push({
chart: 'cpu.core_throttling',
@@ -277,7 +288,8 @@ export function collectMemDeep(batch, ts, dtSec, mem, vm, prevVm) {
}
if (hugeTotal > 0) {
const scale = hugeSizeKiB / 1024 // MiB per page
const used = Math.max(0, hugeTotal - hugeFree - hugeRsvd) * scale
// HugePages_Rsvd ⊆ HugePages_Free — used is simply total free
const used = Math.max(0, hugeTotal - hugeFree) * scale
batch.push({
chart: 'mem.hugepages',
context: 'mem.hugepages',
@@ -548,10 +560,34 @@ export function collectNetDeep(batch, ts, dtSec, state, snmp) {
}
}
export function ifaceSpeedKbps(iface) {
export function ifaceSpeedKbps(iface, state) {
const now = Date.now()
const cache = state?._ifaceMeta || (state && (state._ifaceMeta = {}))
const entry = cache?.[iface]
if (entry && now - entry.ts < 30000 && 'speed' in entry) return entry.speed
const raw = readFile(`/sys/class/net/${iface}/speed`)
if (raw == null) return null
const mbps = Number(raw.trim())
if (!Number.isFinite(mbps) || mbps <= 0) return null
return mbps * 1000
let speed = null
if (raw != null) {
const mbps = Number(raw.trim())
if (Number.isFinite(mbps) && mbps > 0) speed = mbps * 1000
}
if (cache) {
cache[iface] = { ...(entry || {}), ts: now, speed }
}
return speed
}
/** Cached duplex/mtu/qlen — topology rarely changes */
export function ifaceLinkMeta(iface, state) {
const now = Date.now()
const cache = state._ifaceMeta || (state._ifaceMeta = {})
const entry = cache[iface]
if (entry && now - entry.ts < 30000 && entry.duplex != null) {
return { duplex: entry.duplex, mtu: entry.mtu, qlen: entry.qlen }
}
const duplex = (readFile(`/sys/class/net/${iface}/duplex`) || '').trim().toLowerCase() || null
const mtu = Number(readFile(`/sys/class/net/${iface}/mtu`) || 0) || 0
const qlen = Number(readFile(`/sys/class/net/${iface}/tx_queue_len`) || 0) || 0
cache[iface] = { ...(entry || {}), ts: now, duplex, mtu, qlen, speed: entry?.speed }
return { duplex, mtu, qlen }
}
+43 -39
View File
@@ -17,20 +17,6 @@ function readFile(p) {
return readFileCached(p)
}
let _profileIdx = 0
function profile(label, fn) {
const t0 = performance.now()
const r = fn()
const dur = performance.now() - t0
if (dur > 2) {
_profileIdx++
if (_profileIdx <= 20) {
process.stderr.write(`host_more:${label} ${Math.round(dur * 10) / 10}ms\n`)
}
}
return r
}
function rate(prev, cur, dtSec) {
if (dtSec <= 0 || cur < prev) return 0
return (cur - prev) / dtSec
@@ -219,7 +205,7 @@ export function collectCpuidle(batch, ts, dtSec, state) {
} catch {
return
}
/** @type {Record<string, Array<{ id: string, nameFile: string, timeFile: string }>>} */
/** @type {Record<string, Array<{ id: string, name: string, timeFile: string }>>} */
const entries = {}
for (const d of dirs) {
const coreId = d.slice(3)
@@ -232,9 +218,13 @@ export function collectCpuidle(batch, ts, dtSec, state) {
}
const files = []
for (const s of states) {
const nameRaw = (readFile(path.join(idleDir, s, 'name')) || s)
.trim()
.toLowerCase()
.replace(/\s+/g, '_')
files.push({
id: s,
nameFile: path.join(idleDir, s, 'name'),
name: nameRaw || s,
timeFile: path.join(idleDir, s, 'time'),
})
}
@@ -252,8 +242,7 @@ export function collectCpuidle(batch, ts, dtSec, state) {
const files = entries[coreId]
/** @type {Record<string, number>} */
const times = {}
for (const { id, nameFile, timeFile } of files) {
const name = (readFile(nameFile) || id).trim().toLowerCase().replace(/\s+/g, '_')
for (const { id, name, timeFile } of files) {
const t = Number(readFile(timeFile) || 0) // µs
times[name || id] = t
}
@@ -284,7 +273,11 @@ export function collectCpuidle(batch, ts, dtSec, state) {
name: id,
algorithm: 'absolute',
}))
registerChart(def)
const registered = state._registeredCpuidle || (state._registeredCpuidle = new Set())
if (!registered.has(coreId)) {
registerChart(def)
registered.add(coreId)
}
batch.push({
chart: def.id,
context: 'cpuidle.cpu_cstate_residency_time',
@@ -523,7 +516,7 @@ export function collectKsmExtras(batch, ts) {
*/
export function collectHostMore(batch, ts, dtSec, state, { mem, vm, prevVm } = {}) {
// softirqs
const soft = profile('parseSoftirqs', parseSoftirqs)
const soft = parseSoftirqs()
const prevSoft = state.lastSoftirqs
if (Object.keys(soft).length) {
/** @type {Record<string, number>} */
@@ -536,14 +529,18 @@ export function collectHostMore(batch, ts, dtSec, state, { mem, vm, prevVm } = {
}
// IRQs
const irqs = profile('parseInterrupts', parseInterrupts)
const irqs = parseInterrupts()
const prevIrq = state.lastIrqs || {}
/** @type {Record<string, number>} */
const nextIrq = {}
const registeredIrqs = state._registeredIrqs || (state._registeredIrqs = new Set())
for (const { irq, name, count } of irqs) {
nextIrq[irq] = count
const def = makeIrqChart(irq, name)
registerChart(def)
if (!registeredIrqs.has(irq)) {
registerChart(def)
registeredIrqs.add(irq)
}
batch.push({
chart: def.id,
context: 'system.intr_irq',
@@ -556,7 +553,7 @@ export function collectHostMore(batch, ts, dtSec, state, { mem, vm, prevVm } = {
state.lastIrqs = nextIrq
// icmpmsg
const icmpMsg = profile('parseIcmpMsg', parseIcmpMsg)
const icmpMsg = parseIcmpMsg()
const prevMsg = state.lastIcmpMsg
if (Object.keys(icmpMsg).length) {
const pick = (k) => (prevMsg ? rate(prevMsg[k] || 0, icmpMsg[k] || 0, dtSec) : 0)
@@ -579,7 +576,7 @@ export function collectHostMore(batch, ts, dtSec, state, { mem, vm, prevVm } = {
}
// ipv6
const s6 = profile('parseSnmp6', parseSnmp6)
const s6 = parseSnmp6()
const prev6 = state.lastSnmp6
if (Object.keys(s6).length) {
const r = (k) => (prev6 ? rate(prev6[k] || 0, s6[k] || 0, dtSec) : 0)
@@ -621,7 +618,7 @@ export function collectHostMore(batch, ts, dtSec, state, { mem, vm, prevVm } = {
}
// conntrack
const ct = profile('parseConntrack', parseConntrack)
const ct = parseConntrack()
const prevCt = state.lastCt
batch.push({
chart: 'net.conntrack',
@@ -640,30 +637,37 @@ export function collectHostMore(batch, ts, dtSec, state, { mem, vm, prevVm } = {
})
state.lastCt = ct
// inotify limits
// inotify limits — static; refresh every 30s
const now = Date.now()
if (!state._inotifyCache || now - state._inotifyCache.ts > 30000) {
state._inotifyCache = {
ts: now,
values: {
max_user_watches: Number(readFile('/proc/sys/fs/inotify/max_user_watches') || 0),
max_user_instances: Number(readFile('/proc/sys/fs/inotify/max_user_instances') || 0),
},
}
}
batch.push({
chart: 'system.inotify',
context: 'system.inotify',
ts,
values: {
max_user_watches: Number(readFile('/proc/sys/fs/inotify/max_user_watches') || 0),
max_user_instances: Number(readFile('/proc/sys/fs/inotify/max_user_instances') || 0),
},
values: state._inotifyCache.values,
})
batch.push({
chart: 'mem.pagetype_global',
context: 'mem.pagetype_global',
ts,
values: profile('parsePagetypeGlobal', parsePagetypeGlobal),
values: parsePagetypeGlobal(),
})
profile('collectNumaNodes', () => { collectNumaNodes(batch, ts); return true })
profile('collectCpuidle', () => { collectCpuidle(batch, ts, dtSec, state); return true })
profile('collectPowercap', () => { collectPowercap(batch, ts, state); return true })
profile('collectEdac', () => { collectEdac(batch, ts); return true })
profile('collectDebugfsExtras', () => { collectDebugfsExtras(batch, ts); return true })
if (mem) profile('collectMemExtras', () => { collectMemExtras(batch, ts, mem); return true })
profile('collectThpMore', () => { collectThpMore(batch, ts, dtSec, vm, prevVm); return true })
profile('collectKsmExtras', () => { collectKsmExtras(batch, ts); return true })
collectNumaNodes(batch, ts)
collectCpuidle(batch, ts, dtSec, state)
collectPowercap(batch, ts, state)
collectEdac(batch, ts)
collectDebugfsExtras(batch, ts)
if (mem) collectMemExtras(batch, ts, mem)
collectThpMore(batch, ts, dtSec, vm, prevVm)
collectKsmExtras(batch, ts)
}
+5
View File
@@ -21,6 +21,9 @@ import logger from '../../utils/logger.js'
const log = logger.child('peardock')
/** @type {Set<string>} */
const _mappedCharts = new Set()
export function isPearDockEnabled() {
const v = process.env.PEARDATA_PEARDOCK
return v === '1' || v === 'on' || v === 'true'
@@ -102,6 +105,7 @@ export function extractDockCharts(metrics) {
* @param {{ title?: string, family?: string }} [meta]
*/
function registerMappedChart(mappedId, context, values, meta = {}) {
if (_mappedCharts.has(mappedId)) return
const dims = Object.keys(values).map((id) => ({
id,
name: id,
@@ -134,6 +138,7 @@ function registerMappedChart(mappedId, context, values, meta = {}) {
plugin: 'peardock',
dimensions: dims,
})
_mappedCharts.add(mappedId)
}
export class PearDockCollector extends EventEmitter {
+37 -20
View File
@@ -56,7 +56,7 @@ const PROC_EXTRA_TICK = 5
let procTickCount = 0
/**
* @returns {Array<{ pid: number, name: string, utime: number, stime: number, rssPages: number, threads: number, readBytes: number|null, writeBytes: number|null }>|null}
* @returns {Array<{ pid: number, name: string, utime: number, stime: number, rssPages: number, threads: number, readBytes: number|null, writeBytes: number|null, ioSampleTs: number|null }>|null}
*/
export function listProcStats() {
if (os.platform() !== 'linux') return null
@@ -69,8 +69,9 @@ export function listProcStats() {
procTickCount = (procTickCount + 1) % PROC_EXTRA_TICK
const readExtra = procTickCount === 0
if (readExtra) PROC_EXTRA_CACHE.clear()
const now = Date.now()
/** @type {Array<{ pid: number, name: string, utime: number, stime: number, rssPages: number, threads: number, readBytes: number|null, writeBytes: number|null }>} */
/** @type {Array<{ pid: number, name: string, utime: number, stime: number, rssPages: number, threads: number, readBytes: number|null, writeBytes: number|null, ioSampleTs: number|null }>} */
const out = []
for (const ent of dirs) {
if (!/^\d+$/.test(ent)) continue
@@ -90,6 +91,7 @@ export function listProcStats() {
let threads = 0
let readBytes = null
let writeBytes = null
let ioSampleTs = null
if (readExtra) {
const status = readFileBuf(path.join('/proc', ent, 'status'))
@@ -103,16 +105,17 @@ export function listProcStats() {
const wb = ioRaw.match(/^write_bytes:\s*(\d+)/m)
if (rb) readBytes = Number(rb[1]) || 0
if (wb) writeBytes = Number(wb[1]) || 0
if (readBytes != null || writeBytes != null) ioSampleTs = now
}
PROC_EXTRA_CACHE.set(pid, { threads, readBytes, writeBytes, ioSampleTs })
} else {
const cached = PROC_EXTRA_CACHE.get(pid)
if (cached) {
threads = cached.threads
readBytes = cached.readBytes
writeBytes = cached.writeBytes
ioSampleTs = cached.ioSampleTs
}
}
const cached = PROC_EXTRA_CACHE.get(pid)
if (cached) {
if (threads === 0) threads = cached.threads
if (readBytes === null) readBytes = cached.readBytes
if (writeBytes === null) writeBytes = cached.writeBytes
}
if (readExtra || !cached) {
PROC_EXTRA_CACHE.set(pid, { threads, readBytes, writeBytes })
}
out.push({
@@ -124,6 +127,7 @@ export function listProcStats() {
threads,
readBytes,
writeBytes,
ioSampleTs,
})
}
return out
@@ -266,36 +270,49 @@ export class ProcessCollector extends EventEmitter {
const key = p.name
cpuByName.set(key, (cpuByName.get(key) || 0) + Math.min(100 * ncpu, pct))
// IO is sampled every PROC_EXTRA_TICK ticks — rate over the actual sample gap
if (
p.ioSampleTs != null &&
prev.ioSampleTs != null &&
p.ioSampleTs > prev.ioSampleTs &&
p.readBytes != null &&
p.writeBytes != null &&
prev.readBytes != null &&
prev.writeBytes != null
) {
const dBytes =
Math.max(0, p.readBytes - prev.readBytes) +
Math.max(0, p.writeBytes - prev.writeBytes)
const rateKib = dBytes / dSec / 1024
ioByName.set(key, (ioByName.get(key) || 0) + rateKib)
const ioSec = (p.ioSampleTs - prev.ioSampleTs) / 1000
if (ioSec > 0) {
const dBytes =
Math.max(0, p.readBytes - prev.readBytes) +
Math.max(0, p.writeBytes - prev.writeBytes)
const rateKib = dBytes / ioSec / 1024
ioByName.set(key, (ioByName.get(key) || 0) + rateKib)
}
}
}
}
/** @type {Map<number, { ticks: number, wallMs: number, name: string, readBytes: number, writeBytes: number }>} */
// Hold last IO rates between sparse /proc/pid/io samples so the chart stays continuous
if (ioByName.size) this._lastIoByName = ioByName
const ioRates = ioByName.size ? ioByName : this._lastIoByName || ioByName
/** @type {Map<number, { ticks: number, wallMs: number, name: string, readBytes: number|null, writeBytes: number|null, ioSampleTs: number|null }>} */
const next = new Map()
for (const p of procs) {
const prev = this._prev?.get(p.pid)
next.set(p.pid, {
ticks: p.utime + p.stime,
wallMs,
name: p.name,
readBytes: p.readBytes ?? 0,
writeBytes: p.writeBytes ?? 0,
readBytes: p.readBytes,
writeBytes: p.writeBytes,
ioSampleTs: p.ioSampleTs ?? prev?.ioSampleTs ?? null,
})
}
this._prev = next
const cpuRanked = [...cpuByName.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit)
const ioRanked = [...ioByName.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit)
const ioRanked = [...ioRates.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit)
const rssRanked = [...procs]
.sort((a, b) => b.rssPages - a.rssPages)
.slice(0, limit)
+4 -3
View File
@@ -6,7 +6,7 @@ import fs from 'fs'
import os from 'os'
import { EventEmitter } from 'events'
import { SAMPLE_INTERVAL_MS, registerChart } from '../../../shared/metrics.js'
import { readFileBuf } from '../../utils/fd-cache.js'
import { readFileCached } from '../../utils/fd-cache.js'
import logger from '../../utils/logger.js'
const log = logger.child('sockets')
@@ -43,7 +43,8 @@ function countTcp(file) {
syn_recv: 0,
other: 0,
}
const raw = readFileBuf(file)
// /proc/net/tcp can be large — use growing fd-cache, not the 8KB small buf
const raw = readFileCached(file)
if (!raw) return counts
for (const line of raw.split('\n').slice(1)) {
const parts = line.trim().split(/\s+/)
@@ -57,7 +58,7 @@ function countTcp(file) {
}
function countUdp(file) {
const raw = readFileBuf(file)
const raw = readFileCached(file)
if (!raw) return 0
return Math.max(0, raw.trim().split('\n').length - 1)
}