perf: include process table usage metrics
This commit is contained in:
parent
3b0c2ed24b
commit
169ac8bb68
4 changed files with 62 additions and 9 deletions
|
|
@ -28,11 +28,36 @@ export interface RuntimeProcessTableRow {
|
||||||
pid: number;
|
pid: number;
|
||||||
ppid: number;
|
ppid: number;
|
||||||
command: string;
|
command: string;
|
||||||
|
cpuPercent?: number;
|
||||||
|
rssBytes?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseRuntimeProcessTable(output: string): RuntimeProcessTableRow[] {
|
export function parseRuntimeProcessTable(output: string): RuntimeProcessTableRow[] {
|
||||||
const rows: RuntimeProcessTableRow[] = [];
|
const rows: RuntimeProcessTableRow[] = [];
|
||||||
for (const line of output.split('\n')) {
|
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);
|
const match = /^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(line);
|
||||||
if (!match) continue;
|
if (!match) continue;
|
||||||
|
|
||||||
|
|
@ -169,7 +194,12 @@ export class TmuxPlatformCommandExecutor {
|
||||||
async listRuntimeProcesses(): Promise<RuntimeProcessTableRow[]> {
|
async listRuntimeProcesses(): Promise<RuntimeProcessTableRow[]> {
|
||||||
const result =
|
const result =
|
||||||
process.platform === 'win32'
|
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();
|
: await this.#execNativePs();
|
||||||
if (result.exitCode !== 0) {
|
if (result.exitCode !== 0) {
|
||||||
throw new Error(result.stderr || 'Failed to list runtime processes');
|
throw new Error(result.stderr || 'Failed to list runtime processes');
|
||||||
|
|
@ -251,7 +281,7 @@ export class TmuxPlatformCommandExecutor {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
execFile(
|
execFile(
|
||||||
'ps',
|
'ps',
|
||||||
['-ax', '-o', 'pid=,ppid=,command='],
|
['-ax', '-o', 'pid=,ppid=,pcpu=,rss=,command='],
|
||||||
{ env: process.env, timeout: 3_000, maxBuffer: 2 * 1024 * 1024 },
|
{ env: process.env, timeout: 3_000, maxBuffer: 2 * 1024 * 1024 },
|
||||||
(error, stdout, stderr) => {
|
(error, stdout, stderr) => {
|
||||||
const errorCode =
|
const errorCode =
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ describe('TmuxPlatformCommandExecutor', () => {
|
||||||
setPlatform('win32');
|
setPlatform('win32');
|
||||||
const execInPreferredDistro = vi.fn(async () => ({
|
const execInPreferredDistro = vi.fn(async () => ({
|
||||||
exitCode: 0,
|
exitCode: 0,
|
||||||
stdout: ' 42 1 opencode runtime --team-name demo\n',
|
stdout: ' 42 1 7.5 128 opencode runtime --team-name demo\n',
|
||||||
stderr: '',
|
stderr: '',
|
||||||
}));
|
}));
|
||||||
const executor = new TmuxPlatformCommandExecutor(
|
const executor = new TmuxPlatformCommandExecutor(
|
||||||
|
|
@ -116,9 +116,20 @@ describe('TmuxPlatformCommandExecutor', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
await expect(executor.listRuntimeProcesses()).resolves.toEqual([
|
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();
|
expect(childProcess.execFile).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
|
|
||||||
import { parseRuntimeProcessTable } from '@features/tmux-installer/main';
|
import { parseRuntimeProcessTable } from '@features/tmux-installer/main';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
describe('parseRuntimeProcessTable', () => {
|
describe('parseRuntimeProcessTable', () => {
|
||||||
it('parses pid, ppid and command rows', () => {
|
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', () => {
|
it('skips malformed rows', () => {
|
||||||
expect(parseRuntimeProcessTable('bad\n 0 1 nope\n 12 0 /bin/node')).toEqual([
|
expect(parseRuntimeProcessTable('bad\n 0 1 nope\n 12 0 /bin/node')).toEqual([
|
||||||
{ pid: 12, ppid: 0, command: '/bin/node' },
|
{ pid: 12, ppid: 0, command: '/bin/node' },
|
||||||
|
|
|
||||||
|
|
@ -620,6 +620,9 @@ type TeamProvisioningServicePrivateHarness = {
|
||||||
applyProcessBootstrapTransportOverlay: (
|
applyProcessBootstrapTransportOverlay: (
|
||||||
input: Record<string, unknown>
|
input: Record<string, unknown>
|
||||||
) => Record<string, unknown>;
|
) => Record<string, unknown>;
|
||||||
|
readProcessUsageStatsByPid: (
|
||||||
|
pids: readonly number[]
|
||||||
|
) => Promise<Map<number, { rssBytes?: number; cpuPercent?: number }>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function privateHarness(svc: TeamProvisioningService): TeamProvisioningServicePrivateHarness {
|
function privateHarness(svc: TeamProvisioningService): TeamProvisioningServicePrivateHarness {
|
||||||
|
|
@ -4747,8 +4750,9 @@ describe('TeamProvisioningService', () => {
|
||||||
};
|
};
|
||||||
vi.mocked(pidusage).mockResolvedValueOnce(usageByPid);
|
vi.mocked(pidusage).mockResolvedValueOnce(usageByPid);
|
||||||
|
|
||||||
const first = await (svc as any).readProcessUsageStatsByPid([111]);
|
const harness = privateHarness(svc);
|
||||||
const second = await (svc as any).readProcessUsageStatsByPid([111]);
|
const first = await harness.readProcessUsageStatsByPid([111]);
|
||||||
|
const second = await harness.readProcessUsageStatsByPid([111]);
|
||||||
|
|
||||||
expect(pidusage).toHaveBeenCalledTimes(1);
|
expect(pidusage).toHaveBeenCalledTimes(1);
|
||||||
expect(pidusage).toHaveBeenCalledWith([111], EXPECTED_RUNTIME_PIDUSAGE_OPTIONS);
|
expect(pidusage).toHaveBeenCalledWith([111], EXPECTED_RUNTIME_PIDUSAGE_OPTIONS);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue