@cocalc/project
Version:
CoCalc: project daemon
155 lines (131 loc) • 4.19 kB
text/typescript
/*
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
* License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details
*/
/*
* This little utility tames process of this project to be kind to other users.
* It's inspired by and – http://and.sourceforge.net/
*/
import debug from "debug";
const L = debug("project:autorenice");
import { reverse, sortBy } from "lodash";
import { setPriority } from "os";
import { delay } from "awaiting";
import { ProjectInfoServer, get_ProjectInfoServer } from "./project-info";
import { ProjectInfo, Processes, Process } from "./project-info/types";
import { is_free_project, DEFAULT_FREE_PROCS_NICENESS } from "./project-setup";
const INTERVAL_S = 10;
// renice configuration -- the first time values must be decreasing
const RENICE = reverse(
sortBy(
[
{ time_s: 10 * 60, niceness: 19 },
{ time_s: 5 * 60, niceness: 10 },
{ time_s: 60, niceness: 4 },
],
"time_s"
)
);
interface Opts {
verbose?: boolean;
config?: string; // TODO: make it possible to set via env var COCALC_PROJECT_AUTORENICE (also there are only harcoded values).
}
class ProcessRenicer {
private readonly verbose: boolean;
private readonly free_project: boolean;
private readonly project_info: ProjectInfoServer;
private readonly config: string;
private timestamp?: number;
private processes?: Processes;
constructor(opts?: Opts) {
const { verbose = false, config = "1" } = opts ?? {};
this.free_project = is_free_project();
this.verbose = verbose;
this.config = config;
L("config", this.config);
if (config == "0") return;
this.project_info = get_ProjectInfoServer();
this.init();
this.start();
}
private async init(): Promise<void> {
this.project_info.start();
this.project_info.on("info", (info: ProjectInfo) => {
this.update(info);
});
}
// got new data from the ProjectInfoServer
private update(info: ProjectInfo) {
if (info != null) {
this.processes = info.processes;
this.timestamp = info.timestamp;
}
}
// this is the main "infinite loop"
private async start(): Promise<void> {
if (this.verbose) L("starting main loop");
while (true) {
await delay(INTERVAL_S * 1000);
// no data yet
if (this.processes == null || this.timestamp == null) continue;
// ignore outdated data
if (this.timestamp < Date.now() - 60 * 1000) continue;
// check processes
for (const proc of Object.values(this.processes)) {
// ignore the init process
if (proc.pid == 1) continue;
// we also skip the project process
if (proc.cocalc?.type == "project") continue;
this.adjust_proc(proc);
}
}
}
private adjust_proc(proc: Process) {
// special case: free project processes have a low default priority
const old_nice = proc.stat.nice;
const new_nice = this.nice(proc.stat);
if (old_nice < new_nice) {
const msg = `${proc.pid} from ${old_nice} to ${new_nice}`;
try {
L(`setPriority ${msg}`);
setPriority(proc.pid, new_nice);
} catch (err) {
L(`Error setPriority ${msg}`, err);
}
}
}
private nice(stat) {
// for free projects we do not bother with actual usage – just down prioritize all of them
if (this.free_project) {
return DEFAULT_FREE_PROCS_NICENESS;
}
const { utime, stime, cutime, cstime } = stat;
const self = utime + stime;
const child = cutime + cstime;
for (const { time_s, niceness } of RENICE) {
if (self > time_s || child > time_s) {
return niceness;
}
}
return 0;
}
}
let singleton: ProcessRenicer | undefined = undefined;
export function activate(opts?: Opts) {
if (singleton != null) {
L("blocking attempt to run ProcessRenicer twice");
return;
}
singleton = new ProcessRenicer(opts);
return singleton;
}
// testing: $ ts-node autorenice.ts
async function test() {
const pr = activate({ verbose: true });
L("activated ProcessRenicer in test mode", pr);
await delay(3 * 1000);
L("test done");
}
if (require.main === module) {
test();
}