Experimental CPU Optimize Techniques
CI / test (push) Successful in 1m1s
Release rolling / release (push) Successful in 7m25s

This commit is contained in:
Raven Scott
2026-07-21 11:58:29 -04:00
parent 58fcc52d6c
commit 1d902a89b0
8 changed files with 149 additions and 68 deletions
+1
View File
@@ -225,6 +225,7 @@ export class AnomalyEngine extends EventEmitter {
evaluate(batch) { evaluate(batch) {
/** @type {AnomalyEvent[]} */ /** @type {AnomalyEvent[]} */
const fired = [] const fired = []
if (!this.configs.size) return fired
const byChart = new Map(batch.map((s) => [s.chart, s])) const byChart = new Map(batch.map((s) => [s.chart, s]))
for (const id of [...this.configs.keys()]) { for (const id of [...this.configs.keys()]) {
+50 -6
View File
@@ -44,10 +44,13 @@ import {
ifaceSpeedKbps, ifaceSpeedKbps,
} from './collectors/host-deep.js' } from './collectors/host-deep.js'
import { collectHostMore } from './collectors/host-more.js' import { collectHostMore } from './collectors/host-more.js'
import { readFileCached, closeFdCache } from '../utils/fd-cache.js'
import logger from '../utils/logger.js' import logger from '../utils/logger.js'
const log = logger.child('collector') const log = logger.child('collector')
const IS_LINUX = os.platform() === 'linux'
function safeProcessUptime() { function safeProcessUptime() {
try { try {
if (typeof process !== 'undefined' && typeof process.uptime === 'function') { if (typeof process !== 'undefined' && typeof process.uptime === 'function') {
@@ -66,11 +69,7 @@ function bytesToGiB(n) {
return n / (1024 * 1024 * 1024) return n / (1024 * 1024 * 1024)
} }
function readFile(path) { function readFile(path) {
try { return IS_LINUX ? readFileCached(path) : null
return fs.readFileSync(path, 'utf8')
} catch {
return null
}
} }
function rate(prev, cur, dtSec) { function rate(prev, cur, dtSec) {
if (dtSec <= 0 || cur < prev) return 0 if (dtSec <= 0 || cur < prev) return 0
@@ -423,6 +422,7 @@ export class MetricsCollector extends EventEmitter {
this.deepState = {} this.deepState = {}
/** @type {Map<string, Record<string, number|null>>} */ /** @type {Map<string, Record<string, number|null>>} */
this.latest = new Map() this.latest = new Map()
this._chartsRegistered = false
} }
start() { start() {
@@ -438,6 +438,7 @@ export class MetricsCollector extends EventEmitter {
this.running = false this.running = false
if (this.timer) clearInterval(this.timer) if (this.timer) clearInterval(this.timer)
this.timer = null this.timer = null
if (IS_LINUX) closeFdCache()
} }
_tick() { _tick() {
@@ -446,9 +447,11 @@ export class MetricsCollector extends EventEmitter {
const dtSec = this.lastTs ? (ts - this.lastTs) / 1000 : this.intervalMs / 1000 const dtSec = this.lastTs ? (ts - this.lastTs) / 1000 : this.intervalMs / 1000
this.lastTs = ts this.lastTs = ts
const prevVmSnapshot = this.lastVm const prevVmSnapshot = this.lastVm
/** @type {Array<{ chart: string, context: string, ts: number, values: Record<string, number|null> }>} */ /** @type {Array<{ chart: string, context: string, ts: number, values: Record<string, number|null> }>} */
const batch = [] const batch = []
if (IS_LINUX) {
this._safeCollect('cpu', () => this._collectCpuAndProcs(batch, ts, dtSec)) this._safeCollect('cpu', () => this._collectCpuAndProcs(batch, ts, dtSec))
this._safeCollect('cpu_sysfs', () => collectCpuSysfs(batch, ts, dtSec, this.deepState)) 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))
@@ -464,14 +467,20 @@ export class MetricsCollector extends EventEmitter {
prevVm: prevVmSnapshot, prevVm: prevVmSnapshot,
}) })
}) })
} else {
this._safeCollect('cpu', () => this._collectCpuFallback(batch, ts, dtSec))
this._safeCollect('memory', () => this._collectMemoryFallback(batch, ts))
}
let uptime = 0 let uptime = 0
try { try {
uptime = os.uptime() uptime = os.uptime()
} catch { } catch {
if (IS_LINUX) {
const raw = readFile('/proc/uptime') const raw = readFile('/proc/uptime')
if (raw) uptime = Number(raw.trim().split(/\s+/)[0]) || 0 if (raw) uptime = Number(raw.trim().split(/\s+/)[0]) || 0
} }
}
batch.push({ batch.push({
chart: 'system.uptime', chart: 'system.uptime',
context: 'system.uptime', context: 'system.uptime',
@@ -479,6 +488,7 @@ export class MetricsCollector extends EventEmitter {
values: { uptime }, values: { uptime },
}) })
if (IS_LINUX) {
const entropy = readFile('/proc/sys/kernel/random/entropy_avail') const entropy = readFile('/proc/sys/kernel/random/entropy_avail')
if (entropy != null) { if (entropy != null) {
batch.push({ batch.push({
@@ -488,9 +498,11 @@ export class MetricsCollector extends EventEmitter {
values: { entropy: Number(entropy.trim()) || 0 }, values: { entropy: Number(entropy.trim()) || 0 },
}) })
} }
}
for (const s of batch) this.latest.set(s.chart, s.values) for (const s of batch) this.latest.set(s.chart, s.values)
this.sampleCount++ this.sampleCount++
this._chartsRegistered = true
this.emit('samples', batch) this.emit('samples', batch)
} catch (err) { } catch (err) {
log.error('Collector tick failed', { error: err.message }) log.error('Collector tick failed', { error: err.message })
@@ -505,6 +517,32 @@ export class MetricsCollector extends EventEmitter {
} }
} }
_collectCpuFallback(batch, ts, dtSec) {
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 }
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))
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
}
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 }
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] } })
}
_collectMemoryFallback(batch, ts) {
const total = os.totalmem()
const free = os.freemem()
batch.push({ chart: 'system.ram', context: 'system.ram', ts, values: { free: bytesToMiB(free), used: bytesToMiB(total - free), cached: 0, buffers: 0 } })
batch.push({ chart: 'mem.available', context: 'mem.available', ts, values: { avail: bytesToMiB(free) } })
}
_collectCpuAndProcs(batch, ts, dtSec) { _collectCpuAndProcs(batch, ts, dtSec) {
const stat = parseProcStat() const stat = parseProcStat()
const load = parseLoadavg() const load = parseLoadavg()
@@ -514,7 +552,7 @@ export class MetricsCollector extends EventEmitter {
batch.push({ chart: 'system.cpu', context: 'system.cpu', ts, values: cpuVals }) batch.push({ chart: 'system.cpu', context: 'system.cpu', ts, values: cpuVals })
for (const core of stat.cores) { for (const core of stat.cores) {
registerChart(makeCpuCoreChart(core.id)) if (!this._chartsRegistered) registerChart(makeCpuCoreChart(core.id))
const prev = this.lastStat?.cores?.find((c) => c.id === core.id) const prev = this.lastStat?.cores?.find((c) => c.id === core.id)
batch.push({ batch.push({
chart: `cpu.cpu${core.id}`, chart: `cpu.cpu${core.id}`,
@@ -764,6 +802,7 @@ export class MetricsCollector extends EventEmitter {
for (const [name, d] of Object.entries(disks)) { for (const [name, d] of Object.entries(disks)) {
if (name === '__agg') continue if (name === '__agg') continue
if (!this._chartsRegistered) {
registerChart(makeDiskIoChart(name)) registerChart(makeDiskIoChart(name))
registerChart(makeDiskOpsChart(name)) registerChart(makeDiskOpsChart(name))
registerChart(makeDiskUtilChart(name)) registerChart(makeDiskUtilChart(name))
@@ -776,6 +815,7 @@ export class MetricsCollector extends EventEmitter {
registerChart(makeDiskSvctmChart(name)) registerChart(makeDiskSvctmChart(name))
registerChart(makeDiskMergedChart(name)) registerChart(makeDiskMergedChart(name))
registerChart(makeDiskFlushChart(name)) registerChart(makeDiskFlushChart(name))
}
const prev = prevAll[name] const prev = prevAll[name]
const readBytes = d.readSectors * 512 const readBytes = d.readSectors * 512
const writeBytes = d.writeSectors * 512 const writeBytes = d.writeSectors * 512
@@ -911,6 +951,7 @@ export class MetricsCollector extends EventEmitter {
for (const [name, n] of Object.entries(ifaces)) { for (const [name, n] of Object.entries(ifaces)) {
if (name === '__agg') continue if (name === '__agg') continue
if (!this._chartsRegistered) {
registerChart(makeNetChart(name)) registerChart(makeNetChart(name))
registerChart(makeNetPacketsChart(name)) registerChart(makeNetPacketsChart(name))
registerChart(makeNetErrorsChart(name)) registerChart(makeNetErrorsChart(name))
@@ -919,6 +960,7 @@ export class MetricsCollector extends EventEmitter {
registerChart(makeNetDuplexChart(name)) registerChart(makeNetDuplexChart(name))
registerChart(makeNetMtuChart(name)) registerChart(makeNetMtuChart(name))
registerChart(makeNetQueueChart(name)) registerChart(makeNetQueueChart(name))
}
const prev = prevAll[name] const prev = prevAll[name]
rx += n.rxBytes rx += n.rxBytes
tx += n.txBytes tx += n.txBytes
@@ -1189,8 +1231,10 @@ export class MetricsCollector extends EventEmitter {
const id = mountId(m.path) const id = mountId(m.path)
const sample = sampleMount(m.path) const sample = sampleMount(m.path)
if (!sample) continue if (!sample) continue
if (!this._chartsRegistered) {
registerChart(makeDiskSpaceChart(id, m.path)) registerChart(makeDiskSpaceChart(id, m.path))
registerChart(makeDiskInodesChart(id, m.path)) registerChart(makeDiskInodesChart(id, m.path))
}
batch.push({ batch.push({
chart: `disk_space.${id}`, chart: `disk_space.${id}`,
context: 'disk.space', context: 'disk.space',
+2 -5
View File
@@ -6,13 +6,10 @@ import fs from 'fs'
import os from 'os' import os from 'os'
import path from 'path' import path from 'path'
import { registerChart } from '../../../shared/metrics.js' import { registerChart } from '../../../shared/metrics.js'
import { readFileCached } from '../../utils/fd-cache.js'
function readFile(p) { function readFile(p) {
try { return readFileCached(p)
return fs.readFileSync(p, 'utf8')
} catch {
return null
}
} }
function rate(prev, cur, dtSec) { function rate(prev, cur, dtSec) {
+2 -5
View File
@@ -11,13 +11,10 @@ import {
makeCpuIdleChart, makeCpuIdleChart,
makeNumaNodeMemChart, makeNumaNodeMemChart,
} from '../../../shared/metrics.js' } from '../../../shared/metrics.js'
import { readFileCached } from '../../utils/fd-cache.js'
function readFile(p) { function readFile(p) {
try { return readFileCached(p)
return fs.readFileSync(p, 'utf8')
} catch {
return null
}
} }
function rate(prev, cur, dtSec) { function rate(prev, cur, dtSec) {
+3 -1
View File
@@ -58,9 +58,11 @@ export function isSelfMonitorEnabled() {
return true return true
} }
const SELF_MONITOR_INTERVAL = Number(process.env.PEARDATA_SELF_MONITOR_MS) || 5000
export class SelfMonitorCollector extends CollectorPlugin { export class SelfMonitorCollector extends CollectorPlugin {
constructor(opts = {}) { constructor(opts = {}) {
super({ name: 'self-monitor', intervalMs: opts.intervalMs }) super({ name: 'self-monitor', intervalMs: opts.intervalMs || SELF_MONITOR_INTERVAL })
/** @type {{ user: number, system: number }|null} */ /** @type {{ user: number, system: number }|null} */
this._prevCpu = null this._prevCpu = null
/** @type {number|null} */ /** @type {number|null} */
+2 -2
View File
@@ -102,7 +102,7 @@ export class MetricStore extends EventEmitter {
this.series.set(s.chart, entry) this.series.set(s.chart, entry)
} }
entry.points.push({ ts: s.ts, values: s.values }) entry.points.push({ ts: s.ts, values: s.values })
if (entry.points.length > this.tier0Max) { if (entry.points.length > this.tier0Max + 60) {
entry.points.splice(0, entry.points.length - this.tier0Max) entry.points.splice(0, entry.points.length - this.tier0Max)
} }
@@ -125,7 +125,7 @@ export class MetricStore extends EventEmitter {
} }
const warm = { ts: s.ts, values: avg } const warm = { ts: s.ts, values: avg }
entry.tier1.push(warm) entry.tier1.push(warm)
if (entry.tier1.length > this.tier1Max) { if (entry.tier1.length > this.tier1Max + 30) {
entry.tier1.splice(0, entry.tier1.length - this.tier1Max) entry.tier1.splice(0, entry.tier1.length - this.tier1Max)
} }
entry.acc = null entry.acc = null
+9 -3
View File
@@ -46,7 +46,9 @@ export function unsubscribeAnomalies(session) {
* @param {Array<{ chart: string, context: string, ts: number, values: object }>} batch * @param {Array<{ chart: string, context: string, ts: number, values: object }>} batch
*/ */
export function broadcastMetrics(batch) { export function broadcastMetrics(batch) {
for (const session of peers.list()) { const list = peers.list()
if (!list.length) return
for (const session of list) {
const sub = session.state.get('metricSub') const sub = session.state.get('metricSub')
if (!sub || session.closed) continue if (!sub || session.closed) continue
const filtered = const filtered =
@@ -73,7 +75,9 @@ export function broadcastMetrics(batch) {
* @param {object} anomaly * @param {object} anomaly
*/ */
export function broadcastAnomaly(anomaly) { export function broadcastAnomaly(anomaly) {
for (const session of peers.list()) { const list = peers.list()
if (!list.length) return
for (const session of list) {
if (!session.state.get('anomalySub') || session.closed) continue if (!session.state.get('anomalySub') || session.closed) continue
try { try {
session.push(Pushes.anomaly, anomaly) session.push(Pushes.anomaly, anomaly)
@@ -87,7 +91,9 @@ export function broadcastAnomaly(anomaly) {
* @param {object} payload * @param {object} payload
*/ */
export function broadcastHealth(payload) { export function broadcastHealth(payload) {
for (const session of peers.list()) { const list = peers.list()
if (!list.length) return
for (const session of list) {
if (session.closed) continue if (session.closed) continue
try { try {
session.push(Pushes.health, payload) session.push(Pushes.health, payload)
+34
View File
@@ -0,0 +1,34 @@
import fs from 'fs'
const READ_BUF = Buffer.alloc(65536)
const fdCache = new Map()
export function readFileCached(path) {
let entry = fdCache.get(path)
if (entry === undefined) {
try {
const fd = fs.openSync(path, 'r')
entry = { fd }
fdCache.set(path, entry)
} catch {
fdCache.set(path, null)
return null
}
}
if (!entry) return null
try {
const bytesRead = fs.readSync(entry.fd, READ_BUF, 0, READ_BUF.length, 0)
return READ_BUF.toString('utf8', 0, bytesRead)
} catch {
return null
}
}
export function closeFdCache() {
for (const [path, entry] of fdCache) {
if (entry) {
try { fs.closeSync(entry.fd) } catch {}
}
}
fdCache.clear()
}