agent-yes 1.284.1 → 1.285.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{SUPPORTED_CLIS-DZwCkmvh.js → SUPPORTED_CLIS-CXvLHLG2.js} +3 -3
- package/dist/{SUPPORTED_CLIS-B2Jis2RD.js → SUPPORTED_CLIS-tYZC_KWR.js} +2 -2
- package/dist/{agentShare-BSh14vJq.js → agentShare-0hRw0bQn.js} +2 -2
- package/dist/{callback-nuwvhn5x.js → callback-BN6cqDjb.js} +3 -3
- package/dist/{callback-Cyr2rDek.js → callback-DCaGBxD1.js} +2 -2
- package/dist/cli.js +5 -5
- package/dist/{cmdPs-tzFiiKC5.js → cmdPs-CUkarOK7.js} +123 -18
- package/dist/index.js +2 -2
- package/dist/{notifyDaemon-BBQKDl7Q.js → notifyDaemon-DNTRkaSU.js} +2 -2
- package/dist/{parentPingLoop-CVy-ueZc.js → parentPingLoop-CovRlBFM.js} +2 -2
- package/dist/{rustBinary-CRR9Sr3R.js → rustBinary-CkKsMycJ.js} +2 -2
- package/dist/{rustBinary-C9B5hRAU.js → rustBinary-ydpdBd3-.js} +2 -2
- package/dist/{schedule-CH_evU-N.js → schedule-CgzQNLfP.js} +4 -4
- package/dist/{serve-CC074vGJ.js → serve-nk0r7fxV.js} +17 -17
- package/dist/{setup-DDZMpGsh.js → setup-P7AMLtbV.js} +2 -2
- package/dist/{subcommands-D2iZ_g0A.js → subcommands-B_Lg_zDe.js} +15 -15
- package/dist/{subcommands-DafbEv0e.js → subcommands-SeVXiezY.js} +1 -1
- package/dist/{terminal-C-UzqGmj.js → terminal-DM_kIymf.js} +2 -2
- package/dist/trayApp-BIOUrF8R.js +5 -0
- package/dist/{trayApp-Dylg_sbJ.js → trayApp-CUxqXOmu.js} +2 -2
- package/dist/{ts-BmiGlG1v.js → ts-LgWg13cL.js} +3 -3
- package/dist/{versionChecker-D59qJF9N.js → versionChecker-CbqhEGUc.js} +2 -2
- package/dist/{widget-BENlkcvI.js → widget-DUkLAsSR.js} +3 -3
- package/dist/{ws-PKok408W.js → ws-BAy7xMDI.js} +2 -2
- package/dist/{ws-DvMM1i2j.js → ws-ChkhtUQm.js} +2 -2
- package/package.json +1 -1
- package/ts/cmdPs.spec.ts +126 -1
- package/ts/cmdPs.ts +148 -12
- package/ts/procStats.spec.ts +32 -6
- package/ts/procStats.ts +77 -5
- package/dist/trayApp-D5SzLS6p.js +0 -5
package/ts/cmdPs.ts
CHANGED
|
@@ -21,6 +21,7 @@ import yargs from "yargs";
|
|
|
21
21
|
import { buildAgentForest, flattenForest } from "./agentTree.ts";
|
|
22
22
|
import {
|
|
23
23
|
humanBytes,
|
|
24
|
+
type ProcRow,
|
|
24
25
|
sampleTrees,
|
|
25
26
|
snapshotProcs,
|
|
26
27
|
systemStats,
|
|
@@ -85,8 +86,62 @@ export interface PsRow {
|
|
|
85
86
|
status: string;
|
|
86
87
|
cwd: string;
|
|
87
88
|
repo: string;
|
|
89
|
+
/** This agent's OWN cost, exclusive of any nested agent (see sampleTrees). */
|
|
88
90
|
stats: TreeStats;
|
|
89
91
|
self: boolean;
|
|
92
|
+
/** `├─ `/`└─ ` rails from the agent forest. Empty outside --tree. */
|
|
93
|
+
prefix?: string;
|
|
94
|
+
/** Depth in the agent forest; 0 for roots. */
|
|
95
|
+
depth?: number;
|
|
96
|
+
/**
|
|
97
|
+
* This agent's cost PLUS every descendant agent's. Set only in --tree mode,
|
|
98
|
+
* and only for rows that actually have descendants — a leaf's subtree IS its
|
|
99
|
+
* own row, so repeating the numbers would be noise.
|
|
100
|
+
*/
|
|
101
|
+
subtree?: TreeStats | null;
|
|
102
|
+
/**
|
|
103
|
+
* The OS processes this agent owns (wrapper, the CLI itself, its shells and
|
|
104
|
+
* pty hosts). Populated in --tree so the agent's number can be read as a sum
|
|
105
|
+
* of things you can actually point at.
|
|
106
|
+
*/
|
|
107
|
+
procRows?: ProcRow[];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Roll each row up with its descendants, using the forest `depth` column: a
|
|
112
|
+
* row's descendants are the following rows with a strictly greater depth, up to
|
|
113
|
+
* the next row at or above its own level.
|
|
114
|
+
*
|
|
115
|
+
* Needed because sampleTrees attributes DEEPEST-FIRST and exclusively — a
|
|
116
|
+
* parent's own numbers deliberately omit its subagents, so without this a
|
|
117
|
+
* fan-out parent looks cheap while its real cost sits in rows below it.
|
|
118
|
+
*
|
|
119
|
+
* Returns null for a row with no descendants.
|
|
120
|
+
*/
|
|
121
|
+
export function subtreeTotals(rows: Pick<PsRow, "depth" | "stats">[]): (TreeStats | null)[] {
|
|
122
|
+
return rows.map((r, i) => {
|
|
123
|
+
const depth = r.depth ?? 0;
|
|
124
|
+
// Guard the row's OWN stats the same way its descendants' are guarded
|
|
125
|
+
// below: the sampler can miss an agent that exited mid-window, and one
|
|
126
|
+
// absent entry must not take down the whole table.
|
|
127
|
+
const own = r.stats;
|
|
128
|
+
if (!own) return null;
|
|
129
|
+
let rss = own.rss;
|
|
130
|
+
let cpuPercent = own.cpuPercent;
|
|
131
|
+
let procs = own.procs;
|
|
132
|
+
let found = false;
|
|
133
|
+
for (let j = i + 1; j < rows.length; j++) {
|
|
134
|
+
const d = rows[j]?.depth ?? 0;
|
|
135
|
+
if (d <= depth) break; // left this row's subtree
|
|
136
|
+
const st = rows[j]?.stats;
|
|
137
|
+
if (!st) continue;
|
|
138
|
+
found = true;
|
|
139
|
+
rss += st.rss;
|
|
140
|
+
cpuPercent += st.cpuPercent;
|
|
141
|
+
procs += st.procs;
|
|
142
|
+
}
|
|
143
|
+
return found ? { pid: own.pid, rss, cpuPercent, procs } : null;
|
|
144
|
+
});
|
|
90
145
|
}
|
|
91
146
|
|
|
92
147
|
function pad(s: string, n: number): string {
|
|
@@ -96,15 +151,27 @@ function padStart(s: string, n: number): string {
|
|
|
96
151
|
return s.length >= n ? s : " ".repeat(n - s.length) + s;
|
|
97
152
|
}
|
|
98
153
|
|
|
99
|
-
export function renderTable(
|
|
154
|
+
export function renderTable(
|
|
155
|
+
rows: PsRow[],
|
|
156
|
+
unattributed: TreeStats | null,
|
|
157
|
+
opts: { tree?: boolean } = {},
|
|
158
|
+
): string {
|
|
159
|
+
const tree = opts.tree === true;
|
|
100
160
|
const cells = rows.map((r) => ({
|
|
101
|
-
|
|
161
|
+
// In tree mode the rails live on the PID cell, so the hierarchy reads down
|
|
162
|
+
// the left edge exactly like `ay ls`.
|
|
163
|
+
pid: (tree ? (r.prefix ?? "") : "") + String(r.pid),
|
|
102
164
|
cli: r.cli,
|
|
103
165
|
status: r.status,
|
|
104
166
|
cpu: r.stats.cpuPercent.toFixed(1),
|
|
105
167
|
rss: humanBytes(r.stats.rss),
|
|
106
168
|
procs: String(r.stats.procs),
|
|
107
169
|
repo: r.repo,
|
|
170
|
+
cwd: shortenPath(r.cwd),
|
|
171
|
+
// Σ columns: this agent plus its descendants. Blank for leaves.
|
|
172
|
+
scpu: r.subtree ? r.subtree.cpuPercent.toFixed(1) : "",
|
|
173
|
+
srss: r.subtree ? humanBytes(r.subtree.rss) : "",
|
|
174
|
+
sprocs: r.subtree ? String(r.subtree.procs) : "",
|
|
108
175
|
self: r.self,
|
|
109
176
|
}));
|
|
110
177
|
// The unmanaged row participates in the width pass — "unmanaged" is wider than
|
|
@@ -118,6 +185,10 @@ export function renderTable(rows: PsRow[], unattributed: TreeStats | null): stri
|
|
|
118
185
|
rss: humanBytes(unattributed.rss),
|
|
119
186
|
procs: String(unattributed.procs),
|
|
120
187
|
repo: "-",
|
|
188
|
+
cwd: "-",
|
|
189
|
+
scpu: "",
|
|
190
|
+
srss: "",
|
|
191
|
+
sprocs: "",
|
|
121
192
|
self: false,
|
|
122
193
|
});
|
|
123
194
|
}
|
|
@@ -129,6 +200,9 @@ export function renderTable(rows: PsRow[], unattributed: TreeStats | null): stri
|
|
|
129
200
|
rss: Math.max(5, ...cells.map((c) => c.rss.length)),
|
|
130
201
|
procs: Math.max(5, ...cells.map((c) => c.procs.length)),
|
|
131
202
|
repo: Math.max(4, ...cells.map((c) => c.repo.length)),
|
|
203
|
+
scpu: Math.max(5, ...cells.map((c) => c.scpu.length)),
|
|
204
|
+
srss: Math.max(5, ...cells.map((c) => c.srss.length)),
|
|
205
|
+
sprocs: Math.max(6, ...cells.map((c) => c.sprocs.length)),
|
|
132
206
|
};
|
|
133
207
|
const line = (
|
|
134
208
|
pid: string,
|
|
@@ -138,18 +212,63 @@ export function renderTable(rows: PsRow[], unattributed: TreeStats | null): stri
|
|
|
138
212
|
rss: string,
|
|
139
213
|
procs: string,
|
|
140
214
|
repo: string,
|
|
215
|
+
cwd: string,
|
|
216
|
+
scpu = "",
|
|
217
|
+
srss = "",
|
|
218
|
+
sprocs = "",
|
|
141
219
|
tail = "",
|
|
142
220
|
) =>
|
|
143
221
|
`${pad(pid, w.pid)} ${pad(cli, w.cli)} ${pad(status, w.status)} ` +
|
|
144
222
|
`${padStart(cpu, w.cpu)} ${padStart(rss, w.rss)} ${padStart(procs, w.procs)} ` +
|
|
145
|
-
|
|
223
|
+
// The Σ block only exists in tree mode, where a parent's own numbers exclude
|
|
224
|
+
// its subagents and are therefore misleading on their own.
|
|
225
|
+
(tree
|
|
226
|
+
? `${padStart(scpu, w.scpu)} ${padStart(srss, w.srss)} ${padStart(sprocs, w.sprocs)} `
|
|
227
|
+
: "") +
|
|
228
|
+
// REPO stays as the at-a-glance identity; CWD follows with the full path
|
|
229
|
+
// (home collapsed to ~) because sibling worktrees of one repo share a REPO
|
|
230
|
+
// label and are otherwise indistinguishable here. NOT width-padded: it is
|
|
231
|
+
// the last column, and paths run long enough (120 chars on a real fleet)
|
|
232
|
+
// that padding to the widest would trail ~100 blanks on every other row.
|
|
233
|
+
`${pad(repo, w.repo)} ${cwd}${tail}`;
|
|
146
234
|
|
|
147
|
-
const out: string[] = [
|
|
148
|
-
|
|
235
|
+
const out: string[] = [
|
|
236
|
+
line("PID", "CLI", "STATUS", "CPU%", "RSS", "PROCS", "REPO", "CWD", "ΣCPU%", "ΣRSS", "ΣPROCS"),
|
|
237
|
+
];
|
|
238
|
+
cells.forEach((c, i) => {
|
|
149
239
|
out.push(
|
|
150
|
-
line(
|
|
240
|
+
line(
|
|
241
|
+
c.pid,
|
|
242
|
+
c.cli,
|
|
243
|
+
c.status,
|
|
244
|
+
c.cpu,
|
|
245
|
+
c.rss,
|
|
246
|
+
c.procs,
|
|
247
|
+
c.repo,
|
|
248
|
+
c.cwd,
|
|
249
|
+
c.scpu,
|
|
250
|
+
c.srss,
|
|
251
|
+
c.sprocs,
|
|
252
|
+
c.self ? " ← this session" : "",
|
|
253
|
+
),
|
|
151
254
|
);
|
|
152
|
-
|
|
255
|
+
// The agent's own processes, one indent deeper than its rails. Rendered as
|
|
256
|
+
// a continuation of the row above rather than as table rows: they are a
|
|
257
|
+
// DIFFERENT kind of thing (an OS process, not an agent), and giving them
|
|
258
|
+
// the agent columns would invite reading a shell as a fifth agent.
|
|
259
|
+
const kids = rows[i]?.procRows;
|
|
260
|
+
if (!tree || !kids?.length) return;
|
|
261
|
+
const indent = " ".repeat((rows[i]?.prefix ?? "").length + 2);
|
|
262
|
+
const pw = Math.max(...kids.map((k) => String(k.pid).length));
|
|
263
|
+
const cw = Math.max(...kids.map((k) => k.comm.length));
|
|
264
|
+
for (const k of kids) {
|
|
265
|
+
out.push(
|
|
266
|
+
`${indent}· ${padStart(String(k.pid), pw)} ${pad(k.comm, cw)} ` +
|
|
267
|
+
`${padStart(k.cpuPercent.toFixed(1), 5)} ${padStart(humanBytes(k.rss), 7)}` +
|
|
268
|
+
(k.state === "Z" ? " zombie" : ""),
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
153
272
|
return out.join("\n");
|
|
154
273
|
}
|
|
155
274
|
|
|
@@ -176,7 +295,8 @@ export async function cmdPs(rest: string[]): Promise<number> {
|
|
|
176
295
|
.option("tree", {
|
|
177
296
|
type: "boolean",
|
|
178
297
|
default: false,
|
|
179
|
-
description:
|
|
298
|
+
description:
|
|
299
|
+
"Forest order with ├─ rails, plus Σ columns rolling each agent up with its subagents",
|
|
180
300
|
})
|
|
181
301
|
.option("interval", {
|
|
182
302
|
type: "number",
|
|
@@ -187,6 +307,7 @@ export async function cmdPs(rest: string[]): Promise<number> {
|
|
|
187
307
|
.option("help", { alias: "h", type: "boolean", default: false, description: "Show this help" })
|
|
188
308
|
.example("ay ps", "biggest agents first, with box vitals")
|
|
189
309
|
.example("ay ps --sort cpu", "who is actually burning CPU right now")
|
|
310
|
+
.example("ay ps --tree", "parent>child forest; Σ columns include each agent's subagents")
|
|
190
311
|
.example("ay ps --json", "machine-readable rollup")
|
|
191
312
|
.help(false)
|
|
192
313
|
.version(false)
|
|
@@ -216,9 +337,10 @@ export async function cmdPs(rest: string[]): Promise<number> {
|
|
|
216
337
|
// first-come, so sampling in that order would let a parent absorb a nested
|
|
217
338
|
// agent's whole subtree and render the child as a bogus 0-proc row. Reverse to
|
|
218
339
|
// claim deepest-first, then display in forest order.
|
|
219
|
-
const
|
|
340
|
+
const flat = flattenForest(buildAgentForest(records));
|
|
341
|
+
const ordered = flat.map((r) => r.record);
|
|
220
342
|
const windowMs = Math.max(100, (Number.isFinite(argv.interval) ? argv.interval : 1) * 1000);
|
|
221
|
-
const { trees, unattributed } = await sampleTrees(
|
|
343
|
+
const { trees, unattributed, members } = await sampleTrees(
|
|
222
344
|
[...ordered].reverse().map((r) => r.pid),
|
|
223
345
|
{ windowMs },
|
|
224
346
|
);
|
|
@@ -227,7 +349,7 @@ export async function cmdPs(rest: string[]): Promise<number> {
|
|
|
227
349
|
await Promise.all(ordered.map(async (r) => [r.pid, (await deriveLiveState(r)).state] as const)),
|
|
228
350
|
);
|
|
229
351
|
const selfPid = process.pid;
|
|
230
|
-
const rows: PsRow[] = ordered.map((r) => ({
|
|
352
|
+
const rows: PsRow[] = ordered.map((r, i) => ({
|
|
231
353
|
pid: r.pid,
|
|
232
354
|
cli: r.cli,
|
|
233
355
|
status: states.get(r.pid) ?? r.status,
|
|
@@ -237,8 +359,22 @@ export async function cmdPs(rest: string[]): Promise<number> {
|
|
|
237
359
|
// Mark the tree we are standing in — anything that later grows a kill
|
|
238
360
|
// affordance must not let you shoot the session issuing the command.
|
|
239
361
|
self: trees.get(r.pid) !== undefined && isSelfTree(r.pid, selfPid),
|
|
362
|
+
prefix: flat[i]?.prefix ?? "",
|
|
363
|
+
depth: flat[i]?.depth ?? 0,
|
|
240
364
|
}));
|
|
241
365
|
|
|
366
|
+
// Only meaningful in forest order — sorting by rss would scatter descendants
|
|
367
|
+
// away from their parent and make a rollup describe the wrong span of rows.
|
|
368
|
+
if (argv.tree) {
|
|
369
|
+
const totals = subtreeTotals(rows);
|
|
370
|
+
rows.forEach((r, i) => {
|
|
371
|
+
r.subtree = totals[i] ?? null;
|
|
372
|
+
// Heaviest first: the reason to expand an agent is to find what inside it
|
|
373
|
+
// is costing something, and that is almost never the wrapper.
|
|
374
|
+
r.procRows = [...(members?.get(r.pid) ?? [])].sort((x, y) => y.rss - x.rss);
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
242
378
|
if (!argv.tree) {
|
|
243
379
|
const key = String(argv.sort);
|
|
244
380
|
rows.sort((a, b) => {
|
|
@@ -281,7 +417,7 @@ export async function cmdPs(rest: string[]): Promise<number> {
|
|
|
281
417
|
|
|
282
418
|
const header = formatSystemLine(sys);
|
|
283
419
|
if (header) process.stdout.write(` ${header}\n\n`);
|
|
284
|
-
process.stdout.write(renderTable(rows, unattributed) + "\n");
|
|
420
|
+
process.stdout.write(renderTable(rows, unattributed, { tree: argv.tree === true }) + "\n");
|
|
285
421
|
return 0;
|
|
286
422
|
}
|
|
287
423
|
|
package/ts/procStats.spec.ts
CHANGED
|
@@ -53,6 +53,7 @@ describe("parseProcStat", () => {
|
|
|
53
53
|
expect(s).toEqual({
|
|
54
54
|
pid: 42,
|
|
55
55
|
ppid: 7,
|
|
56
|
+
comm: "claude", // statLine's default
|
|
56
57
|
rss: 10 * 4096,
|
|
57
58
|
cpuSeconds: 2, // (150 + 50) ticks / 100 USER_HZ
|
|
58
59
|
state: "S",
|
|
@@ -97,12 +98,37 @@ describe("parsePsTime", () => {
|
|
|
97
98
|
});
|
|
98
99
|
});
|
|
99
100
|
|
|
101
|
+
describe("comm extraction", () => {
|
|
102
|
+
it("takes the comm from a stat line even when it holds spaces and parens", () => {
|
|
103
|
+
// The killer case: a comm containing ')' is why the field parse anchors on
|
|
104
|
+
// the LAST ')' — and the comm itself must span the FIRST '(' to that same
|
|
105
|
+
// anchor, or a name like "Web Content (1)" gets truncated.
|
|
106
|
+
const s = parseProcStat(9, statLine({ pid: 9, ppid: 1, comm: "Web Content (1)" }));
|
|
107
|
+
expect(s?.comm).toBe("Web Content (1)");
|
|
108
|
+
expect(s?.ppid).toBe(1); // fields after the comm still line up
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("keeps a plain comm intact", () => {
|
|
112
|
+
expect(parseProcStat(9, statLine({ pid: 9, ppid: 1, comm: "bash" }))?.comm).toBe("bash");
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
100
116
|
describe("parsePsTable", () => {
|
|
117
|
+
it("rejoins a comm that contains spaces, since it is the LAST column", () => {
|
|
118
|
+
const procs = parsePsTable(" 10 1 2048 00:10 S /usr/bin/Google Chrome Helper\n");
|
|
119
|
+
expect(procs.get(10)?.comm).toBe("/usr/bin/Google Chrome Helper");
|
|
120
|
+
// The columns before it must be unaffected by those spaces.
|
|
121
|
+
expect(procs.get(10)?.rss).toBe(2048 * 1024);
|
|
122
|
+
expect(procs.get(10)?.state).toBe("S");
|
|
123
|
+
});
|
|
124
|
+
|
|
101
125
|
it("normalizes ps KiB into bytes so both backends agree", () => {
|
|
102
126
|
const procs = parsePsTable(" 10 1 2048 00:10 S\n 11 10 512 00:00 Z\n");
|
|
103
127
|
expect(procs.get(10)).toEqual({
|
|
104
128
|
pid: 10,
|
|
105
129
|
ppid: 1,
|
|
130
|
+
// This fixture predates the comm column; an absent one must read as "".
|
|
131
|
+
comm: "",
|
|
106
132
|
rss: 2048 * 1024,
|
|
107
133
|
cpuSeconds: 10,
|
|
108
134
|
state: "S",
|
|
@@ -129,7 +155,7 @@ describe("descendantsOf", () => {
|
|
|
129
155
|
] as [number, number][]
|
|
130
156
|
).map(([pid, ppid]) => [
|
|
131
157
|
pid,
|
|
132
|
-
{ pid, ppid, rss: 0, cpuSeconds: 0, state: "S", startToken: `t${pid}` },
|
|
158
|
+
{ pid, ppid, comm: "node", rss: 0, cpuSeconds: 0, state: "S", startToken: `t${pid}` },
|
|
133
159
|
]),
|
|
134
160
|
);
|
|
135
161
|
const kids = buildChildIndex(procs);
|
|
@@ -144,8 +170,8 @@ describe("descendantsOf", () => {
|
|
|
144
170
|
|
|
145
171
|
it("terminates on a parent cycle instead of blowing the stack", () => {
|
|
146
172
|
const cyclic = new Map<number, ProcSample>([
|
|
147
|
-
[1, { pid: 1, ppid: 2, rss: 0, cpuSeconds: 0, state: "S", startToken: "t1" }],
|
|
148
|
-
[2, { pid: 2, ppid: 1, rss: 0, cpuSeconds: 0, state: "S", startToken: "t2" }],
|
|
173
|
+
[1, { pid: 1, ppid: 2, comm: "node", rss: 0, cpuSeconds: 0, state: "S", startToken: "t1" }],
|
|
174
|
+
[2, { pid: 2, ppid: 1, comm: "node", rss: 0, cpuSeconds: 0, state: "S", startToken: "t2" }],
|
|
149
175
|
]);
|
|
150
176
|
expect(descendantsOf(1, buildChildIndex(cyclic)).size).toBe(2);
|
|
151
177
|
});
|
|
@@ -319,9 +345,9 @@ describe("parseMeminfo", () => {
|
|
|
319
345
|
describe("systemStats", () => {
|
|
320
346
|
it("reads load/mem and counts zombies from the snapshot", async () => {
|
|
321
347
|
const procs = new Map<number, ProcSample>([
|
|
322
|
-
[1, { pid: 1, ppid: 0, rss: 0, cpuSeconds: 0, state: "S", startToken: "t1" }],
|
|
323
|
-
[2, { pid: 2, ppid: 1, rss: 0, cpuSeconds: 0, state: "Z", startToken: "t2" }],
|
|
324
|
-
[3, { pid: 3, ppid: 1, rss: 0, cpuSeconds: 0, state: "Z", startToken: "t3" }],
|
|
348
|
+
[1, { pid: 1, ppid: 0, comm: "node", rss: 0, cpuSeconds: 0, state: "S", startToken: "t1" }],
|
|
349
|
+
[2, { pid: 2, ppid: 1, comm: "node", rss: 0, cpuSeconds: 0, state: "Z", startToken: "t2" }],
|
|
350
|
+
[3, { pid: 3, ppid: 1, comm: "node", rss: 0, cpuSeconds: 0, state: "Z", startToken: "t3" }],
|
|
325
351
|
]);
|
|
326
352
|
const sys = await systemStats(procs, {
|
|
327
353
|
readSys: async (name) =>
|
package/ts/procStats.ts
CHANGED
|
@@ -34,6 +34,8 @@ const execFileAsync = promisify(execFile);
|
|
|
34
34
|
export interface ProcSample {
|
|
35
35
|
pid: number;
|
|
36
36
|
ppid: number;
|
|
37
|
+
/** Executable name (`bash`, `node`). May be a full path from the ps fallback. */
|
|
38
|
+
comm: string;
|
|
37
39
|
/** Resident set size in bytes. */
|
|
38
40
|
rss: number;
|
|
39
41
|
/** Cumulative CPU (user+sys) in seconds since process start. */
|
|
@@ -113,6 +115,10 @@ export function parseProcStat(
|
|
|
113
115
|
): ProcSample | null {
|
|
114
116
|
const close = stat.lastIndexOf(")");
|
|
115
117
|
if (close < 0) return null;
|
|
118
|
+
// comm is field 2, wrapped in parens and free to contain spaces AND parens —
|
|
119
|
+
// hence first '(' to LAST ')', the same anchor the field parse below uses.
|
|
120
|
+
const open = stat.indexOf("(");
|
|
121
|
+
const comm = open >= 0 && open < close ? stat.slice(open + 1, close) : "";
|
|
116
122
|
const f = stat
|
|
117
123
|
.slice(close + 1)
|
|
118
124
|
.trim()
|
|
@@ -128,6 +134,7 @@ export function parseProcStat(
|
|
|
128
134
|
return {
|
|
129
135
|
pid,
|
|
130
136
|
ppid,
|
|
137
|
+
comm,
|
|
131
138
|
rss: Number.isFinite(rssPages) ? rssPages * pageSizeBytes : 0,
|
|
132
139
|
cpuSeconds: (utime + stime) / USER_HZ,
|
|
133
140
|
state,
|
|
@@ -144,7 +151,7 @@ export interface ProcReaders {
|
|
|
144
151
|
readStat?: (pid: number) => Promise<string | null>;
|
|
145
152
|
/** Whole-file read of /proc/loadavg, /proc/meminfo, … or null. */
|
|
146
153
|
readSys?: (name: string) => Promise<string | null>;
|
|
147
|
-
/** `ps -eo pid=,ppid=,rss=,time=,state=` output, for the non-Linux fallback. */
|
|
154
|
+
/** `ps -eo pid=,ppid=,rss=,time=,state=,comm=` output, for the non-Linux fallback. */
|
|
148
155
|
readPsTable?: () => Promise<string | null>;
|
|
149
156
|
}
|
|
150
157
|
|
|
@@ -183,7 +190,7 @@ export async function defaultReadSys(name: string): Promise<string | null> {
|
|
|
183
190
|
|
|
184
191
|
export async function defaultReadPsTable(): Promise<string | null> {
|
|
185
192
|
try {
|
|
186
|
-
const { stdout } = await execFileAsync("ps", ["-eo", "pid=,ppid=,rss=,time=,state="], {
|
|
193
|
+
const { stdout } = await execFileAsync("ps", ["-eo", "pid=,ppid=,rss=,time=,state=,comm="], {
|
|
187
194
|
encoding: "utf8",
|
|
188
195
|
timeout: 3000,
|
|
189
196
|
maxBuffer: 16 * 1024 * 1024,
|
|
@@ -208,13 +215,20 @@ export function parsePsTime(raw: string): number {
|
|
|
208
215
|
return seconds;
|
|
209
216
|
}
|
|
210
217
|
|
|
211
|
-
/**
|
|
218
|
+
/**
|
|
219
|
+
* Parse the `ps -eo pid=,ppid=,rss=,time=,state=,comm=` fallback table.
|
|
220
|
+
*
|
|
221
|
+
* comm is requested LAST on purpose: it can contain spaces (`Google Chrome
|
|
222
|
+
* Helper`), so anywhere else it would shift every column after it. Being last,
|
|
223
|
+
* everything from field 6 on is simply rejoined.
|
|
224
|
+
*/
|
|
212
225
|
export function parsePsTable(out: string): Map<number, ProcSample> {
|
|
213
226
|
const procs = new Map<number, ProcSample>();
|
|
214
227
|
for (const line of out.split("\n")) {
|
|
215
228
|
const m = line.trim().split(/\s+/);
|
|
216
229
|
if (m.length < 5) continue;
|
|
217
230
|
const [pidS, ppidS, rssS, timeS, state] = m;
|
|
231
|
+
const comm = m.slice(5).join(" ");
|
|
218
232
|
const pid = Number(pidS);
|
|
219
233
|
const ppid = Number(ppidS);
|
|
220
234
|
const rssKib = Number(rssS);
|
|
@@ -222,6 +236,7 @@ export function parsePsTable(out: string): Map<number, ProcSample> {
|
|
|
222
236
|
procs.set(pid, {
|
|
223
237
|
pid,
|
|
224
238
|
ppid,
|
|
239
|
+
comm,
|
|
225
240
|
// ps reports RSS in KiB; /proc gives bytes. Normalize to bytes so both
|
|
226
241
|
// backends feed identical numbers into the rollup.
|
|
227
242
|
rss: Number.isFinite(rssKib) ? rssKib * 1024 : 0,
|
|
@@ -303,6 +318,52 @@ export interface TreeStats {
|
|
|
303
318
|
procs: number;
|
|
304
319
|
}
|
|
305
320
|
|
|
321
|
+
/** One OS process inside an agent's tree, as rendered by `ay ps --tree`. */
|
|
322
|
+
export interface ProcRow {
|
|
323
|
+
pid: number;
|
|
324
|
+
ppid: number;
|
|
325
|
+
/** Executable basename — the full path from `ps -o comm=` is noise in a table. */
|
|
326
|
+
comm: string;
|
|
327
|
+
rss: number;
|
|
328
|
+
/** Percent of ONE core over the sample window. */
|
|
329
|
+
cpuPercent: number;
|
|
330
|
+
state: string;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Per-process detail for one claimed member set, same window and pid-reuse
|
|
335
|
+
* rules as `rollup` (see there for why a newcomer or a reused pid contributes
|
|
336
|
+
* no CPU delta).
|
|
337
|
+
*/
|
|
338
|
+
function memberRows(
|
|
339
|
+
members: Set<number>,
|
|
340
|
+
first: Map<number, ProcSample>,
|
|
341
|
+
second: Map<number, ProcSample>,
|
|
342
|
+
elapsedSeconds: number,
|
|
343
|
+
): ProcRow[] {
|
|
344
|
+
const out: ProcRow[] = [];
|
|
345
|
+
for (const pid of members) {
|
|
346
|
+
const now = second.get(pid);
|
|
347
|
+
if (!now) continue;
|
|
348
|
+
const before = first.get(pid);
|
|
349
|
+
const reused =
|
|
350
|
+
before !== undefined &&
|
|
351
|
+
before.startToken !== "" &&
|
|
352
|
+
now.startToken !== "" &&
|
|
353
|
+
before.startToken !== now.startToken;
|
|
354
|
+
const delta = before && !reused ? Math.max(0, now.cpuSeconds - before.cpuSeconds) : 0;
|
|
355
|
+
out.push({
|
|
356
|
+
pid,
|
|
357
|
+
ppid: now.ppid,
|
|
358
|
+
comm: (now.comm || "").split(/[\\/]+/).filter(Boolean).pop() ?? "",
|
|
359
|
+
rss: now.rss,
|
|
360
|
+
cpuPercent: elapsedSeconds > 0 ? (delta / elapsedSeconds) * 100 : 0,
|
|
361
|
+
state: now.state,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
return out;
|
|
365
|
+
}
|
|
366
|
+
|
|
306
367
|
function rollup(
|
|
307
368
|
root: number,
|
|
308
369
|
members: Set<number>,
|
|
@@ -360,7 +421,12 @@ function rollup(
|
|
|
360
421
|
export async function sampleTrees(
|
|
361
422
|
roots: number[],
|
|
362
423
|
opts: { windowMs?: number; deps?: ProcReaders } = {},
|
|
363
|
-
): Promise<{
|
|
424
|
+
): Promise<{
|
|
425
|
+
trees: Map<number, TreeStats>;
|
|
426
|
+
unattributed: TreeStats;
|
|
427
|
+
/** root pid → the OS processes claimed by that root, for --tree. */
|
|
428
|
+
members: Map<number, ProcRow[]>;
|
|
429
|
+
}> {
|
|
364
430
|
const windowMs = Math.max(100, opts.windowMs ?? 1000);
|
|
365
431
|
// Measure the ACTUAL elapsed time, not the requested window: a loaded box (the
|
|
366
432
|
// exact case this command is for) can oversleep by hundreds of ms, and
|
|
@@ -385,10 +451,12 @@ export async function sampleTrees(
|
|
|
385
451
|
const kids = buildChildIndex(second);
|
|
386
452
|
const claimed = new Set<number>();
|
|
387
453
|
const trees = new Map<number, TreeStats>();
|
|
454
|
+
const memberRowsByRoot = new Map<number, ProcRow[]>();
|
|
388
455
|
for (const root of roots) {
|
|
389
456
|
const members = descendantsOf(root, kids, claimed);
|
|
390
457
|
for (const pid of members) claimed.add(pid);
|
|
391
458
|
trees.set(root, rollup(root, members, first, second, elapsedSeconds));
|
|
459
|
+
memberRowsByRoot.set(root, memberRows(members, first, second, elapsedSeconds));
|
|
392
460
|
}
|
|
393
461
|
|
|
394
462
|
// Everything ay does NOT manage, as one row. This is the answer to "is there
|
|
@@ -396,7 +464,11 @@ export async function sampleTrees(
|
|
|
396
464
|
// box from one quietly hoarding orphans.
|
|
397
465
|
const rest = new Set<number>();
|
|
398
466
|
for (const pid of second.keys()) if (!claimed.has(pid)) rest.add(pid);
|
|
399
|
-
return {
|
|
467
|
+
return {
|
|
468
|
+
trees,
|
|
469
|
+
unattributed: rollup(0, rest, first, second, elapsedSeconds),
|
|
470
|
+
members: memberRowsByRoot,
|
|
471
|
+
};
|
|
400
472
|
}
|
|
401
473
|
|
|
402
474
|
/** Whole-box vitals for the header line. */
|
package/dist/trayApp-D5SzLS6p.js
DELETED