mirror of
https://github.com/wekan/wekan.git
synced 2026-11-07 11:02:33 -05:00
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.
63 lines
2.3 KiB
JavaScript
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 };
|