Files
wekan/models/lib/cpuHighTracker.js
T
Lauri Ojansivu 1c636f09b3 CPU-usage monitor + governor + Admin Panel / Problems / CPU usage report
WeKan and FerretDB on one machine can drive CPU high enough to starve other
software. Add a monitor, a governor, and a report. Design: docs/Features/CPU/CPU.md.

Monitor (server/lib/cpuMonitor.js): samples system-wide CPU% from os.cpus()
idle/total deltas on an interval (so it includes WeKan, FerretDB and every other
process). A pure, hysteresis state machine (models/lib/cpuHighTracker.js) turns
the samples into ONLY the START and END of each sustained high-CPU period — two
rows per episode, never a flood. Enters high after 3 consecutive samples >= 85%,
leaves after 3 consecutive < 70%. Each row carries duration, peak, system load,
and what WeKan was doing (setActivity label). Recorded to a new 'cpu' event
stream (server/lib/cpuLog.js).

Governor: cpuMonitor exports isHighCpu() and pauseIfBusy(), so long batch
operations await a short pause and yield the CPU only while the system is busy
(no cost when idle). Wired into the existing-file extension corrector. Tunable
via WEKAN_CPU_* env vars.

Report: 'cpu' added to EVENT_STREAMS; reuses the generic eventStreamReport
(stream="cpu") with search + pagination; a "CPU usage" item is added to the
Admin Panel / Problems side menu (cpuReportTitle i18n).

Tests: cpuHighTracker unit (start/end, duration/peak, no-flap hysteresis;
positive + negative) and cpuMonitorWiring source guards. CHANGELOG + design doc.
2026-07-20 03:11:07 +03:00

63 lines
2.3 KiB
JavaScript

'use strict';
// Turn a stream of CPU-usage samples into ONLY "start" and "end" events for
// sustained high-CPU periods — so the Admin Panel → Problems → CPU usage report
// gets two rows per episode (when it began, when it ended), never a flood.
//
// Hysteresis: enter the high state only after `enterSamples` consecutive samples
// at/above `highPct`, and leave it only after `exitSamples` consecutive samples
// below `lowPct` (lowPct < highPct). This keeps a value hovering around the
// threshold from flapping start/end/start/end. Pure and unit-testable (no timers,
// no os calls) — the caller feeds it samples and timestamps.
class HighCpuTracker {
constructor(opts = {}) {
this.highPct = opts.highPct != null ? opts.highPct : 85;
this.lowPct = opts.lowPct != null ? opts.lowPct : 70;
this.enterSamples = opts.enterSamples != null ? opts.enterSamples : 3;
this.exitSamples = opts.exitSamples != null ? opts.exitSamples : 3;
this.high = false;
this.aboveCount = 0;
this.belowCount = 0;
this.startedAt = null;
this.peak = 0;
}
// Feed one CPU% sample (0..100) taken at time `now` (ms). Returns:
// { event: 'start', at, pct } when a high period begins
// { event: 'end', at, startedAt, durationMs, peak } when it ends
// { event: null } otherwise
update(pct, now) {
const value = Number(pct) || 0;
if (!this.high) {
this.aboveCount = value >= this.highPct ? this.aboveCount + 1 : 0;
if (this.aboveCount >= this.enterSamples) {
this.high = true;
this.belowCount = 0;
this.startedAt = now;
this.peak = value;
return { event: 'start', at: now, pct: value };
}
return { event: null };
}
if (value > this.peak) this.peak = value;
this.belowCount = value < this.lowPct ? this.belowCount + 1 : 0;
if (this.belowCount >= this.exitSamples) {
const startedAt = this.startedAt;
const peak = this.peak;
this.high = false;
this.aboveCount = 0;
this.startedAt = null;
this.peak = 0;
return { event: 'end', at: now, startedAt, durationMs: now - startedAt, peak };
}
return { event: null };
}
isHigh() {
return this.high;
}
}
module.exports = { HighCpuTracker };