CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/project/autorenice.ts
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
/*
7
* This little utility tames process of this project to be kind to other users.
8
* It's inspired by and – http://and.sourceforge.net/
9
*/
10
11
import { delay } from "awaiting";
12
import { reverse, sortBy } from "lodash";
13
import { setPriority } from "node:os";
14
15
import { getLogger } from "./logger";
16
import { ProjectInfoServer, get_ProjectInfoServer } from "./project-info";
17
import {
18
Process,
19
Processes,
20
ProjectInfo,
21
} from "@cocalc/util/types/project-info/types";
22
import { DEFAULT_FREE_PROCS_NICENESS, is_free_project } from "./project-setup";
23
24
const L = getLogger("autorenice").debug;
25
26
const INTERVAL_S = 10;
27
28
// renice configuration -- the first time values must be decreasing
29
const RENICE = reverse(
30
sortBy(
31
[
32
{ time_s: 10 * 60, niceness: 19 },
33
{ time_s: 5 * 60, niceness: 10 },
34
{ time_s: 60, niceness: 4 },
35
],
36
"time_s",
37
),
38
);
39
40
interface Opts {
41
verbose?: boolean;
42
config?: string; // TODO: make it possible to set via env var COCALC_PROJECT_AUTORENICE (also there are only harcoded values).
43
}
44
45
class ProcessRenicer {
46
private readonly verbose: boolean;
47
private readonly free_project: boolean;
48
private readonly project_info: ProjectInfoServer;
49
private readonly config: string;
50
private timestamp?: number;
51
private processes?: Processes;
52
53
constructor(opts?: Opts) {
54
const { verbose = false, config = "1" } = opts ?? {};
55
this.free_project = is_free_project();
56
this.verbose = verbose;
57
this.config = config;
58
L("config", this.config);
59
if (config == "0") return;
60
this.project_info = get_ProjectInfoServer();
61
this.init();
62
this.start();
63
}
64
65
private async init(): Promise<void> {
66
this.project_info.start();
67
this.project_info.on("info", (info: ProjectInfo) => {
68
this.update(info);
69
});
70
}
71
72
// got new data from the ProjectInfoServer
73
private update(info: ProjectInfo) {
74
if (info != null) {
75
this.processes = info.processes;
76
this.timestamp = info.timestamp;
77
}
78
}
79
80
// this is the main "infinite loop"
81
private async start(): Promise<void> {
82
if (this.verbose) L("starting main loop");
83
while (true) {
84
await delay(INTERVAL_S * 1000);
85
86
// no data yet
87
if (this.processes == null || this.timestamp == null) continue;
88
89
// ignore outdated data
90
if (this.timestamp < Date.now() - 60 * 1000) continue;
91
92
// check processes
93
for (const proc of Object.values(this.processes)) {
94
// ignore the init process
95
if (proc.pid == 1) continue;
96
97
// we also skip the project process
98
if (proc.cocalc?.type == "project") continue;
99
100
this.adjust_proc(proc);
101
}
102
}
103
}
104
105
private adjust_proc(proc: Process) {
106
// special case: free project processes have a low default priority
107
const old_nice = proc.stat.nice;
108
const new_nice = this.nice(proc.stat);
109
if (old_nice < new_nice) {
110
const msg = `${proc.pid} from ${old_nice} to ${new_nice}`;
111
try {
112
L(`setPriority ${msg}`);
113
setPriority(proc.pid, new_nice);
114
} catch (err) {
115
L(`Error setPriority ${msg}`, err);
116
}
117
}
118
}
119
120
private nice(stat) {
121
// for free projects we do not bother with actual usage – just down prioritize all of them
122
if (this.free_project) {
123
return DEFAULT_FREE_PROCS_NICENESS;
124
}
125
126
const { utime, stime, cutime, cstime } = stat;
127
const self = utime + stime;
128
const child = cutime + cstime;
129
130
for (const { time_s, niceness } of RENICE) {
131
if (self > time_s || child > time_s) {
132
return niceness;
133
}
134
}
135
return 0;
136
}
137
}
138
139
let singleton: ProcessRenicer | undefined = undefined;
140
141
export function activate(opts?: Opts) {
142
if (singleton != null) {
143
L("blocking attempt to run ProcessRenicer twice");
144
return;
145
}
146
singleton = new ProcessRenicer(opts);
147
return singleton;
148
}
149
150
// testing: $ ts-node autorenice.ts
151
async function test() {
152
const pr = activate({ verbose: true });
153
L("activated ProcessRenicer in test mode", pr);
154
await delay(3 * 1000);
155
L("test done");
156
}
157
158
if (require.main === module) {
159
test();
160
}
161
162