137 lines
3.7 KiB
TypeScript
137 lines
3.7 KiB
TypeScript
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import { execSync } from 'child_process';
|
|
|
|
// CPU core count (cached at module load)
|
|
const cpuCoreCount = typeof os.availableParallelism === 'function'
|
|
? os.availableParallelism()
|
|
: os.cpus().length;
|
|
|
|
// Cached system constants
|
|
let clkTck: number | null = null;
|
|
let pageSize: number | null = null;
|
|
|
|
function getClkTck(): number {
|
|
if (clkTck === null) {
|
|
try {
|
|
clkTck = parseInt(execSync('getconf CLK_TCK', { encoding: 'utf8' }).trim(), 10);
|
|
} catch {
|
|
clkTck = 100; // standard Linux default
|
|
}
|
|
}
|
|
return clkTck;
|
|
}
|
|
|
|
function getPageSize(): number {
|
|
if (pageSize === null) {
|
|
try {
|
|
pageSize = parseInt(execSync('getconf PAGESIZE', { encoding: 'utf8' }).trim(), 10);
|
|
} catch {
|
|
pageSize = 4096; // standard Linux default
|
|
}
|
|
}
|
|
return pageSize;
|
|
}
|
|
|
|
// History for CPU delta tracking
|
|
interface ISnapshot {
|
|
utime: number;
|
|
stime: number;
|
|
timestamp: number; // hrtime in seconds
|
|
}
|
|
|
|
const history = new Map<number, ISnapshot>();
|
|
|
|
function readProcStat(pid: number): { utime: number; stime: number; rss: number } | null {
|
|
try {
|
|
const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
// Format: pid (comm) state ppid ... fields
|
|
// utime is field 14, stime is field 15, rss is field 24 (1-indexed)
|
|
const closeParenIdx = stat.lastIndexOf(')');
|
|
if (closeParenIdx === -1) return null;
|
|
const afterComm = stat.slice(closeParenIdx + 2);
|
|
const fields = afterComm.split(' ');
|
|
// fields[0] = state (field 3), so utime = fields[11] (field 14), stime = fields[12] (field 15), rss = fields[21] (field 24)
|
|
const utime = parseInt(fields[11], 10);
|
|
const stime = parseInt(fields[12], 10);
|
|
const rss = parseInt(fields[21], 10);
|
|
return { utime, stime, rss };
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function hrtimeSeconds(): number {
|
|
const [sec, nsec] = process.hrtime();
|
|
return sec + nsec / 1e9;
|
|
}
|
|
|
|
export interface IPidUsageResult {
|
|
cpu: number; // raw per-core CPU% (can exceed 100%)
|
|
cpuCoreCount: number; // number of CPU cores on the machine
|
|
cpuNormalizedPercent: number; // cpu / coreCount — 0-100% of total machine
|
|
memory: number; // RSS in bytes
|
|
}
|
|
|
|
/**
|
|
* Get CPU percentage and memory usage for the given PIDs.
|
|
* CPU% is calculated as a delta between successive calls.
|
|
*/
|
|
export async function getPidUsage(
|
|
pids: number[]
|
|
): Promise<Record<number, IPidUsageResult>> {
|
|
const tck = getClkTck();
|
|
const ps = getPageSize();
|
|
const result: Record<number, IPidUsageResult> = {};
|
|
|
|
for (const pid of pids) {
|
|
const stat = readProcStat(pid);
|
|
if (!stat) {
|
|
continue;
|
|
}
|
|
|
|
const now = hrtimeSeconds();
|
|
const totalTicks = stat.utime + stat.stime;
|
|
const memoryBytes = stat.rss * ps;
|
|
|
|
const prev = history.get(pid);
|
|
if (prev) {
|
|
const elapsedSeconds = now - prev.timestamp;
|
|
const ticksDelta = totalTicks - (prev.utime + prev.stime);
|
|
const cpuSeconds = ticksDelta / tck;
|
|
const cpuPercent = elapsedSeconds > 0 ? (cpuSeconds / elapsedSeconds) * 100 : 0;
|
|
|
|
result[pid] = {
|
|
cpu: cpuPercent,
|
|
cpuCoreCount,
|
|
cpuNormalizedPercent: cpuPercent / cpuCoreCount,
|
|
memory: memoryBytes,
|
|
};
|
|
} else {
|
|
// First call for this PID — no delta available, report 0% cpu
|
|
result[pid] = {
|
|
cpu: 0,
|
|
cpuCoreCount,
|
|
cpuNormalizedPercent: 0,
|
|
memory: memoryBytes,
|
|
};
|
|
}
|
|
|
|
// Update history
|
|
history.set(pid, {
|
|
utime: stat.utime,
|
|
stime: stat.stime,
|
|
timestamp: now,
|
|
});
|
|
}
|
|
|
|
// Prune history entries for PIDs no longer in the requested set
|
|
for (const histPid of history.keys()) {
|
|
if (!pids.includes(histPid)) {
|
|
history.delete(histPid);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|