pyre-cli 10.2.0 → 10.4.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/README.md +75 -18
- package/dist/{alertChannels-TPSQIJ7S.js → alertChannels-MNX4XH6J.js} +1 -0
- package/dist/{anomaliesCmd-25OFKYPK.js → anomaliesCmd-HHTY5LYL.js} +2 -0
- package/dist/benchGitCmd-HIXUC5EK.js +87 -0
- package/dist/{benchmarkCmd-A3ETK6VY.js → benchmarkCmd-B7DLP2VC.js} +2 -0
- package/dist/{blender-FGH3ASEQ.js → blender-6ZX5SPYC.js} +3 -1
- package/dist/{brewHealth-P2GW5Y5C.js → brewHealth-VFW33QB3.js} +1 -0
- package/dist/budgetCmd-TGO3RYLN.js +11 -0
- package/dist/{buildCmd-CDPZ2ZFA.js → buildCmd-E7KUCFYG.js} +3 -1
- package/dist/{checkCmd-WQAHAUWO.js → checkCmd-QLYTP77Y.js} +5 -3
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/{chunk-VIACN2RU.js → chunk-FYCBGUTW.js} +41 -19
- package/dist/chunk-JH7P6KLN.js +143 -0
- package/dist/{chunk-QM5L43EK.js → chunk-LDJB5V4N.js} +5 -1
- package/dist/chunk-NMGQVRCA.js +111 -0
- package/dist/chunk-OCL7P7N7.js +936 -0
- package/dist/{chunk-23YWCI3O.js → chunk-QEH4LR6P.js} +28 -686
- package/dist/{chunk-KVUMOA6C.js → chunk-RKPGPVTO.js} +77 -4
- package/dist/chunk-UF3XQ3R3.js +3800 -0
- package/dist/{collectors-JDN5Y7EV.js → collectors-V7SCIIDH.js} +5 -3
- package/dist/{completions-ZVTNE52B.js → completions-FTPXDSR2.js} +44 -3
- package/dist/{config-J4YMADEF.js → config-5WW5M63G.js} +2 -1
- package/dist/containersCmd-FSMYOBG2.js +84 -0
- package/dist/{doctor-LHFW4UJP.js → doctor-O4DIAMHK.js} +3 -2
- package/dist/{explainCmd-MF6PIMLW.js → explainCmd-7PBPA7O5.js} +1 -0
- package/dist/{extensions-XJGHG7X3.js → extensions-QQEV7U3D.js} +1 -0
- package/dist/{firebaseSync-ON3RZ6KX.js → firebaseSync-LRN67UTV.js} +2 -0
- package/dist/flightRecorder-GJIAQOXC.js +162 -0
- package/dist/index.js +737 -4125
- package/dist/inspectCmd-5FXJBJTK.js +137 -0
- package/dist/menu-AJFGDLBP.js +1233 -0
- package/dist/monitors/platform/windows-ui.ps1 +31 -29
- package/dist/{netUsageCmd-67IBZZYN.js → netUsageCmd-JP6YKBPF.js} +7 -5
- package/dist/optimizeCmd-WZKLFYZU.js +1110 -0
- package/dist/{pipeCmd-2N4JPKYE.js → pipeCmd-CPK6FVKB.js} +5 -3
- package/dist/playbookCmd-FRROIEXR.js +145 -0
- package/dist/{profileProcCmd-N4H6MCBT.js → profileProcCmd-5Y3733E7.js} +64 -13
- package/dist/{prometheus-5BL5PVLC.js → prometheus-CYNUR3QT.js} +1 -0
- package/dist/{prometheusCmd-LEUM5WCZ.js → prometheusCmd-G2CSEZRQ.js} +5 -3
- package/dist/{replayCmd-Z3PYXY7V.js → replayCmd-ODB5SPOQ.js} +25 -6
- package/dist/{run-HOGQDDTA.js → run-UJQYT2TO.js} +1 -0
- package/dist/{serveCmd-L7LVJOZN.js → serveCmd-KDPWLVQI.js} +5 -3
- package/dist/{service-7GIX3YNC.js → service-LIYYR3UI.js} +1 -0
- package/dist/{smartCmd-ODWKSFNG.js → smartCmd-2BJJ37GV.js} +130 -5
- package/dist/statuslineCmd-EUKYRR2G.js +75 -0
- package/dist/{stressCmd-MFXMFXYX.js → stressCmd-GKHGY26X.js} +2 -0
- package/dist/{syncCmd-42IRF633.js → syncCmd-MIBFKX24.js} +16 -5
- package/dist/topoCmd-RTXQJ6XA.js +189 -0
- package/dist/{updateCheck-52X6LJG5.js → updateCheck-LJHEMG3G.js} +2 -0
- package/dist/{watchdogCmd-6GR4UN6B.js → watchdogCmd-XM6OPTKM.js} +41 -6
- package/dist/{wizard-JQMQWFAW.js → wizard-PRDOJROQ.js} +2 -1
- package/package.json +2 -2
- package/src/benchGitCmd.ts +110 -0
- package/src/budgetCmd.ts +199 -0
- package/src/completions.ts +42 -3
- package/src/containersCmd.ts +106 -0
- package/src/doctor.ts +86 -6
- package/src/easy_features.test.ts +83 -0
- package/src/flightRecorder.ts +214 -0
- package/src/index.ts +183 -9
- package/src/inspectCmd.ts +165 -0
- package/src/live/input.ts +70 -3
- package/src/live/render.ts +1 -1
- package/src/live/state.ts +2 -1
- package/src/menu/commands.ts +754 -0
- package/src/menu/index.ts +358 -0
- package/src/menu/render.ts +272 -0
- package/src/menu/types.ts +63 -0
- package/src/menu.test.ts +110 -0
- package/src/monitors/blender.ts +49 -19
- package/src/monitors/buildTracker.ts +72 -54
- package/src/monitors/platform/windows-ui.ps1 +31 -29
- package/src/monitors/platform/windows.ts +613 -280
- package/src/optimizeCmd.ts +1248 -0
- package/src/platform.test.ts +135 -0
- package/src/playbookCmd.ts +156 -0
- package/src/power_features.test.ts +251 -0
- package/src/profileProcCmd.ts +68 -21
- package/src/replayCmd.ts +15 -6
- package/src/smartCmd.ts +149 -10
- package/src/state/config.ts +13 -0
- package/src/statuslineCmd.ts +87 -0
- package/src/syncCmd.ts +12 -0
- package/src/topoCmd.ts +83 -50
- package/src/watchdogCmd.ts +40 -3
- package/src/xbar.ts +195 -17
- package/dist/chunk-M73HCQD2.js +0 -95
- package/dist/topoCmd-DZ3YS5IL.js +0 -148
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
<p align="center">
|
|
15
15
|
<a href="https://www.npmjs.com/package/pyre-cli"><img src="https://img.shields.io/npm/v/pyre-cli.svg?style=flat" alt="npm version" /></a>
|
|
16
|
-
<a href="package.json"><img src="https://img.shields.io/badge/version-10.
|
|
16
|
+
<a href="package.json"><img src="https://img.shields.io/badge/version-10.2.0-blue?style=flat" alt="Version" /></a>
|
|
17
17
|
<a href="#requirements"><img src="https://img.shields.io/badge/platforms-macOS%20%7C%20Linux%20%7C%20Windows-lightgrey?style=flat" alt="Platforms" /></a>
|
|
18
18
|
<a href="#requirements"><img src="https://img.shields.io/badge/node-%3E%3D18.0.0-green?style=flat" alt="Node" /></a>
|
|
19
19
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green?style=flat" alt="License" /></a>
|
|
@@ -107,6 +107,7 @@ Launch with `pyre live` for a responsive, full-terminal dashboard featuring:
|
|
|
107
107
|
| **Fleet & SSH Aggregation** | Monitor remote nodes over SSH (`pyre ssh <host>`) or aggregate hundreds of servers into an auto-tiled overview (`pyre fleet`). |
|
|
108
108
|
| **Socket & Packet Forensics** | Track socket connection states (`ESTABLISHED`, `LISTENING`, `TIME_WAIT`), top remote hosts, and per-process network bandwidth. |
|
|
109
109
|
| **AI Anomaly Explanation** | Explain performance spikes and thermal throttles in plain English using local Ollama models or OpenAI (`pyre explain`). |
|
|
110
|
+
| **macOS System Optimizer** | Reclaim memory and disk space: quit idle/forgotten apps, flush DNS, purge inactive RAM, clean developer caches, empty trash, and audit broken startup agents (`pyre optimize`). |
|
|
110
111
|
| **Multi-Format Export** | Export instant snapshots to JSON, CSV, TSV, HTML, or Markdown reports, plus an importable Grafana dashboard template. |
|
|
111
112
|
|
|
112
113
|
---
|
|
@@ -144,19 +145,19 @@ pyre web --lan --port 3000
|
|
|
144
145
|
|
|
145
146
|
## Comparison
|
|
146
147
|
|
|
147
|
-
| Feature | Pyre | btop | htop | Glances |
|
|
148
|
-
|
|
149
|
-
| **Interactive TUI & Sparklines** | Yes (6 themes + JSON) | Yes | Limited | Yes |
|
|
150
|
-
| **Native macOS Desktop Window** | Yes (Swift Cocoa) | No | No | No |
|
|
151
|
-
| **Apple Silicon P/E + GPU + ANE** | Yes (Granular watts) | Partial | Partial | Partial |
|
|
152
|
-
| **Encrypted P2P Telemetry** | Yes (TLS + HMAC) | No | No | Partial (Plain) |
|
|
153
|
-
| **Real-time Web Dashboard (SSE)** | Yes (Zero-config) | No | No | Yes |
|
|
154
|
-
| **Multi-Host Fleet Aggregation** | Yes (SSH / P2P) | No | No | Yes (Web only) |
|
|
155
|
-
| **Socket Connection Forensics** | Yes | Partial | No | Partial |
|
|
156
|
-
| **Statistical Anomaly Radar** | Yes (Z-score) | No | No | No |
|
|
157
|
-
| **Energy & Cost Profiler** | Yes (kWh / Joules) | No | No | No |
|
|
158
|
-
| **System Security Doctor** | Yes (SIP / Sensors) | No | No | No |
|
|
159
|
-
| **Export Formats** | JSON, CSV, TSV, HTML, MD | No | No | InfluxDB, CSV |
|
|
148
|
+
| Feature | Pyre | btop | macmon | mactop | asitop | htop | Glances |
|
|
149
|
+
|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
|
150
|
+
| **Interactive TUI & Sparklines** | Yes (6 themes + JSON) | Yes | Yes (Glanceable) | Yes | Yes | Limited | Yes |
|
|
151
|
+
| **Native macOS Desktop Window** | Yes (Swift Cocoa) | No | No | No | No | No | No |
|
|
152
|
+
| **Apple Silicon P/E + GPU + ANE** | Yes (Granular watts) | Partial | Yes (IOReport watts) | Yes (Powermetrics) | Yes (Powermetrics) | Partial | Partial |
|
|
153
|
+
| **Encrypted P2P Telemetry** | Yes (TLS + HMAC) | No | No | No | No | No | Partial (Plain) |
|
|
154
|
+
| **Real-time Web Dashboard (SSE)** | Yes (Zero-config) | No | No | No | No | No | Yes |
|
|
155
|
+
| **Multi-Host Fleet Aggregation** | Yes (SSH / P2P) | No | No | No | No | No | Yes (Web only) |
|
|
156
|
+
| **Socket Connection Forensics** | Yes | Partial | No | No | No | No | Partial |
|
|
157
|
+
| **Statistical Anomaly Radar** | Yes (Z-score) | No | No | No | No | No | No |
|
|
158
|
+
| **Energy & Cost Profiler** | Yes (kWh / Joules) | No | No | No | No | No | No |
|
|
159
|
+
| **System Security Doctor** | Yes (SIP / Sensors) | No | No | No | No | No | No |
|
|
160
|
+
| **Export Formats** | JSON, CSV, TSV, HTML, MD | No | None | None | None | No | InfluxDB, CSV |
|
|
160
161
|
|
|
161
162
|
---
|
|
162
163
|
|
|
@@ -213,6 +214,15 @@ pyre brew # Package manager health check & disk cache repo
|
|
|
213
214
|
pyre smart # S.M.A.R.T. disk health monitor & drive wear
|
|
214
215
|
pyre blender # Background Blender 3D render job tracker
|
|
215
216
|
pyre pipe # Continuous newline-delimited JSON stream
|
|
217
|
+
pyre stress # Synthetic CPU/GPU load generator
|
|
218
|
+
pyre extensions # System Extensions and Driver inspector
|
|
219
|
+
pyre info # Hardware summary including battery health & displays
|
|
220
|
+
pyre explain # Plain-English anomaly explanation (Ollama/OpenAI/builtin)
|
|
221
|
+
pyre diff <file1> <file2> # Compare two saved snapshot files side-by-side
|
|
222
|
+
pyre history # Graph historical resource trends from CSV logs
|
|
223
|
+
pyre replay <file> # Play back a pyre CSV log file in the TUI
|
|
224
|
+
pyre build # Active compiler and build system tracker
|
|
225
|
+
pyre topo # Network topology mapper and pyre peer auto-discovery
|
|
216
226
|
```
|
|
217
227
|
|
|
218
228
|
### Automation, Profiles & Daemon
|
|
@@ -220,10 +230,18 @@ pyre pipe # Continuous newline-delimited JSON stream
|
|
|
220
230
|
```bash
|
|
221
231
|
pyre watchdog # Autonomous daemon monitoring rules & killing rogue procs
|
|
222
232
|
pyre serve [--port 8080] # Lightweight REST API server exposing JSON endpoints
|
|
223
|
-
pyre profile save
|
|
224
|
-
pyre
|
|
225
|
-
pyre
|
|
226
|
-
pyre
|
|
233
|
+
pyre profile <save|load|list> # Atomic configuration profile management
|
|
234
|
+
pyre config <show|reset> # View or reset persistent configuration file
|
|
235
|
+
pyre completions <shell> # Generate shell auto-completions (zsh, bash, fish, powershell)
|
|
236
|
+
pyre prometheus # Start a Prometheus metrics exporter on localhost
|
|
237
|
+
pyre p2p <server|connect> # Start a P2P server or connect to one
|
|
238
|
+
pyre server # Print one-line helper commands for P2P pairing
|
|
239
|
+
pyre alert test # Send a test notification to all configured alert channels
|
|
240
|
+
pyre profile-proc <pid> # CPU call-tree profiler and hot function sampler
|
|
241
|
+
pyre sync <push|pull|status> # Synchronize config profiles across machines
|
|
242
|
+
pyre xbar # Generate an xbar / SwiftBar menu bar plugin script
|
|
243
|
+
pyre optimize [subcommand] # macOS optimizer: quit idle apps, memory, caches, trash, startup, dns
|
|
244
|
+
pyre update # Check for pyre-cli updates
|
|
227
245
|
```
|
|
228
246
|
|
|
229
247
|
---
|
|
@@ -248,6 +266,45 @@ Security and networking capabilities:
|
|
|
248
266
|
|
|
249
267
|
---
|
|
250
268
|
|
|
269
|
+
## macOS System Optimizer (`pyre optimize`)
|
|
270
|
+
|
|
271
|
+
Active memory, disk, and application lifecycle optimization for macOS:
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
# 1. System health & optimization overview report (safe, dry-run by default)
|
|
275
|
+
pyre optimize
|
|
276
|
+
|
|
277
|
+
# 2. Find and gracefully quit apps open but unused recently
|
|
278
|
+
pyre optimize apps # Interactive prompt with memory & CPU summary
|
|
279
|
+
pyre optimize apps --idle 30m --keep 3 # Custom idle duration & recent apps to keep
|
|
280
|
+
pyre optimize apps --dry-run # Preview without quitting
|
|
281
|
+
|
|
282
|
+
# 3. Background app activity tracker (records precise frontmost app usage)
|
|
283
|
+
pyre optimize track # Run tracker in current terminal
|
|
284
|
+
pyre optimize track --install # Install lightweight background launchd agent
|
|
285
|
+
pyre optimize track --auto-quit 2h # Daemon auto-quits idle apps & sends desktop alert
|
|
286
|
+
pyre optimize track --uninstall # Remove background tracker
|
|
287
|
+
|
|
288
|
+
# 4. Memory pressure & inactive purge
|
|
289
|
+
pyre optimize memory # Shows pressure, swap, compressed RAM, top consumers
|
|
290
|
+
pyre optimize memory -y # Flushes inactive file cache via `sudo purge`
|
|
291
|
+
|
|
292
|
+
# 5. Developer & system caches
|
|
293
|
+
pyre optimize caches # Measure Xcode DerivedData, Homebrew, npm/yarn/pnpm
|
|
294
|
+
pyre optimize caches --clean # Clean selected caches (safely skips running apps)
|
|
295
|
+
|
|
296
|
+
# 6. Trash & startup maintenance
|
|
297
|
+
pyre optimize trash --empty # Measure and empty macOS Trash
|
|
298
|
+
pyre optimize startup # Audit Login Items & LaunchAgents, flag broken leftovers
|
|
299
|
+
pyre optimize dns # Flush macOS DNS cache via dscacheutil & mDNSResponder
|
|
300
|
+
|
|
301
|
+
# 7. Protect whitelist
|
|
302
|
+
pyre optimize protect "Spotify" # Whitelist app so it is never automatically quit
|
|
303
|
+
pyre optimize unprotect "Spotify" # Remove app from custom whitelist
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
251
308
|
## Requirements
|
|
252
309
|
|
|
253
310
|
| Platform | Minimum Requirement | Notes |
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatBytes,
|
|
3
|
+
runWithBudget
|
|
4
|
+
} from "./chunk-JH7P6KLN.js";
|
|
5
|
+
import "./chunk-3RG5ZIWI.js";
|
|
6
|
+
|
|
7
|
+
// src/benchGitCmd.ts
|
|
8
|
+
import { execSync } from "child_process";
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
async function runBenchGitCommand(branchA, branchB, commandStr) {
|
|
11
|
+
try {
|
|
12
|
+
const status = execSync("git status --porcelain", { encoding: "utf-8" }).trim();
|
|
13
|
+
if (status) {
|
|
14
|
+
console.error(chalk.red("\n \u2716 Working directory contains uncommitted changes."));
|
|
15
|
+
console.log(chalk.yellow(" Please commit or stash your changes before running `pyre bench-git`.\n"));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
} catch (err) {
|
|
19
|
+
console.error(chalk.red(`
|
|
20
|
+
\u2716 Not a git repository: ${err.message}`));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const startingBranch = execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf-8" }).trim();
|
|
24
|
+
console.log(chalk.bold(`
|
|
25
|
+
\u{1F525} pyre bench-git \u2014 Cross-Branch Performance Differ
|
|
26
|
+
`));
|
|
27
|
+
console.log(chalk.dim(` Command: ${commandStr}`));
|
|
28
|
+
console.log(chalk.dim(` Baseline Branch: ${branchA}`));
|
|
29
|
+
console.log(chalk.dim(` Target Branch: ${branchB}`));
|
|
30
|
+
console.log(chalk.dim(` Current Branch: ${startingBranch}
|
|
31
|
+
`));
|
|
32
|
+
const tokens = commandStr.split(/\s+/).filter(Boolean);
|
|
33
|
+
try {
|
|
34
|
+
console.log(chalk.bold.cyan(`\u25B6 [1/2] Checking out '${branchA}' and benchmarking...`));
|
|
35
|
+
execSync(`git checkout ${branchA}`, { stdio: "pipe" });
|
|
36
|
+
const resA = await runWithBudget(tokens, {});
|
|
37
|
+
console.log(chalk.bold.cyan(`
|
|
38
|
+
\u25B6 [2/2] Checking out '${branchB}' and benchmarking...`));
|
|
39
|
+
execSync(`git checkout ${branchB}`, { stdio: "pipe" });
|
|
40
|
+
const resB = await runWithBudget(tokens, {});
|
|
41
|
+
execSync(`git checkout ${startingBranch}`, { stdio: "pipe" });
|
|
42
|
+
console.log(chalk.dim(`
|
|
43
|
+
Restored workspace to '${startingBranch}'.`));
|
|
44
|
+
printGitComparisonTable(branchA, branchB, resA, resB);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
try {
|
|
47
|
+
execSync(`git checkout ${startingBranch}`, { stdio: "pipe" });
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
console.error(chalk.red(`
|
|
51
|
+
\u2716 Benchmark failed: ${err.message}`));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function printGitComparisonTable(branchA, branchB, resA, resB) {
|
|
55
|
+
const diffPct = (valA, valB, invertGood = false) => {
|
|
56
|
+
if (valA === 0) return "0%";
|
|
57
|
+
const delta = (valB - valA) / valA * 100;
|
|
58
|
+
const sign = delta > 0 ? "+" : "";
|
|
59
|
+
const formatted = `${sign}${delta.toFixed(1)}%`;
|
|
60
|
+
const isGood = invertGood ? delta > 0 : delta < 0;
|
|
61
|
+
const color = delta === 0 ? chalk.dim : isGood ? chalk.green : chalk.red;
|
|
62
|
+
return color(formatted);
|
|
63
|
+
};
|
|
64
|
+
console.log(chalk.bold("\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
65
|
+
console.log(chalk.bold(" \u{1F525} Cross-Branch Performance Comparison Results"));
|
|
66
|
+
console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
67
|
+
const colBranchA = branchA.padEnd(16);
|
|
68
|
+
const colBranchB = branchB.padEnd(16);
|
|
69
|
+
console.log(` ${"Metric".padEnd(18)} ${colBranchA} ${colBranchB} Delta`);
|
|
70
|
+
console.log(chalk.dim(" \u2500".repeat(30)));
|
|
71
|
+
const durA = `${resA.durationSec.toFixed(2)}s`.padEnd(16);
|
|
72
|
+
const durB = `${resB.durationSec.toFixed(2)}s`.padEnd(16);
|
|
73
|
+
console.log(` ${"Duration".padEnd(18)} ${durA} ${durB} ${diffPct(resA.durationSec, resB.durationSec)}`);
|
|
74
|
+
const ramA = formatBytes(resA.peakMemoryBytes).padEnd(16);
|
|
75
|
+
const ramB = formatBytes(resB.peakMemoryBytes).padEnd(16);
|
|
76
|
+
console.log(` ${"Peak Memory".padEnd(18)} ${ramA} ${ramB} ${diffPct(resA.peakMemoryBytes, resB.peakMemoryBytes)}`);
|
|
77
|
+
const cpuA = `${resA.peakCpuPct.toFixed(1)}%`.padEnd(16);
|
|
78
|
+
const cpuB = `${resB.peakCpuPct.toFixed(1)}%`.padEnd(16);
|
|
79
|
+
console.log(` ${"Peak CPU".padEnd(18)} ${cpuA} ${cpuB} ${diffPct(resA.peakCpuPct, resB.peakCpuPct)}`);
|
|
80
|
+
const codeA = `${resA.exitCode}`.padEnd(16);
|
|
81
|
+
const codeB = `${resB.exitCode}`.padEnd(16);
|
|
82
|
+
console.log(` ${"Exit Code".padEnd(18)} ${codeA} ${codeB}`);
|
|
83
|
+
console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
84
|
+
}
|
|
85
|
+
export {
|
|
86
|
+
runBenchGitCommand
|
|
87
|
+
};
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import "./chunk-IM3IQMMT.js";
|
|
2
2
|
import {
|
|
3
3
|
collectAll
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
6
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-QEH4LR6P.js";
|
|
5
|
+
import "./chunk-NMGQVRCA.js";
|
|
6
|
+
import "./chunk-FYCBGUTW.js";
|
|
7
|
+
import "./chunk-OCL7P7N7.js";
|
|
7
8
|
import "./chunk-MZO5QKUR.js";
|
|
9
|
+
import "./chunk-3RG5ZIWI.js";
|
|
8
10
|
|
|
9
11
|
// src/checkCmd.ts
|
|
10
12
|
import chalk from "chalk";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
__require
|
|
10
|
+
};
|
|
@@ -1,14 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
collectWindowsProcesses
|
|
3
|
+
} from "./chunk-OCL7P7N7.js";
|
|
1
4
|
import {
|
|
2
5
|
run
|
|
3
6
|
} from "./chunk-MZO5QKUR.js";
|
|
4
7
|
|
|
5
8
|
// src/monitors/blender.ts
|
|
6
|
-
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import os from "os";
|
|
11
|
+
import path from "path";
|
|
12
|
+
var RENDER_LOG_DIR = path.join(os.tmpdir(), "blender_render_logs");
|
|
7
13
|
function blendFileFromArgs(args) {
|
|
8
14
|
const nonFlag = args.filter((a) => !a.startsWith("-"));
|
|
9
15
|
for (const a of nonFlag) {
|
|
10
16
|
if (a.endsWith(".blend") || a.includes(".blend")) {
|
|
11
|
-
return a.split(
|
|
17
|
+
return a.split(/[/\\]/).pop() || a;
|
|
12
18
|
}
|
|
13
19
|
}
|
|
14
20
|
return "unknown.blend";
|
|
@@ -24,15 +30,17 @@ function outputPathFromArgs(args) {
|
|
|
24
30
|
const oIdx = args.indexOf("-o");
|
|
25
31
|
if (oIdx !== -1 && args[oIdx + 1]) {
|
|
26
32
|
let p = args[oIdx + 1];
|
|
27
|
-
if (p.endsWith("/")) p = p.slice(0, -1);
|
|
28
|
-
|
|
33
|
+
if (p.endsWith("/") || p.endsWith("\\")) p = p.slice(0, -1);
|
|
34
|
+
const parts = p.split(/[/\\]/);
|
|
35
|
+
return parts.slice(0, -1).join("/") || os.tmpdir();
|
|
29
36
|
}
|
|
30
37
|
const fFlag = args.find((a) => a.startsWith("-o="));
|
|
31
38
|
if (fFlag) {
|
|
32
39
|
const p = fFlag.slice(3);
|
|
33
|
-
|
|
40
|
+
const parts = p.split(/[/\\]/);
|
|
41
|
+
return parts.slice(0, -1).join("/") || os.tmpdir();
|
|
34
42
|
}
|
|
35
|
-
return
|
|
43
|
+
return os.tmpdir();
|
|
36
44
|
}
|
|
37
45
|
function frameRangeFromArgs(args) {
|
|
38
46
|
const fIdx = args.indexOf("-f");
|
|
@@ -54,11 +62,12 @@ function frameRangeFromArgs(args) {
|
|
|
54
62
|
async function parseRenderLog(blendName) {
|
|
55
63
|
const baseName = blendName.replace(/\.blend$/i, "");
|
|
56
64
|
try {
|
|
57
|
-
|
|
65
|
+
if (!fs.existsSync(RENDER_LOG_DIR)) return null;
|
|
66
|
+
const entries = fs.readdirSync(RENDER_LOG_DIR).filter((f) => f.startsWith(`${baseName}_`)).map((f) => ({ name: f, time: fs.statSync(path.join(RENDER_LOG_DIR, f)).mtimeMs })).sort((a, b) => b.time - a.time).slice(0, 5);
|
|
58
67
|
for (const entry of entries) {
|
|
59
|
-
const logPath =
|
|
68
|
+
const logPath = path.join(RENDER_LOG_DIR, entry.name);
|
|
60
69
|
try {
|
|
61
|
-
const content =
|
|
70
|
+
const content = fs.readFileSync(logPath, "utf-8");
|
|
62
71
|
if (!content) continue;
|
|
63
72
|
const lines = content.split("\n");
|
|
64
73
|
const rendered = lines.filter((l) => /Fra:(\d+)/i.test(l)).length;
|
|
@@ -80,18 +89,31 @@ async function parseRenderLog(blendName) {
|
|
|
80
89
|
async function collectBlenderRenders() {
|
|
81
90
|
const renders = [];
|
|
82
91
|
try {
|
|
83
|
-
const
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
const procList = [];
|
|
93
|
+
if (process.platform === "win32") {
|
|
94
|
+
const procs = await collectWindowsProcesses();
|
|
95
|
+
for (const p of procs) {
|
|
96
|
+
if (/blender/i.test(p.command)) {
|
|
97
|
+
procList.push({ pid: p.pid, cpuPct: p.cpu, command: p.command });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
const psRaw = await run("ps -eo pid,pcpu,pmem,state,command 2>/dev/null | grep -iE '[b]lender'", "", 2e3);
|
|
102
|
+
if (psRaw) {
|
|
103
|
+
const lines = psRaw.split("\n");
|
|
104
|
+
for (const line of lines) {
|
|
105
|
+
const parts = line.trim().match(/\s*(\d+)\s+([\d.]+)\s+([\d.]+)\s+(\S+)\s+(.+)/);
|
|
106
|
+
if (parts) {
|
|
107
|
+
procList.push({ pid: parseInt(parts[1], 10), cpuPct: parseFloat(parts[2]), command: parts[5] });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const p of procList) {
|
|
113
|
+
const { pid, cpuPct, command } = p;
|
|
92
114
|
const args = command.split(/\s+/);
|
|
93
115
|
const isRender = /-b|--background|-f\s/.test(command) || /render/i.test(command);
|
|
94
|
-
if (!isRender && !command.includes("blender")) continue;
|
|
116
|
+
if (!isRender && !command.toLowerCase().includes("blender")) continue;
|
|
95
117
|
const blendFile = blendFileFromArgs(args);
|
|
96
118
|
const engine = engineFromArgs(args);
|
|
97
119
|
const outputPath = outputPathFromArgs(args);
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__require
|
|
3
|
+
} from "./chunk-3RG5ZIWI.js";
|
|
4
|
+
|
|
5
|
+
// src/budgetCmd.ts
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import { spawn } from "child_process";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
function parseBytesString(s) {
|
|
10
|
+
const trimmed = s.trim().toUpperCase();
|
|
11
|
+
const num = parseFloat(trimmed);
|
|
12
|
+
if (isNaN(num)) return 0;
|
|
13
|
+
if (trimmed.endsWith("G") || trimmed.endsWith("GB")) return num * 1024 * 1024 * 1024;
|
|
14
|
+
if (trimmed.endsWith("M") || trimmed.endsWith("MB")) return num * 1024 * 1024;
|
|
15
|
+
if (trimmed.endsWith("K") || trimmed.endsWith("KB")) return num * 1024;
|
|
16
|
+
return num;
|
|
17
|
+
}
|
|
18
|
+
function formatBytes(b) {
|
|
19
|
+
if (b >= 1024 * 1024 * 1024) return `${(b / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
20
|
+
if (b >= 1024 * 1024) return `${(b / (1024 * 1024)).toFixed(2)} MB`;
|
|
21
|
+
if (b >= 1024) return `${(b / 1024).toFixed(1)} KB`;
|
|
22
|
+
return `${b} B`;
|
|
23
|
+
}
|
|
24
|
+
async function runWithBudget(commandArgs, budgets, opts) {
|
|
25
|
+
if (commandArgs.length === 0) {
|
|
26
|
+
throw new Error("No command specified. Usage: pyre run [budget flags] -- <command>");
|
|
27
|
+
}
|
|
28
|
+
const cmdStr = commandArgs.join(" ");
|
|
29
|
+
const [bin, ...args] = commandArgs;
|
|
30
|
+
const startTime = Date.now();
|
|
31
|
+
const cpuSamples = [];
|
|
32
|
+
let peakMemBytes = 0;
|
|
33
|
+
let peakCpu = 0;
|
|
34
|
+
const child = spawn(bin, args, {
|
|
35
|
+
stdio: "inherit",
|
|
36
|
+
env: process.env
|
|
37
|
+
});
|
|
38
|
+
const pid = child.pid;
|
|
39
|
+
let sampleInterval = null;
|
|
40
|
+
if (pid) {
|
|
41
|
+
sampleInterval = setInterval(() => {
|
|
42
|
+
try {
|
|
43
|
+
const { execSync } = __require("child_process");
|
|
44
|
+
const output = execSync(`ps -o %cpu,rss -p ${pid} 2>/dev/null`, { encoding: "utf-8" });
|
|
45
|
+
const lines = output.trim().split("\n");
|
|
46
|
+
if (lines.length > 1) {
|
|
47
|
+
const parts = lines[1].trim().split(/\s+/);
|
|
48
|
+
const cpu = parseFloat(parts[0]) || 0;
|
|
49
|
+
const rssKb = parseInt(parts[1], 10) || 0;
|
|
50
|
+
const memBytes = rssKb * 1024;
|
|
51
|
+
cpuSamples.push(cpu);
|
|
52
|
+
if (cpu > peakCpu) peakCpu = cpu;
|
|
53
|
+
if (memBytes > peakMemBytes) peakMemBytes = memBytes;
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
}
|
|
57
|
+
}, 100);
|
|
58
|
+
}
|
|
59
|
+
const exitCode = await new Promise((resolve) => {
|
|
60
|
+
child.on("close", (code) => {
|
|
61
|
+
if (sampleInterval) clearInterval(sampleInterval);
|
|
62
|
+
resolve(code ?? 0);
|
|
63
|
+
});
|
|
64
|
+
child.on("error", () => {
|
|
65
|
+
if (sampleInterval) clearInterval(sampleInterval);
|
|
66
|
+
resolve(1);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
const durationSec = (Date.now() - startTime) / 1e3;
|
|
70
|
+
const avgCpu = cpuSamples.length > 0 ? cpuSamples.reduce((a, b) => a + b, 0) / cpuSamples.length : peakCpu;
|
|
71
|
+
const violations = [];
|
|
72
|
+
if (budgets.maxTimeSec !== void 0 && durationSec > budgets.maxTimeSec) {
|
|
73
|
+
violations.push(`Runtime exceeded budget: ${durationSec.toFixed(2)}s > ${budgets.maxTimeSec}s limit`);
|
|
74
|
+
}
|
|
75
|
+
if (budgets.maxMemoryBytes !== void 0 && peakMemBytes > budgets.maxMemoryBytes) {
|
|
76
|
+
violations.push(`Memory exceeded budget: ${formatBytes(peakMemBytes)} > ${formatBytes(budgets.maxMemoryBytes)} limit`);
|
|
77
|
+
}
|
|
78
|
+
if (budgets.maxCpuPct !== void 0 && peakCpu > budgets.maxCpuPct) {
|
|
79
|
+
violations.push(`CPU exceeded budget: ${peakCpu.toFixed(1)}% > ${budgets.maxCpuPct}% limit`);
|
|
80
|
+
}
|
|
81
|
+
const result = {
|
|
82
|
+
command: cmdStr,
|
|
83
|
+
exitCode,
|
|
84
|
+
durationSec,
|
|
85
|
+
peakCpuPct: peakCpu,
|
|
86
|
+
avgCpuPct: avgCpu,
|
|
87
|
+
peakMemoryBytes: peakMemBytes,
|
|
88
|
+
violations
|
|
89
|
+
};
|
|
90
|
+
printBudgetReport(result, budgets, opts);
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
function printBudgetReport(result, budgets, opts) {
|
|
94
|
+
const isViolated = result.violations.length > 0;
|
|
95
|
+
const isFailed = result.exitCode !== 0 || isViolated;
|
|
96
|
+
console.log(chalk.bold("\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
97
|
+
console.log(chalk.bold(` \u{1F525} pyre run \u2014 Performance Budget Report`));
|
|
98
|
+
console.log(chalk.dim(` Command: ${result.command}`));
|
|
99
|
+
console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
100
|
+
const statusTime = budgets.maxTimeSec === void 0 ? chalk.dim("unconstrained") : result.durationSec <= budgets.maxTimeSec ? chalk.green(`\u2714 Pass (<= ${budgets.maxTimeSec}s)`) : chalk.red(`\u2716 FAIL (> ${budgets.maxTimeSec}s)`);
|
|
101
|
+
const statusMem = budgets.maxMemoryBytes === void 0 ? chalk.dim("unconstrained") : result.peakMemoryBytes <= budgets.maxMemoryBytes ? chalk.green(`\u2714 Pass (<= ${formatBytes(budgets.maxMemoryBytes)})`) : chalk.red(`\u2716 FAIL (> ${formatBytes(budgets.maxMemoryBytes)})`);
|
|
102
|
+
const statusCpu = budgets.maxCpuPct === void 0 ? chalk.dim("unconstrained") : result.peakCpuPct <= budgets.maxCpuPct ? chalk.green(`\u2714 Pass (<= ${budgets.maxCpuPct}%)`) : chalk.red(`\u2716 FAIL (> ${budgets.maxCpuPct}%)`);
|
|
103
|
+
console.log(` Duration: ${result.durationSec.toFixed(2)}s`.padEnd(28) + statusTime);
|
|
104
|
+
console.log(` Peak Memory: ${formatBytes(result.peakMemoryBytes)}`.padEnd(28) + statusMem);
|
|
105
|
+
console.log(` Peak CPU: ${result.peakCpuPct.toFixed(1)}% (avg ${result.avgCpuPct.toFixed(1)}%)`.padEnd(28) + statusCpu);
|
|
106
|
+
console.log(` Exit Code: ${result.exitCode === 0 ? chalk.green("0") : chalk.red(result.exitCode)}`);
|
|
107
|
+
if (isViolated) {
|
|
108
|
+
console.log(chalk.bold.red("\n \u2716 Performance Budget Violations:"));
|
|
109
|
+
result.violations.forEach((v) => console.log(chalk.red(` \u2022 ${v}`)));
|
|
110
|
+
}
|
|
111
|
+
console.log(chalk.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
112
|
+
if (opts?.outFile || opts?.markdown) {
|
|
113
|
+
const md = generateMarkdownBudgetReport(result, budgets);
|
|
114
|
+
if (opts.outFile) {
|
|
115
|
+
fs.writeFileSync(opts.outFile, md, "utf-8");
|
|
116
|
+
console.log(chalk.dim(` Saved report to ${opts.outFile}
|
|
117
|
+
`));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function generateMarkdownBudgetReport(result, budgets) {
|
|
122
|
+
return [
|
|
123
|
+
`# Pyre Performance Budget Report`,
|
|
124
|
+
``,
|
|
125
|
+
`**Command**: \`${result.command}\` `,
|
|
126
|
+
`**Status**: ${result.violations.length === 0 && result.exitCode === 0 ? "\u2705 PASSED" : "\u274C FAILED"} `,
|
|
127
|
+
`**Exit Code**: \`${result.exitCode}\` `,
|
|
128
|
+
``,
|
|
129
|
+
`| Metric | Measured | Limit | Status |`,
|
|
130
|
+
`| :--- | :--- | :--- | :--- |`,
|
|
131
|
+
`| Duration | ${result.durationSec.toFixed(2)}s | ${budgets.maxTimeSec ? budgets.maxTimeSec + "s" : "N/A"} | ${budgets.maxTimeSec && result.durationSec > budgets.maxTimeSec ? "\u274C FAIL" : "\u2705 PASS"} |`,
|
|
132
|
+
`| Peak Memory | ${formatBytes(result.peakMemoryBytes)} | ${budgets.maxMemoryBytes ? formatBytes(budgets.maxMemoryBytes) : "N/A"} | ${budgets.maxMemoryBytes && result.peakMemoryBytes > budgets.maxMemoryBytes ? "\u274C FAIL" : "\u2705 PASS"} |`,
|
|
133
|
+
`| Peak CPU | ${result.peakCpuPct.toFixed(1)}% | ${budgets.maxCpuPct ? budgets.maxCpuPct + "%" : "N/A"} | ${budgets.maxCpuPct && result.peakCpuPct > budgets.maxCpuPct ? "\u274C FAIL" : "\u2705 PASS"} |`,
|
|
134
|
+
``,
|
|
135
|
+
...result.violations.length > 0 ? [`### Violations`, ...result.violations.map((v) => `- ${v}`)] : []
|
|
136
|
+
].join("\n");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export {
|
|
140
|
+
parseBytesString,
|
|
141
|
+
formatBytes,
|
|
142
|
+
runWithBudget
|
|
143
|
+
};
|
|
@@ -64,7 +64,11 @@ var DEFAULT_CONFIG = {
|
|
|
64
64
|
historyDuration: 60,
|
|
65
65
|
tableDensity: "standard",
|
|
66
66
|
defaultFilter: "all",
|
|
67
|
-
defaultSort: "cpu"
|
|
67
|
+
defaultSort: "cpu",
|
|
68
|
+
optimizeProtectedApps: [],
|
|
69
|
+
optimizeIdleThreshold: "2h",
|
|
70
|
+
optimizeKeepRecent: 5,
|
|
71
|
+
defaultAction: "live"
|
|
68
72
|
};
|
|
69
73
|
function getConfigDir() {
|
|
70
74
|
return process.env.PYRE_CONFIG_DIR || path.join(os.homedir(), ".config", "pyre");
|