fullcourtdefense-cli 1.26.1 → 1.26.2
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/commands/daemon.js +5 -1
- package/dist/commands/doctor.js +1 -1
- package/dist/perfSnapshot.d.ts +14 -3
- package/dist/perfSnapshot.js +56 -22
- package/dist/version.json +1 -1
- package/package.json +1 -1
package/dist/commands/daemon.js
CHANGED
|
@@ -942,7 +942,11 @@ async function runDaemon(args, config) {
|
|
|
942
942
|
bundleState = { mode: bundle.mode, source: bundle.source, policyHash: bundle.policyHash };
|
|
943
943
|
}
|
|
944
944
|
catch { /* snapshot still useful without mode context */ }
|
|
945
|
-
|
|
945
|
+
// Async collection — the benchmark spawns real hook children that ask
|
|
946
|
+
// THIS daemon for IPC verdicts, so the event loop must stay free or
|
|
947
|
+
// the snapshot measures its own blockage (2.5s IPC timeout per hook)
|
|
948
|
+
// instead of the true per-event cost.
|
|
949
|
+
const snapshot = await (0, perfSnapshot_1.collectPerfSnapshot)({ cliVersion: cliVersion(), ...bundleState });
|
|
946
950
|
const uploaded = await uploadDiagnostics({ perfSnapshot: snapshot });
|
|
947
951
|
if (!uploaded)
|
|
948
952
|
throw new Error('Perf snapshot could not be uploaded (network or backend rejection).');
|
package/dist/commands/doctor.js
CHANGED
|
@@ -105,7 +105,7 @@ async function perfCheck(apiUrl, config) {
|
|
|
105
105
|
else {
|
|
106
106
|
console.log('mode: not enrolled (local defaults, monitor-first)');
|
|
107
107
|
}
|
|
108
|
-
const snapshot = (0, perfSnapshot_1.collectPerfSnapshot)({ mode, source, policyHash });
|
|
108
|
+
const snapshot = await (0, perfSnapshot_1.collectPerfSnapshot)({ mode, source, policyHash });
|
|
109
109
|
const hp = snapshot.hotPath;
|
|
110
110
|
console.log(`node spawn floor: ${hp.nodeSpawnFloorMs}ms median — OS+AV cost of any per-event process`);
|
|
111
111
|
console.log(`hook end-to-end: ${hp.hookMedianMs}ms median, ${hp.hookMinMs}-${hp.hookMaxMs}ms range — per IDE event (shell/MCP/file)`);
|
package/dist/perfSnapshot.d.ts
CHANGED
|
@@ -7,6 +7,17 @@
|
|
|
7
7
|
* only. Nothing here installs a profiler, starts a timer, or samples in the
|
|
8
8
|
* background — every number is collected in one shot, in a few seconds, and
|
|
9
9
|
* the process that collected it exits.
|
|
10
|
+
*
|
|
11
|
+
* NON-BLOCKING BY CONTRACT: everything here uses async `spawn`, never
|
|
12
|
+
* `spawnSync`. The remote action runs INSIDE the daemon — the same process
|
|
13
|
+
* that answers verdict IPC for every live hook. A spawnSync benchmark froze
|
|
14
|
+
* the daemon's event loop, so each benchmarked hook child asked the daemon,
|
|
15
|
+
* got silence, waited out the full IPC response timeout (~2.5s) and fell back
|
|
16
|
+
* to local evaluation: the snapshot reported "hook median 2605ms" on a machine
|
|
17
|
+
* whose real per-event cost was milliseconds — the measurement was poisoning
|
|
18
|
+
* itself. Async spawns keep the daemon answering during the benchmark, so the
|
|
19
|
+
* number is the true production path (and real developer hooks keep their
|
|
20
|
+
* fast path while a snapshot is being collected).
|
|
10
21
|
*/
|
|
11
22
|
export interface PerfProcessInfo {
|
|
12
23
|
pid: number;
|
|
@@ -67,13 +78,13 @@ export interface PerfSnapshot {
|
|
|
67
78
|
distress: unknown[];
|
|
68
79
|
}
|
|
69
80
|
/** Micro-bench of the per-event hot path: node spawn floor, hook end-to-end, cmd guard. */
|
|
70
|
-
export declare function benchHotPath(entryJs?: string): PerfHotPathBench
|
|
81
|
+
export declare function benchHotPath(entryJs?: string): Promise<PerfHotPathBench>;
|
|
71
82
|
/**
|
|
72
83
|
* Per-process footprint of every FullCourtDefense-owned process (daemon, MCP
|
|
73
84
|
* gateways, desktop chat watcher, watchdog): working set, cumulative CPU,
|
|
74
85
|
* uptime, handle count. On-demand only — this spawns ONE OS query and exits.
|
|
75
86
|
*/
|
|
76
|
-
export declare function collectProcessFootprint(): PerfProcessInfo[]
|
|
87
|
+
export declare function collectProcessFootprint(): Promise<PerfProcessInfo[]>;
|
|
77
88
|
/** Disk footprint: our logs/spool sizes, spool backlog, update-attempt markers. */
|
|
78
89
|
export declare function collectDiskState(): PerfDiskState;
|
|
79
90
|
export interface CollectPerfSnapshotInput {
|
|
@@ -86,6 +97,6 @@ export interface CollectPerfSnapshotInput {
|
|
|
86
97
|
entryJs?: string;
|
|
87
98
|
}
|
|
88
99
|
/** One-shot collection of the full snapshot. Completes in a few seconds. */
|
|
89
|
-
export declare function collectPerfSnapshot(input?: CollectPerfSnapshotInput): PerfSnapshot
|
|
100
|
+
export declare function collectPerfSnapshot(input?: CollectPerfSnapshotInput): Promise<PerfSnapshot>;
|
|
90
101
|
/** One-line human summary for machine-action result reporting. */
|
|
91
102
|
export declare function summarizePerfSnapshot(s: PerfSnapshot): string;
|
package/dist/perfSnapshot.js
CHANGED
|
@@ -55,23 +55,61 @@ function fileSizeKB(p) {
|
|
|
55
55
|
return undefined;
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Run one child to completion WITHOUT blocking the event loop, returning
|
|
60
|
+
* elapsed wall time and captured stdout. The event loop staying free is the
|
|
61
|
+
* whole point (see the header): the daemon must keep answering verdict IPC
|
|
62
|
+
* while these children run. Never throws — a spawn failure or timeout still
|
|
63
|
+
* resolves with whatever elapsed/output there is.
|
|
64
|
+
*/
|
|
65
|
+
function runTimed(cmd, args, opts) {
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
const t0 = Date.now();
|
|
68
|
+
let stdout = '';
|
|
69
|
+
let settled = false;
|
|
70
|
+
let killTimer;
|
|
71
|
+
const finish = () => {
|
|
72
|
+
if (settled)
|
|
73
|
+
return;
|
|
74
|
+
settled = true;
|
|
75
|
+
if (killTimer)
|
|
76
|
+
clearTimeout(killTimer);
|
|
77
|
+
resolve({ elapsedMs: Date.now() - t0, stdout });
|
|
78
|
+
};
|
|
79
|
+
let child;
|
|
80
|
+
try {
|
|
81
|
+
child = (0, child_process_1.spawn)(cmd, args, { windowsHide: true, stdio: ['pipe', 'pipe', 'ignore'] });
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
finish();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
killTimer = setTimeout(() => { try {
|
|
88
|
+
child.kill();
|
|
89
|
+
}
|
|
90
|
+
catch { /* already gone */ } }, opts.timeoutMs);
|
|
91
|
+
child.on('error', finish);
|
|
92
|
+
child.on('close', finish);
|
|
93
|
+
child.stdout?.on('data', (chunk) => { stdout += String(chunk); });
|
|
94
|
+
child.stdin?.on('error', () => { });
|
|
95
|
+
if (opts.input !== undefined)
|
|
96
|
+
child.stdin?.write(opts.input);
|
|
97
|
+
child.stdin?.end();
|
|
98
|
+
});
|
|
99
|
+
}
|
|
58
100
|
/** Micro-bench of the per-event hot path: node spawn floor, hook end-to-end, cmd guard. */
|
|
59
|
-
function benchHotPath(entryJs) {
|
|
101
|
+
async function benchHotPath(entryJs) {
|
|
60
102
|
const entry = entryJs || process.argv[1] || '';
|
|
103
|
+
// Sequential on purpose: overlapping children would contend for CPU and
|
|
104
|
+
// inflate each other's numbers.
|
|
61
105
|
const spawnTimes = [];
|
|
62
106
|
for (let i = 0; i < 5; i++) {
|
|
63
|
-
|
|
64
|
-
(0, child_process_1.spawnSync)(process.execPath, ['-e', '0'], { windowsHide: true, timeout: 30_000 });
|
|
65
|
-
spawnTimes.push(Date.now() - t0);
|
|
107
|
+
spawnTimes.push((await runTimed(process.execPath, ['-e', '0'], { timeoutMs: 30_000 })).elapsedMs);
|
|
66
108
|
}
|
|
67
109
|
const hookEvent = JSON.stringify({ hook_event_name: 'beforeShellExecution', command: 'echo fcd-perf-probe' });
|
|
68
110
|
const hookTimes = [];
|
|
69
111
|
for (let i = 0; i < 5; i++) {
|
|
70
|
-
|
|
71
|
-
(0, child_process_1.spawnSync)(process.execPath, [entry, 'hook', '--event', 'shell', '--fcd-managed', 'true'], {
|
|
72
|
-
input: hookEvent, encoding: 'utf8', windowsHide: true, timeout: 60_000,
|
|
73
|
-
});
|
|
74
|
-
hookTimes.push(Date.now() - t0);
|
|
112
|
+
hookTimes.push((await runTimed(process.execPath, [entry, 'hook', '--event', 'shell', '--fcd-managed', 'true'], { input: hookEvent, timeoutMs: 60_000 })).elapsedMs);
|
|
75
113
|
}
|
|
76
114
|
let cmdGuardOverheadMs;
|
|
77
115
|
const guardJs = path.join(os.homedir(), '.fullcourtdefense-cmd-guard.js');
|
|
@@ -79,12 +117,8 @@ function benchHotPath(entryJs) {
|
|
|
79
117
|
const direct = [];
|
|
80
118
|
const guarded = [];
|
|
81
119
|
for (let i = 0; i < 4; i++) {
|
|
82
|
-
|
|
83
|
-
(
|
|
84
|
-
direct.push(Date.now() - t0);
|
|
85
|
-
t0 = Date.now();
|
|
86
|
-
(0, child_process_1.spawnSync)(process.execPath, [guardJs, 'echo', 'fcd-perf'], { windowsHide: true, timeout: 30_000 });
|
|
87
|
-
guarded.push(Date.now() - t0);
|
|
120
|
+
direct.push((await runTimed(process.env.ComSpec || 'cmd.exe', ['/d', '/c', 'echo', 'fcd-perf'], { timeoutMs: 30_000 })).elapsedMs);
|
|
121
|
+
guarded.push((await runTimed(process.execPath, [guardJs, 'echo', 'fcd-perf'], { timeoutMs: 30_000 })).elapsedMs);
|
|
88
122
|
}
|
|
89
123
|
cmdGuardOverheadMs = Math.max(0, median(guarded) - median(direct));
|
|
90
124
|
}
|
|
@@ -102,11 +136,11 @@ function benchHotPath(entryJs) {
|
|
|
102
136
|
* gateways, desktop chat watcher, watchdog): working set, cumulative CPU,
|
|
103
137
|
* uptime, handle count. On-demand only — this spawns ONE OS query and exits.
|
|
104
138
|
*/
|
|
105
|
-
function collectProcessFootprint() {
|
|
139
|
+
async function collectProcessFootprint() {
|
|
106
140
|
try {
|
|
107
141
|
if (process.platform === 'win32') {
|
|
108
142
|
// On-demand action (never a hot path), so one PowerShell query is fine.
|
|
109
|
-
const ps =
|
|
143
|
+
const ps = await runTimed('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', `
|
|
110
144
|
$now = Get-Date
|
|
111
145
|
Get-CimInstance Win32_Process |
|
|
112
146
|
Where-Object { $_.CommandLine -and ($_.CommandLine -match 'fullcourtdefense' -or $_.CommandLine -match 'FullCourtDefense') -and $_.CommandLine -notmatch 'Get-CimInstance' } |
|
|
@@ -121,7 +155,7 @@ function collectProcessFootprint() {
|
|
|
121
155
|
cmd = if ($_.CommandLine.Length -gt 160) { $_.CommandLine.Substring(0,160) } else { $_.CommandLine }
|
|
122
156
|
}
|
|
123
157
|
} | ConvertTo-Json -Compress
|
|
124
|
-
`], {
|
|
158
|
+
`], { timeoutMs: 20_000 });
|
|
125
159
|
const raw = (ps.stdout || '').trim();
|
|
126
160
|
if (!raw)
|
|
127
161
|
return [];
|
|
@@ -138,7 +172,7 @@ function collectProcessFootprint() {
|
|
|
138
172
|
}));
|
|
139
173
|
}
|
|
140
174
|
// POSIX: rss (KB), cumulative cpu time, elapsed, args.
|
|
141
|
-
const ps =
|
|
175
|
+
const ps = await runTimed('ps', ['-eo', 'pid=,rss=,time=,etimes=,args='], { timeoutMs: 20_000 });
|
|
142
176
|
const lines = (ps.stdout || '').split('\n').filter(l => /fullcourtdefense/i.test(l) && !/ps -eo/.test(l));
|
|
143
177
|
const result = [];
|
|
144
178
|
for (const line of lines) {
|
|
@@ -190,8 +224,8 @@ function collectDiskState() {
|
|
|
190
224
|
};
|
|
191
225
|
}
|
|
192
226
|
/** One-shot collection of the full snapshot. Completes in a few seconds. */
|
|
193
|
-
function collectPerfSnapshot(input = {}) {
|
|
194
|
-
const processes = collectProcessFootprint();
|
|
227
|
+
async function collectPerfSnapshot(input = {}) {
|
|
228
|
+
const processes = await collectProcessFootprint();
|
|
195
229
|
return {
|
|
196
230
|
collectedAt: new Date().toISOString(),
|
|
197
231
|
cliVersion: input.cliVersion,
|
|
@@ -203,7 +237,7 @@ function collectPerfSnapshot(input = {}) {
|
|
|
203
237
|
node: process.version,
|
|
204
238
|
},
|
|
205
239
|
enforcement: { mode: input.mode, source: input.source, policyHash: input.policyHash },
|
|
206
|
-
hotPath: benchHotPath(input.entryJs),
|
|
240
|
+
hotPath: await benchHotPath(input.entryJs),
|
|
207
241
|
processes,
|
|
208
242
|
totalWorkingSetMB: Math.round(processes.reduce((sum, p) => sum + p.workingSetMB, 0) * 10) / 10,
|
|
209
243
|
disk: collectDiskState(),
|
package/dist/version.json
CHANGED