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/backend/process-stats.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
import { exec as cp_exec } from "node:child_process";
7
import { readFile, readdir, readlink } from "node:fs/promises";
8
import { join } from "node:path";
9
import { promisify } from "node:util";
10
11
import { reuseInFlight } from "@cocalc/util/reuse-in-flight";
12
import {
13
Cpu,
14
Process,
15
Processes,
16
Stat,
17
State,
18
} from "@cocalc/util/types/project-info/types";
19
import { getLogger } from "./logger";
20
import { envToInt } from "./misc/env-to-number";
21
22
const exec = promisify(cp_exec);
23
24
/**
25
* Return information about all processes (up to a limit or filter) in the environment, where this node.js process runs.
26
* This has been refactored out of project/project-info/server.ts.
27
* It is also used by the backend itself in "execute-code.ts" – to gather info about a spawned async process.
28
*/
29
30
// this is a hard limit on the number of processes we gather, just to
31
// be on the safe side to avoid processing too much data.
32
const LIMIT = envToInt("COCALC_PROJECT_INFO_PROC_LIMIT", 256);
33
34
interface ProcessStatsOpts {
35
procLimit?: number;
36
testing?: boolean;
37
dbg?: Function;
38
}
39
40
export class ProcessStats {
41
private readonly testing: boolean;
42
private readonly procLimit: number;
43
private readonly dbg: Function;
44
private ticks: number;
45
private pagesize: number;
46
private last?: { timestamp: number; processes: Processes };
47
48
constructor(opts?: ProcessStatsOpts) {
49
this.procLimit = opts?.procLimit ?? LIMIT;
50
this.dbg = opts?.dbg ?? getLogger("process-stats").debug;
51
this.init();
52
}
53
54
// this grabs some kernel configuration values we need. they won't change
55
public init = reuseInFlight(async () => {
56
if (this.ticks == null) {
57
const [p_ticks, p_pagesize] = await Promise.all([
58
exec("getconf CLK_TCK"),
59
exec("getconf PAGESIZE"),
60
]);
61
// should be 100, usually
62
this.ticks = parseInt(p_ticks.stdout.trim());
63
// 4096?
64
this.pagesize = parseInt(p_pagesize.stdout.trim());
65
}
66
});
67
68
// the "stat" file contains all the information
69
// this page explains what is what
70
// https://man7.org/linux/man-pages/man5/proc.5.html
71
private async stat(path: string): Promise<Stat> {
72
// all time-values are in seconds
73
const raw = await readFile(path, "utf8");
74
// the "comm" field could contain additional spaces or parents
75
const [i, j] = [raw.indexOf("("), raw.lastIndexOf(")")];
76
const start = raw.slice(0, i - 1).trim();
77
const end = raw.slice(j + 1).trim();
78
const data = `${start} comm ${end}`.split(" ");
79
const get = (idx) => parseInt(data[idx]);
80
// "comm" is now a placeholder to keep indices as they are.
81
// don't forget to account for 0 vs. 1 based indexing.
82
const ret = {
83
ppid: get(3),
84
state: data[2] as State,
85
utime: get(13) / this.ticks, // CPU time spent in user code, measured in clock ticks (#14)
86
stime: get(14) / this.ticks, // CPU time spent in kernel code, measured in clock ticks (#15)
87
cutime: get(15) / this.ticks, // Waited-for children's CPU time spent in user code (in clock ticks) (#16)
88
cstime: get(16) / this.ticks, // Waited-for children's CPU time spent in kernel code (in clock ticks) (#17)
89
starttime: get(21) / this.ticks, // Time when the process started, measured in clock ticks (#22)
90
nice: get(18),
91
num_threads: get(19),
92
mem: { rss: (get(23) * this.pagesize) / (1024 * 1024) }, // MiB
93
};
94
return ret;
95
}
96
97
// delta-time for this and the previous process information
98
private dt(timestamp) {
99
return (timestamp - (this.last?.timestamp ?? 0)) / 1000;
100
}
101
102
// calculate cpu times
103
private cpu({ pid, stat, timestamp }): Cpu {
104
// we are interested in that processes total usage: user + system
105
const total_cpu = stat.utime + stat.stime;
106
// the fallback is chosen in such a way, that it says 0% if we do not have historic data
107
const prev_cpu = this.last?.processes?.[pid]?.cpu.secs ?? total_cpu;
108
const dt = this.dt(timestamp);
109
// how much cpu time was used since last time we checked this process…
110
const pct = 100 * ((total_cpu - prev_cpu) / dt);
111
return { pct: pct, secs: total_cpu };
112
}
113
114
private async cmdline(path: string): Promise<string[]> {
115
// we split at the null-delimiter and filter all empty elements
116
return (await readFile(path, "utf8"))
117
.split("\0")
118
.filter((c) => c.length > 0);
119
}
120
121
// this gathers all the information for a specific process with the given pid
122
private async process({ pid: pid_str, uptime, timestamp }): Promise<Process> {
123
const base = join("/proc", pid_str);
124
const pid = parseInt(pid_str);
125
const fn = (name) => join(base, name);
126
const [cmdline, exe, stat] = await Promise.all([
127
this.cmdline(fn("cmdline")),
128
readlink(fn("exe")),
129
this.stat(fn("stat")),
130
]);
131
return {
132
pid,
133
ppid: stat.ppid,
134
cmdline,
135
exe,
136
stat,
137
cpu: this.cpu({ pid, timestamp, stat }),
138
uptime: uptime - stat.starttime,
139
};
140
}
141
142
// this is how long the underlying machine is running
143
// we need this information, because the processes' start time is
144
// measured in "ticks" since the machine started
145
private async uptime(): Promise<[number, Date]> {
146
// return uptime in secs
147
const out = await readFile("/proc/uptime", "utf8");
148
const uptime = parseFloat(out.split(" ")[0]);
149
const boottime = new Date(new Date().getTime() - 1000 * uptime);
150
return [uptime, boottime];
151
}
152
153
// this is where we gather information about all running processes
154
public async processes(
155
timestamp?: number,
156
): Promise<{ procs: Processes; uptime: number; boottime: Date }> {
157
timestamp ??= new Date().getTime();
158
const [uptime, boottime] = await this.uptime();
159
160
const procs: Processes = {};
161
let n = 0;
162
for (const pid of await readdir("/proc")) {
163
if (!pid.match(/^[0-9]+$/)) continue;
164
try {
165
const proc = await this.process({ pid, uptime, timestamp });
166
procs[proc.pid] = proc;
167
} catch (err) {
168
if (this.testing)
169
this.dbg(`process ${pid} likely vanished – could happen – ${err}`);
170
}
171
// we avoid processing and sending too much data
172
if (n > this.procLimit) {
173
this.dbg(`too many processes – limit of ${this.procLimit} reached!`);
174
break;
175
} else {
176
n += 1;
177
}
178
}
179
this.last = { timestamp, processes: procs };
180
return { procs, uptime, boottime };
181
}
182
}
183
184