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