Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/backend/process-stats-scan.ts
6569 views
1
/*
2
* This file is part of CoCalc: Copyright © 2020–2026 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { readdirSync, readFileSync, readlinkSync } from "node:fs";
7
import { join } from "node:path";
8
import LRUCache from "lru-cache";
9
10
import type {
11
Cpu,
12
Process,
13
Processes,
14
Stat,
15
State,
16
} from "@cocalc/util/types/project-info/types";
17
import { getLogger } from "./logger";
18
19
export interface ScanProcessesSyncInput {
20
timestamp?: number;
21
sampleKey: string;
22
procLimit: number;
23
ticks: number;
24
pagesize: number;
25
}
26
27
export interface ScanProcessesSyncResult {
28
procs: Processes;
29
uptime: number;
30
boottimeMs: number;
31
}
32
33
const lastByKey = new LRUCache<
34
string,
35
{ timestamp: number; cpuByPid: Map<number, number> }
36
>({
37
max: 8,
38
});
39
const dbg = getLogger("process-stats").debug;
40
41
function parseStat(path: string, ticks: number, pagesize: number): Stat {
42
// all time-values are in seconds
43
const raw = readFileSync(path, "utf8");
44
// the "comm" field could contain additional spaces or parents
45
const [i, j] = [raw.indexOf("("), raw.lastIndexOf(")")];
46
const start = raw.slice(0, i - 1).trim();
47
const end = raw.slice(j + 1).trim();
48
const data = `${start} comm ${end}`.split(" ");
49
const get = (idx: number) => parseInt(data[idx], 10);
50
// "comm" is now a placeholder to keep indices as they are.
51
// don't forget to account for 0 vs. 1 based indexing.
52
return {
53
ppid: get(3),
54
state: data[2] as State,
55
utime: get(13) / ticks, // CPU time spent in user code, measured in clock ticks (#14)
56
stime: get(14) / ticks, // CPU time spent in kernel code, measured in clock ticks (#15)
57
cutime: get(15) / ticks, // Waited-for children's CPU time spent in user code (in clock ticks) (#16)
58
cstime: get(16) / ticks, // Waited-for children's CPU time spent in kernel code (in clock ticks) (#17)
59
starttime: get(21) / ticks, // Time when the process started, measured in clock ticks (#22)
60
nice: get(18),
61
num_threads: get(19),
62
mem: { rss: (get(23) * pagesize) / (1024 * 1024) }, // MiB
63
};
64
}
65
66
function getCmdline(path: string): string[] {
67
// we split at the null-delimiter and filter all empty elements
68
return readFileSync(path, "utf8")
69
.split("\0")
70
.filter((c) => c.length > 0);
71
}
72
73
function getCpu({
74
pid,
75
stat,
76
timestamp,
77
lastCpuByPid,
78
lastTimestamp,
79
}: {
80
pid: number;
81
stat: Stat;
82
timestamp: number;
83
lastCpuByPid?: Map<number, number>;
84
lastTimestamp?: number;
85
}): Cpu {
86
// we are interested in that processes total usage: user + system
87
const totalCpu = stat.utime + stat.stime;
88
// the fallback is chosen in such a way, that it says 0% if we do not have historic data
89
const prevCpu = lastCpuByPid?.get(pid) ?? totalCpu;
90
const dt = (timestamp - (lastTimestamp ?? 0)) / 1000;
91
// how much cpu time was used since last time we checked this process…
92
const pct = dt > 0 ? 100 * ((totalCpu - prevCpu) / dt) : 0;
93
return { pct: pct, secs: totalCpu };
94
}
95
96
function readUptime(timestamp: number): [number, number] {
97
const out = readFileSync("/proc/uptime", "utf8");
98
const uptime = parseFloat(out.split(" ")[0]);
99
const boottimeMs = timestamp - 1000 * uptime;
100
return [uptime, boottimeMs];
101
}
102
103
export function scanProcessesSync({
104
timestamp,
105
sampleKey,
106
procLimit,
107
ticks,
108
pagesize,
109
}: ScanProcessesSyncInput): ScanProcessesSyncResult {
110
const sampleTimestamp = timestamp ?? Date.now();
111
const [uptime, boottimeMs] = readUptime(sampleTimestamp);
112
const last = lastByKey.get(sampleKey);
113
const cpuByPid = new Map<number, number>();
114
115
const procs: Processes = {};
116
let pids = readdirSync("/proc").filter((pid) => pid.match(/^[0-9]+$/));
117
if (pids.length > procLimit) {
118
dbg(`too many processes (${pids.length}), truncating scan to ${procLimit}`);
119
pids = pids.slice(0, procLimit);
120
}
121
122
for (const pidStr of pids) {
123
const base = join("/proc", pidStr);
124
const fn = (name: string) => join(base, name);
125
try {
126
const pid = parseInt(pidStr, 10);
127
const stat = parseStat(fn("stat"), ticks, pagesize);
128
const proc: Process = {
129
pid,
130
ppid: stat.ppid,
131
cmdline: getCmdline(fn("cmdline")),
132
exe: readlinkSync(fn("exe")),
133
stat,
134
cpu: getCpu({
135
pid,
136
timestamp: sampleTimestamp,
137
stat,
138
lastCpuByPid: last?.cpuByPid,
139
lastTimestamp: last?.timestamp,
140
}),
141
uptime: uptime - stat.starttime,
142
};
143
procs[proc.pid] = proc;
144
cpuByPid.set(proc.pid, proc.cpu.secs);
145
} catch {
146
// Processes can vanish while scanning /proc, which is expected.
147
}
148
}
149
150
lastByKey.set(sampleKey, { timestamp: sampleTimestamp, cpuByPid });
151
152
return {
153
procs,
154
uptime,
155
boottimeMs,
156
};
157
}
158
159