CPU reductions (same charts/features)
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:
@@ -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 },
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user