diff --git a/server/services/collectors/bcache.js b/server/services/collectors/bcache.js index 2c6d99f..866554d 100644 --- a/server/services/collectors/bcache.js +++ b/server/services/collectors/bcache.js @@ -6,6 +6,7 @@ import fs from 'fs' import path from 'path' import { CollectorPlugin } from './plugin.js' import { registerChart } from '../../../shared/metrics.js' +import { readFileBuf } from '../../utils/fd-cache.js' export function isBcacheEnabled() { const v = process.env.PEARDATA_BCACHE @@ -15,11 +16,8 @@ export function isBcacheEnabled() { } function readNum(p) { - try { - return Number(fs.readFileSync(p, 'utf8').trim()) || 0 - } catch { - return 0 - } + const raw = readFileBuf(p) + return raw ? Number(raw.trim()) || 0 : 0 } export class BcacheCollector extends CollectorPlugin { diff --git a/server/services/collectors/cgroups.js b/server/services/collectors/cgroups.js index 52290d4..02bbf31 100644 --- a/server/services/collectors/cgroups.js +++ b/server/services/collectors/cgroups.js @@ -20,6 +20,7 @@ import { } from '../../../shared/metrics.js' import { resolveContainerLabel } from '../../../shared/container-names.js' import { loadContainerNameMap, resolveDockerSocket } from './docker.js' +import { readFileBuf } from '../../utils/fd-cache.js' import logger from '../../utils/logger.js' const log = logger.child('cgroups') @@ -37,11 +38,7 @@ function maxCgroups() { } function readFile(p) { - try { - return fs.readFileSync(p, 'utf8') - } catch { - return null - } + return readFileBuf(p) } function cgroupRoot() { diff --git a/server/services/collectors/dmcache.js b/server/services/collectors/dmcache.js index 2a629fc..d1e689f 100644 --- a/server/services/collectors/dmcache.js +++ b/server/services/collectors/dmcache.js @@ -10,6 +10,7 @@ import fs from 'fs' import path from 'path' import { CollectorPlugin } from './plugin.js' import { registerChart } from '../../../shared/metrics.js' +import { readFileBuf } from '../../utils/fd-cache.js' import { execFile } from '../../utils/exec.js' import logger from '../../utils/logger.js' @@ -39,11 +40,7 @@ export function isDmcacheEnabled() { } function readFile(p) { - try { - return fs.readFileSync(p, 'utf8') - } catch { - return null - } + return readFileBuf(p) } /** diff --git a/server/services/collectors/docker.js b/server/services/collectors/docker.js index eb3292d..d762d8c 100644 --- a/server/services/collectors/docker.js +++ b/server/services/collectors/docker.js @@ -16,6 +16,7 @@ import net from 'net' import os from 'os' import { EventEmitter } from 'events' import { execFile } from '../../utils/exec.js' +import { readFileBuf } from '../../utils/fd-cache.js' import { SAMPLE_INTERVAL_MS, registerChart, @@ -59,11 +60,7 @@ export function resolveDockerSocket(preferred) { } function readFile(p) { - try { - return fs.readFileSync(p, 'utf8') - } catch { - return null - } + return readFileBuf(p) } function bytesToMiB(n) { diff --git a/server/services/collectors/ebpf.js b/server/services/collectors/ebpf.js index dfdb602..c7998b4 100644 --- a/server/services/collectors/ebpf.js +++ b/server/services/collectors/ebpf.js @@ -16,6 +16,7 @@ import { SAMPLE_INTERVAL_MS, registerChart, } from '../../../shared/metrics.js' +import { readFileCached, readFileBuf } from '../../utils/fd-cache.js' import { extractHelper, hasEmbeddedHelper } from '../../native/extract-helper.js' import logger from '../../utils/logger.js' @@ -202,13 +203,12 @@ export class EbpfCollector extends EventEmitter { _readVm() { /** @type {Record} */ const out = {} - try { - for (const line of fs.readFileSync('/proc/vmstat', 'utf8').split('\n')) { + const raw = readFileCached('/proc/vmstat') + if (raw) { + for (const line of raw.split('\n')) { const [k, v] = line.trim().split(/\s+/) if (k) out[k] = Number(v) || 0 } - } catch { - // ignore } return out } @@ -228,30 +228,27 @@ export class EbpfCollector extends EventEmitter { const den = dHit + dMiss let fileNr = [0, 0] - try { - const parts = fs.readFileSync('/proc/sys/fs/file-nr', 'utf8').trim().split(/\s+/) + const fileNrRaw = readFileCached('/proc/sys/fs/file-nr') + if (fileNrRaw) { + const parts = fileNrRaw.trim().split(/\s+/) fileNr = [Number(parts[0]) || 0, Number(parts[2]) || 0] - } catch { - // ignore } let forks = 0 let ctxt = 0 - try { - for (const line of fs.readFileSync('/proc/stat', 'utf8').split('\n')) { + const statRaw = readFileCached('/proc/stat') + if (statRaw) { + for (const line of statRaw.split('\n')) { if (line.startsWith('processes ')) forks = Number(line.slice(10)) || 0 if (line.startsWith('ctxt ')) ctxt = Number(line.slice(5)) || 0 } - } catch { - // ignore } let tcp = 0 - try { - const m = fs.readFileSync('/proc/net/sockstat', 'utf8').match(/TCP:\s+inuse\s+(\d+)/) + const sockRaw = readFileCached('/proc/net/sockstat') + if (sockRaw) { + const m = sockRaw.match(/TCP:\s+inuse\s+(\d+)/) if (m) tcp = Number(m[1]) || 0 - } catch { - // ignore } const batch = [ diff --git a/server/services/collectors/fs-stats.js b/server/services/collectors/fs-stats.js index 6754bc2..29f3231 100644 --- a/server/services/collectors/fs-stats.js +++ b/server/services/collectors/fs-stats.js @@ -11,6 +11,7 @@ import path from 'path' import os from 'os' import { CollectorPlugin } from './plugin.js' import { registerChart } from '../../../shared/metrics.js' +import { readFileBuf } from '../../utils/fd-cache.js' import logger from '../../utils/logger.js' const log = logger.child('fs-stats') @@ -23,11 +24,7 @@ export function isFsStatsEnabled() { } function readFile(p) { - try { - return fs.readFileSync(p, 'utf8') - } catch { - return null - } + return readFileBuf(p) } function readNum(p) { diff --git a/server/services/collectors/mdstat.js b/server/services/collectors/mdstat.js index dee8a49..f27bffa 100644 --- a/server/services/collectors/mdstat.js +++ b/server/services/collectors/mdstat.js @@ -9,6 +9,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 logger from '../../utils/logger.js' const log = logger.child('mdstat') @@ -31,11 +32,7 @@ const CHART_HEALTH = { } function readFile(p) { - try { - return fs.readFileSync(p, 'utf8') - } catch { - return null - } + return readFileBuf(p) } /** diff --git a/server/services/collectors/processes.js b/server/services/collectors/processes.js index 842b8b3..2ae8744 100644 --- a/server/services/collectors/processes.js +++ b/server/services/collectors/processes.js @@ -17,6 +17,7 @@ import path from 'path' 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 logger from '../../utils/logger.js' const log = logger.child('processes') @@ -50,6 +51,10 @@ function sanitizeDim(name) { return s || 'unknown' } +const PROC_EXTRA_CACHE = new Map() +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} */ @@ -61,47 +66,52 @@ export function listProcStats() { } catch { return null } + procTickCount = (procTickCount + 1) % PROC_EXTRA_TICK + const readExtra = procTickCount === 0 + /** @type {Array<{ pid: number, name: string, utime: number, stime: number, rssPages: number, threads: number, readBytes: number|null, writeBytes: number|null }>} */ const out = [] for (const ent of dirs) { if (!/^\d+$/.test(ent)) continue const pid = Number(ent) - let raw - try { - raw = fs.readFileSync(path.join('/proc', ent, 'stat'), 'utf8') - } catch { - continue - } + const raw = readFileBuf(path.join('/proc', ent, 'stat')) + if (!raw) continue const open = raw.indexOf('(') const close = raw.lastIndexOf(')') if (open < 0 || close < open) continue const name = raw.slice(open + 1, close) const rest = raw.slice(close + 2).split(/\s+/) - // fields after comm: state(0) … utime(11) stime(12) … rss(21) const utime = Number(rest[11]) const stime = Number(rest[12]) const rssPages = Number(rest[21]) if (!Number.isFinite(utime) || !Number.isFinite(stime)) continue let threads = 0 - try { - const status = fs.readFileSync(path.join('/proc', ent, 'status'), 'utf8') - const m = status.match(/^Threads:\s*(\d+)/m) - if (m) threads = Number(m[1]) || 0 - } catch { - // ignore - } - let readBytes = null let writeBytes = null - try { - const ioRaw = fs.readFileSync(path.join('/proc', ent, 'io'), 'utf8') - const rb = ioRaw.match(/^read_bytes:\s*(\d+)/m) - const wb = ioRaw.match(/^write_bytes:\s*(\d+)/m) - if (rb) readBytes = Number(rb[1]) || 0 - if (wb) writeBytes = Number(wb[1]) || 0 - } catch { - // unreadable for this pid + + if (readExtra) { + const status = readFileBuf(path.join('/proc', ent, 'status')) + if (status) { + const m = status.match(/^Threads:\s*(\d+)/m) + if (m) threads = Number(m[1]) || 0 + } + const ioRaw = readFileBuf(path.join('/proc', ent, 'io')) + if (ioRaw) { + const rb = ioRaw.match(/^read_bytes:\s*(\d+)/m) + const wb = ioRaw.match(/^write_bytes:\s*(\d+)/m) + if (rb) readBytes = Number(rb[1]) || 0 + if (wb) writeBytes = Number(wb[1]) || 0 + } + } + 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({ diff --git a/server/services/collectors/sensors.js b/server/services/collectors/sensors.js index 699854c..eead684 100644 --- a/server/services/collectors/sensors.js +++ b/server/services/collectors/sensors.js @@ -14,6 +14,7 @@ import { makeSensorTempChart, makeThermalZoneChart, } from '../../../shared/metrics.js' +import { readFileBuf } from '../../utils/fd-cache.js' import logger from '../../utils/logger.js' const log = logger.child('sensors') @@ -26,11 +27,7 @@ export function isSensorsEnabled() { } function readFile(p) { - try { - return fs.readFileSync(p, 'utf8') - } catch { - return null - } + return readFileBuf(p) } function makeSensorFanChart(chip, label) { diff --git a/server/services/collectors/sockets.js b/server/services/collectors/sockets.js index cf9c2d2..47c28bb 100644 --- a/server/services/collectors/sockets.js +++ b/server/services/collectors/sockets.js @@ -6,6 +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 logger from '../../utils/logger.js' const log = logger.child('sockets') @@ -42,12 +43,8 @@ function countTcp(file) { syn_recv: 0, other: 0, } - let raw - try { - raw = fs.readFileSync(file, 'utf8') - } catch { - return counts - } + const raw = readFileBuf(file) + if (!raw) return counts for (const line of raw.split('\n').slice(1)) { const parts = line.trim().split(/\s+/) if (parts.length < 4) continue @@ -60,11 +57,9 @@ function countTcp(file) { } function countUdp(file) { - try { - return Math.max(0, fs.readFileSync(file, 'utf8').trim().split('\n').length - 1) - } catch { - return 0 - } + const raw = readFileBuf(file) + if (!raw) return 0 + return Math.max(0, raw.trim().split('\n').length - 1) } export class SocketsCollector extends EventEmitter { diff --git a/server/utils/fd-cache.js b/server/utils/fd-cache.js index faf8263..6da7167 100644 --- a/server/utils/fd-cache.js +++ b/server/utils/fd-cache.js @@ -1,6 +1,7 @@ import fs from 'fs' const READ_BUF = Buffer.alloc(65536) +const SMALL_BUF = Buffer.alloc(8192) const fdCache = new Map() export function readFileCached(path) { @@ -24,6 +25,25 @@ export function readFileCached(path) { } } +/** + * Read a file using the shared small buffer, no FD caching. + * Good for dynamic paths (per-PID, per-cgroup) where open+close each tick is acceptable + * but Buffer allocation is not. + */ +export function readFileBuf(path) { + try { + const fd = fs.openSync(path, 'r') + try { + const bytes = fs.readSync(fd, SMALL_BUF, 0, SMALL_BUF.length, 0) + return SMALL_BUF.toString('utf8', 0, bytes) + } finally { + fs.closeSync(fd) + } + } catch { + return null + } +} + export function closeFdCache() { for (const [path, entry] of fdCache) { if (entry) {