Files
smartmetrics/ts/smartmetrics.pidusage.ts

130 lines
3.6 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,
});
}
return result;
}