perf: include process table usage metrics

This commit is contained in:
777genius 2026-05-29 12:34:13 +03:00
parent 3b0c2ed24b
commit 169ac8bb68
4 changed files with 62 additions and 9 deletions

View file

@ -28,11 +28,36 @@ export interface RuntimeProcessTableRow {
pid: number;
ppid: number;
command: string;
cpuPercent?: number;
rssBytes?: number;
}
export function parseRuntimeProcessTable(output: string): RuntimeProcessTableRow[] {
const rows: RuntimeProcessTableRow[] = [];
for (const line of output.split('\n')) {
const enrichedMatch = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(.*)$/.exec(line);
if (enrichedMatch) {
const pid = Number.parseInt(enrichedMatch[1], 10);
const ppid = Number.parseInt(enrichedMatch[2], 10);
const cpuPercent = Number(enrichedMatch[3]);
const rssKb = Number(enrichedMatch[4]);
const command = enrichedMatch[5]?.trim() ?? '';
if (
Number.isFinite(pid) &&
pid > 0 &&
Number.isFinite(ppid) &&
ppid >= 0 &&
Number.isFinite(cpuPercent) &&
cpuPercent >= 0 &&
Number.isFinite(rssKb) &&
rssKb >= 0 &&
command.length > 0
) {
rows.push({ pid, ppid, command, cpuPercent, rssBytes: Math.round(rssKb * 1024) });
continue;
}
}
const match = /^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(line);
if (!match) continue;
@ -169,7 +194,12 @@ export class TmuxPlatformCommandExecutor {
async listRuntimeProcesses(): Promise<RuntimeProcessTableRow[]> {
const result =
process.platform === 'win32'
? await this.#wslService.execInPreferredDistro(['ps', '-ax', '-o', 'pid=,ppid=,command='])
? await this.#wslService.execInPreferredDistro([
'ps',
'-ax',
'-o',
'pid=,ppid=,pcpu=,rss=,command=',
])
: await this.#execNativePs();
if (result.exitCode !== 0) {
throw new Error(result.stderr || 'Failed to list runtime processes');
@ -251,7 +281,7 @@ export class TmuxPlatformCommandExecutor {
return new Promise((resolve) => {
execFile(
'ps',
['-ax', '-o', 'pid=,ppid=,command='],
['-ax', '-o', 'pid=,ppid=,pcpu=,rss=,command='],
{ env: process.env, timeout: 3_000, maxBuffer: 2 * 1024 * 1024 },
(error, stdout, stderr) => {
const errorCode =

View file

@ -104,7 +104,7 @@ describe('TmuxPlatformCommandExecutor', () => {
setPlatform('win32');
const execInPreferredDistro = vi.fn(async () => ({
exitCode: 0,
stdout: ' 42 1 opencode runtime --team-name demo\n',
stdout: ' 42 1 7.5 128 opencode runtime --team-name demo\n',
stderr: '',
}));
const executor = new TmuxPlatformCommandExecutor(
@ -116,9 +116,20 @@ describe('TmuxPlatformCommandExecutor', () => {
);
await expect(executor.listRuntimeProcesses()).resolves.toEqual([
{ pid: 42, ppid: 1, command: 'opencode runtime --team-name demo' },
{
pid: 42,
ppid: 1,
command: 'opencode runtime --team-name demo',
cpuPercent: 7.5,
rssBytes: 131_072,
},
]);
expect(execInPreferredDistro).toHaveBeenCalledWith([
'ps',
'-ax',
'-o',
'pid=,ppid=,pcpu=,rss=,command=',
]);
expect(execInPreferredDistro).toHaveBeenCalledWith(['ps', '-ax', '-o', 'pid=,ppid=,command=']);
expect(childProcess.execFile).not.toHaveBeenCalled();
});
});

View file

@ -1,6 +1,5 @@
import { describe, expect, it } from 'vitest';
import { parseRuntimeProcessTable } from '@features/tmux-installer/main';
import { describe, expect, it } from 'vitest';
describe('parseRuntimeProcessTable', () => {
it('parses pid, ppid and command rows', () => {
@ -12,6 +11,15 @@ describe('parseRuntimeProcessTable', () => {
]);
});
it('parses optional cpu and rss columns', () => {
expect(
parseRuntimeProcessTable(' 10 1 3.5 120000 /bin/zsh\n 11 10 0.1 42 node demo')
).toEqual([
{ pid: 10, ppid: 1, command: '/bin/zsh', cpuPercent: 3.5, rssBytes: 122_880_000 },
{ pid: 11, ppid: 10, command: 'node demo', cpuPercent: 0.1, rssBytes: 43_008 },
]);
});
it('skips malformed rows', () => {
expect(parseRuntimeProcessTable('bad\n 0 1 nope\n 12 0 /bin/node')).toEqual([
{ pid: 12, ppid: 0, command: '/bin/node' },

View file

@ -620,6 +620,9 @@ type TeamProvisioningServicePrivateHarness = {
applyProcessBootstrapTransportOverlay: (
input: Record<string, unknown>
) => Record<string, unknown>;
readProcessUsageStatsByPid: (
pids: readonly number[]
) => Promise<Map<number, { rssBytes?: number; cpuPercent?: number }>>;
};
function privateHarness(svc: TeamProvisioningService): TeamProvisioningServicePrivateHarness {
@ -4747,8 +4750,9 @@ describe('TeamProvisioningService', () => {
};
vi.mocked(pidusage).mockResolvedValueOnce(usageByPid);
const first = await (svc as any).readProcessUsageStatsByPid([111]);
const second = await (svc as any).readProcessUsageStatsByPid([111]);
const harness = privateHarness(svc);
const first = await harness.readProcessUsageStatsByPid([111]);
const second = await harness.readProcessUsageStatsByPid([111]);
expect(pidusage).toHaveBeenCalledTimes(1);
expect(pidusage).toHaveBeenCalledWith([111], EXPECTED_RUNTIME_PIDUSAGE_OPTIONS);