@jarred/cpu
Version:
Process resource usage measurement utility
339 lines (289 loc) • 11 kB
text/typescript
/**
* @jarred/cpu - Process resource usage measurement utility
*
* A tool that runs another process and reports detailed resource usage statistics
*/
import pkg from "./package.json";
// Import the os module to get system information
import { totalmem } from "node:os";
// Get the total system memory in bytes
const TOTAL_SYSTEM_MEMORY = totalmem();
let verbose = false;
// Check if colors are supported
const useColors = Bun.enableANSIColors;
// ANSI color codes (only used if colors are supported)
const RESET = useColors ? "\x1b[0m" : "";
const BOLD = useColors ? "\x1b[1m" : "";
const DIM = useColors ? "\x1b[2m" : "";
const RED = useColors ? "\x1b[31m" : "";
const GREEN = useColors ? "\x1b[32m" : "";
const YELLOW = useColors ? "\x1b[33m" : "";
const BLUE = useColors ? "\x1b[34m" : "";
const MAGENTA = useColors ? "\x1b[35m" : "";
const CYAN = useColors ? "\x1b[36m" : "";
const GRAY = useColors ? "\x1b[90m" : "";
// Handle help and version flags
if (
process.argv.length < 3 ||
process.argv[2] === "-h" ||
process.argv[2] === "--help"
) {
console.log(`
${BOLD}${CYAN}cpu${RESET} - ${BLUE}Process resource usage measurement utility${RESET} ${GREEN}v${pkg.version}${RESET}
${BOLD}Usage:${RESET} ${CYAN}cpu${RESET} <command> [args...]
${DIM}Measures and displays resource usage statistics of the specified command.${RESET}
${BOLD}Options:${RESET}
${YELLOW}-v, --verbose${RESET} Enable detailed metrics output
${YELLOW}-h, --help${RESET} Display this help message
${YELLOW}--version${RESET} Display version information
${BOLD}Examples:${RESET}
${CYAN}cpu${RESET} ${MAGENTA}sleep 1${RESET}
${CYAN}cpu${RESET} ${MAGENTA}bun -e "console.log('Hello')"${RESET}
${CYAN}cpu${RESET} ${MAGENTA}node -e "console.log('Hello')"${RESET}
${CYAN}cpu${RESET} ${MAGENTA}./my-script.sh${RESET}
`);
process.exit(0);
}
if (process.argv[2] === "--version") {
console.log(`cpu v${pkg.version}`);
process.exit(0);
}
if (process.argv[2] === "-v" || process.argv[2] === "--verbose") {
verbose = true;
process.argv.splice(2, 1);
}
// Extract the command and arguments
const command = process.argv[2];
const args = process.argv.slice(3);
// Helper function to format bytes to a human-readable format
function formatBytes(bytes: number): string {
const formatter = new Intl.NumberFormat("en-US", {
style: "unit",
unit:
bytes < 1024
? "byte"
: bytes < 1024 * 1024
? "kilobyte"
: bytes < 1024 * 1024 * 1024
? "megabyte"
: "gigabyte",
unitDisplay: "short",
maximumFractionDigits: 2,
});
const value =
bytes < 1024
? bytes
: bytes < 1024 * 1024
? bytes / 1024
: bytes < 1024 * 1024 * 1024
? bytes / (1024 * 1024)
: bytes / (1024 * 1024 * 1024);
return formatter.format(value);
}
let printed = false;
async function print() {
if (printed) return;
printed = true;
const endTime = performance.now();
// Calculate execution time in seconds
const execTimeMs = endTime - startTime;
const execTimeSeconds = execTimeMs / 1000;
// Get resource usage metrics
const resourceUsage = result.resourceUsage()!;
// Calculate CPU percentage (user + system time divided by total execution time)
const cpuTimeUser = Number(resourceUsage.cpuTime.user);
const cpuTimeSystem = Number(resourceUsage.cpuTime.system);
const cpuTimeMs = (cpuTimeUser + cpuTimeSystem) / 1000;
// Get memory usage and format appropriately (MB or GB)
const memoryUsageBytes = resourceUsage.maxRSS;
const memoryUsageMB = memoryUsageBytes / (1024 * 1024);
const memoryUsageGB = memoryUsageMB / 1024;
// Calculate memory usage as percentage of total system memory
const memoryUsagePercentage = (memoryUsageBytes / TOTAL_SYSTEM_MEMORY) * 100;
// Format memory display with appropriate unit
let memoryDisplay: string;
if (memoryUsageMB < 1024) {
// Display in MB if less than 1GB, rounding to whole number
memoryDisplay = `${Math.round(memoryUsageMB)} MB`;
} else {
// Display in GB if 1GB or more, with 1 decimal place
memoryDisplay = `${memoryUsageGB.toFixed(1)} GB`;
}
// Get more detailed metrics
const voluntaryContextSwitches = resourceUsage.contextSwitches.voluntary;
const involuntaryContextSwitches = resourceUsage.contextSwitches.involuntary;
const totalContextSwitches =
voluntaryContextSwitches + involuntaryContextSwitches;
// Format the user and system time in seconds with appropriate precision
const userSeconds = (cpuTimeUser / 1000000).toFixed(2);
const systemSeconds = (cpuTimeSystem / 1000000).toFixed(2);
// Format memory with appropriate unit
let memValue: string;
let memUnit: string;
if (memoryUsageMB >= 1024) {
// Display in GB if 1GB or more
memValue = memoryUsageGB.toFixed(1);
memUnit = "GB";
} else if (memoryUsageMB >= 1) {
// Display in MB if 1MB or more
memValue = Math.round(memoryUsageMB).toString();
memUnit = "MB";
} else {
// Display in KB if less than 1MB
memValue = Math.round(memoryUsageBytes / 1024).toString();
memUnit = "KB";
}
// Color for memory based on amount and usage percentage
let memColor = "";
// Only use green for very small memory usage (less than 100MB)
if (memoryUsageMB < 100) memColor = GREEN;
// Use warning colors for high percentages of total memory
if (memoryUsagePercentage > 50) memColor = YELLOW;
if (memoryUsagePercentage > 80) memColor = RED;
// Get the components of our output to calculate proper spacing
const timeStr = `${execTimeSeconds.toFixed(2)}s`;
const userStr = `${userSeconds}s user`;
const sysStr = `${systemSeconds}s sys`;
const memStr = `${memValue} ${memUnit} mem`;
// Define column padding for better readability
const paddingSize = 2;
const padding = " ".repeat(paddingSize);
// Measure the actual width of each component including formatting
// (color codes don't contribute to visual width)
const timeTextWidth = Bun.stringWidth(timeStr);
const userTextWidth = Bun.stringWidth(userStr);
const sysTextWidth = Bun.stringWidth(sysStr);
const memTextWidth = Bun.stringWidth(memStr);
// Calculate total column widths including padding
const timeColWidth = timeTextWidth + paddingSize * 2;
const userColWidth = userTextWidth + paddingSize * 2;
const sysColWidth = sysTextWidth + paddingSize * 2;
const memColWidth = memTextWidth + paddingSize * 2;
// Use box-drawing unicode characters for a connected look
const topLeftCorner = `${DIM}┌${RESET}`; // U+250C (Box Drawings Light Down and Right)
const topMiddleT = `${DIM}┬${RESET}`; // U+252C (Box Drawings Light Down and Horizontal)
const topRightCorner = `${DIM}┐${RESET}`; // U+2510 (Box Drawings Light Down and Left)
const botLeftCorner = `${DIM}└${RESET}`; // U+2514 (Box Drawings Light Up and Right)
const botMiddleT = `${DIM}┴${RESET}`; // U+2534 (Box Drawings Light Up and Horizontal)
const botRightCorner = `${DIM}┘${RESET}`; // U+2518 (Box Drawings Light Up and Left)
const horizontalLine = `${DIM}─${RESET}`; // U+2500 (Box Drawings Light Horizontal)
const verticalLine = `${DIM}│${RESET}`; // U+2502 (Box Drawings Light Vertical)
// Create the top border with connecting points for the vertical separators
const topBorder =
topLeftCorner +
horizontalLine.repeat(timeColWidth) +
topMiddleT +
horizontalLine.repeat(userColWidth) +
topMiddleT +
horizontalLine.repeat(sysColWidth) +
topMiddleT +
horizontalLine.repeat(memColWidth) +
topRightCorner;
// Create the bottom border for verbose mode
const bottomBorder =
botLeftCorner +
horizontalLine.repeat(timeColWidth) +
botMiddleT +
horizontalLine.repeat(userColWidth) +
botMiddleT +
horizontalLine.repeat(sysColWidth) +
botMiddleT +
horizontalLine.repeat(memColWidth) +
botRightCorner;
// Create the simple summary output with time and memory metrics
console.log(topBorder);
console.log(
`${verticalLine}${padding}${BOLD}${timeStr}${RESET}${padding}${verticalLine}` +
`${padding}${BLUE}${userStr}${RESET}${padding}${verticalLine}` +
`${padding}${MAGENTA}${sysStr}${RESET}${padding}${verticalLine}` +
`${padding}${memColor}${memStr}${RESET}${padding}${verticalLine}`
);
// Add bottom border if not in verbose mode
if (!verbose) {
console.log(bottomBorder);
}
// Add verbose verbose is set
if (verbose) {
console.log(`\n${BOLD}Detailed metrics:${RESET}`);
// Time metrics
console.log(` ${CYAN}Time:${RESET}`);
console.log(
` Total: ${BOLD}${execTimeSeconds.toFixed(3)}s${RESET}`
);
console.log(
` User CPU: ${BLUE}${userSeconds}s${RESET} ${DIM}(${(
(Number(userSeconds) / execTimeSeconds) *
100
).toFixed(1)}% of total)${RESET}`
);
console.log(
` System CPU: ${MAGENTA}${systemSeconds}s${RESET} ${DIM}(${(
(Number(systemSeconds) / execTimeSeconds) *
100
).toFixed(1)}% of total)${RESET}`
);
// Memory metrics
let memPercentStr = memoryUsagePercentage.toFixed(2);
if (memoryUsagePercentage > 50)
memPercentStr = `${YELLOW}${memPercentStr}${RESET}`;
if (memoryUsagePercentage > 80)
memPercentStr = `${RED}${memPercentStr}${RESET}`;
console.log(` ${CYAN}Memory:${RESET}`);
console.log(` Peak: ${memColor}${memValue} ${memUnit}${RESET}`);
console.log(
` System: ${formatBytes(
TOTAL_SYSTEM_MEMORY
)} total ${DIM}(${memPercentStr}% used)${RESET}`
);
// Process metrics
console.log(` ${CYAN}Process:${RESET}`);
console.log(
` Context switches: ${totalContextSwitches} ${DIM}(voluntary: ${voluntaryContextSwitches}, involuntary: ${involuntaryContextSwitches})${RESET}`
);
console.log(
` IO operations: ${DIM}in: ${resourceUsage.ops.in}, out: ${resourceUsage.ops.out}${RESET}`
);
console.log(
` Exit code: ${
result.exitCode === 0
? `${GREEN}0${RESET}`
: `${RED}${result.exitCode}${RESET}`
}`
);
// Add bottom border after verbose output
console.log(bottomBorder);
}
// Pass through the command's exit code
process.exit(result.exitCode);
}
process.on("SIGINT", async () => {
if (result) {
console.log();
try {
result.kill();
} catch (e) {}
}
await Promise.race([result.exited, Bun.sleep(10)]);
if (result?.resourceUsage()) {
await print();
}
process.exit(result.exitCode);
});
process.once("beforeExit", async () => {
if (result) {
console.log();
result?.kill?.();
}
await print();
});
// Execute the process and measure its resource usage
var startTime = performance.now();
var result = Bun.spawn({
stdout: "inherit",
stderr: "inherit",
stdin: "inherit",
cmd: [command, ...args],
});
await result.exited;
await print();