diff --git a/server/services/collectors/cgroups.js b/server/services/collectors/cgroups.js index eeec5d5..35cfb3d 100644 --- a/server/services/collectors/cgroups.js +++ b/server/services/collectors/cgroups.js @@ -199,6 +199,9 @@ export class CgroupsCollector extends EventEmitter { /** @type {Map} */ this._names = new Map() this._nameRefreshAt = 0 + /** @type {Array<{ id: string, title: string, path: string }>|null} */ + this._cgCache = null + this._cgTs = 0 } start() { @@ -253,18 +256,28 @@ export class CgroupsCollector extends EventEmitter { /** @type {Array<{ chart: string, context: string, ts: number, values: object }>} */ const batch = [] - for (const cg of discoverCgroups()) { + const now = Date.now() + if (!this._cgCache || now - this._cgTs > 30000) { + this._cgCache = discoverCgroups() + this._cgTs = now + for (const cg of this._cgCache) { + const title = resolveContainerLabel(cg.title, this._names) + registerChart(makeCgroupCpuChart(cg.id, title)) + registerChart(makeCgroupMemChart(cg.id, title)) + registerChart(makeCgroupIoChart(cg.id, title)) + registerChart(makeCgroupMemDetailChart(cg.id, title)) + registerChart(makeCgroupThrottleChart(cg.id, title)) + } + } + const cgList = this._cgCache + + for (const cg of cgList) { const title = resolveContainerLabel(cg.title, this._names) const cpuDef = makeCgroupCpuChart(cg.id, title) const memDef = makeCgroupMemChart(cg.id, title) const ioDef = makeCgroupIoChart(cg.id, title) const memDetailDef = makeCgroupMemDetailChart(cg.id, title) const throttleDef = makeCgroupThrottleChart(cg.id, title) - registerChart(cpuDef) - registerChart(memDef) - registerChart(ioDef) - registerChart(memDetailDef) - registerChart(throttleDef) const cpu = parseCpuStat(cg.path) let usageUsec = cpu?.usage_usec ?? 0 diff --git a/server/services/collectors/processes.js b/server/services/collectors/processes.js index 4e3a969..d924804 100644 --- a/server/services/collectors/processes.js +++ b/server/services/collectors/processes.js @@ -212,7 +212,7 @@ function registerTopChart(names, kind) { export class ProcessCollector extends EventEmitter { constructor(opts = {}) { super() - this.intervalMs = opts.intervalMs || Number(process.env.PEARDATA_PROCESSES_MS) || Number(process.env.PEARDATA_SAMPLE_MS) || SAMPLE_INTERVAL_MS + this.intervalMs = opts.intervalMs || Number(process.env.PEARDATA_PROCESSES_MS) || Number(process.env.PEARDATA_SAMPLE_MS) || 3000 this._timer = null /** @type {Map|null} */ this._prev = null diff --git a/server/services/collectors/sensors.js b/server/services/collectors/sensors.js index eead684..1f6589f 100644 --- a/server/services/collectors/sensors.js +++ b/server/services/collectors/sensors.js @@ -78,16 +78,66 @@ function makeSensorPowerChart(chip, label) { } } +/** + * @returns {Array<{ chartId: string, context: string, values: Record }>} + */ /** * @returns {Array<{ chartId: string, context: string, values: Record }>} */ export function sampleSensors() { /** @type {Array<{ chartId: string, context: string, values: Record }>} */ const out = [] + + for (const entry of getSensorPaths()) { + let raw + if (entry._type === 'temp') { + raw = readFile(entry._inputPath) + if (raw == null) continue + const milli = Number(raw.trim()) + if (!Number.isFinite(milli)) continue + out.push({ chartId: entry.chartId, context: entry.context, values: { temperature: milli / 1000 } }) + } else if (entry._type === 'fan') { + raw = readFile(entry._inputPath) + if (raw == null) continue + const rpm = Number(raw.trim()) + if (!Number.isFinite(rpm)) continue + out.push({ chartId: entry.chartId, context: entry.context, values: { rpm } }) + } else if (entry._type === 'volt') { + raw = readFile(entry._inputPath) + if (raw == null) continue + const mv = Number(raw.trim()) + if (!Number.isFinite(mv)) continue + out.push({ chartId: entry.chartId, context: entry.context, values: { millivolts: mv } }) + } else if (entry._type === 'power') { + raw = readFile(entry._inputPath) + if (raw == null) continue + const uw = Number(raw.trim()) + if (!Number.isFinite(uw)) continue + out.push({ chartId: entry.chartId, context: entry.context, values: { microwatts: uw } }) + } else if (entry._type === 'thermal') { + raw = readFile(entry._inputPath) + if (raw == null) continue + const milli = Number(raw.trim()) + if (!Number.isFinite(milli) || milli === 0) continue + const celsius = milli > 1000 ? milli / 1000 : milli + out.push({ chartId: entry.chartId, context: entry.context, values: { temperature: celsius } }) + } + } + return out +} + +let _sensorPaths = null +let _sensorPathsTs = 0 + +function getSensorPaths() { + const now = Date.now() + if (_sensorPaths && now - _sensorPathsTs < 30000) return _sensorPaths + _sensorPaths = [] + _sensorPathsTs = now + const hwmonRoot = '/sys/class/hwmon' try { - const chips = fs.readdirSync(hwmonRoot) - for (const chip of chips) { + for (const chip of fs.readdirSync(hwmonRoot)) { const dir = path.join(hwmonRoot, chip) const name = (readFile(path.join(dir, 'name')) || chip).trim() let ents @@ -98,60 +148,40 @@ export function sampleSensors() { } for (const ent of ents) { const m = ent.match(/^temp(\d+)_input$/) - if (!m) continue - const n = m[1] - const raw = readFile(path.join(dir, ent)) - if (raw == null) continue - const milli = Number(raw.trim()) - if (!Number.isFinite(milli)) continue - const label = - (readFile(path.join(dir, `temp${n}_label`)) || `temp${n}`).trim() || `temp${n}` - const def = makeSensorTempChart(name, label) - registerChart(def) - out.push({ - chartId: def.id, - context: def.context, - values: { temperature: milli / 1000 }, - }) - } - const fan = ent.match(/^fan(\d+)_input$/) - if (fan) { - const n = fan[1] - const raw = readFile(path.join(dir, ent)) - if (raw == null) continue - const rpm = Number(raw.trim()) - if (!Number.isFinite(rpm)) continue - const label = - (readFile(path.join(dir, `fan${n}_label`)) || `fan${n}`).trim() || `fan${n}` - const def = makeSensorFanChart(name, label) - registerChart(def) - out.push({ chartId: def.id, context: def.context, values: { rpm } }) - } - const vin = ent.match(/^in(\d+)_input$/) - if (vin) { - const n = vin[1] - const raw = readFile(path.join(dir, ent)) - if (raw == null) continue - const mv = Number(raw.trim()) - if (!Number.isFinite(mv)) continue - const label = - (readFile(path.join(dir, `in${n}_label`)) || `in${n}`).trim() || `in${n}` - const def = makeSensorVoltageChart(name, label) - registerChart(def) - out.push({ chartId: def.id, context: def.context, values: { millivolts: mv } }) - } - const pwr = ent.match(/^power(\d+)_input$/) - if (pwr) { - const n = pwr[1] - const raw = readFile(path.join(dir, ent)) - if (raw == null) continue - const uw = Number(raw.trim()) - if (!Number.isFinite(uw)) continue - const label = - (readFile(path.join(dir, `power${n}_label`)) || `power${n}`).trim() || `power${n}` - const def = makeSensorPowerChart(name, label) - registerChart(def) - out.push({ chartId: def.id, context: def.context, values: { microwatts: uw } }) + if (m) { + const n = m[1] + const label = (readFile(path.join(dir, `temp${n}_label`)) || `temp${n}`).trim() || `temp${n}` + const def = makeSensorTempChart(name, label) + registerChart(def) + _sensorPaths.push({ chartId: def.id, context: def.context, _type: 'temp', _inputPath: path.join(dir, ent) }) + continue + } + const fan = ent.match(/^fan(\d+)_input$/) + if (fan) { + const n = fan[1] + const label = (readFile(path.join(dir, `fan${n}_label`)) || `fan${n}`).trim() || `fan${n}` + const def = makeSensorFanChart(name, label) + registerChart(def) + _sensorPaths.push({ chartId: def.id, context: def.context, _type: 'fan', _inputPath: path.join(dir, ent) }) + continue + } + const vin = ent.match(/^in(\d+)_input$/) + if (vin) { + const n = vin[1] + const label = (readFile(path.join(dir, `in${n}_label`)) || `in${n}`).trim() || `in${n}` + const def = makeSensorVoltageChart(name, label) + registerChart(def) + _sensorPaths.push({ chartId: def.id, context: def.context, _type: 'volt', _inputPath: path.join(dir, ent) }) + continue + } + const pwr = ent.match(/^power(\d+)_input$/) + if (pwr) { + const n = pwr[1] + const label = (readFile(path.join(dir, `power${n}_label`)) || `power${n}`).trim() || `power${n}` + const def = makeSensorPowerChart(name, label) + registerChart(def) + _sensorPaths.push({ chartId: def.id, context: def.context, _type: 'power', _inputPath: path.join(dir, ent) }) + } } } } catch { @@ -164,20 +194,15 @@ export function sampleSensors() { if (!zone.startsWith('thermal_zone')) continue const dir = path.join(thermalRoot, zone) const type = (readFile(path.join(dir, 'type')) || zone).trim() - const raw = readFile(path.join(dir, 'temp')) - if (raw == null) continue - const milli = Number(raw.trim()) - if (!Number.isFinite(milli) || milli === 0) continue - // some platforms report already in C - const celsius = milli > 1000 ? milli / 1000 : milli const def = makeThermalZoneChart(zone, type) registerChart(def) - out.push({ chartId: def.id, context: def.context, values: { temperature: celsius } }) + _sensorPaths.push({ chartId: def.id, context: def.context, _type: 'thermal', _inputPath: path.join(dir, 'temp') }) } } catch { // no thermal } - return out + + return _sensorPaths } export class SensorsCollector extends EventEmitter {