@andespindola/crow-skills 0.10.0 → 0.10.1
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 +2 -2
- package/crow_skills/__init__.py +1 -1
- package/crow_skills/cli.py +4 -0
- package/crow_skills/events.py +5 -2
- package/crow_skills/integrations.py +219 -3
- package/crow_skills/resources.py +326 -102
- package/docs/cli.md +28 -14
- package/package.json +1 -1
- package/release.json +14 -14
- package/skills/crow-behaviour/SKILL.md +2 -1
- package/skills/crow-behaviour/references/progress-ledger.md +1 -1
- package/skills/crow-behaviour/references/resources.md +25 -5
- package/skills/crow-behaviour/scripts/crow_ledger.py +7 -1
package/crow_skills/resources.py
CHANGED
|
@@ -8,15 +8,23 @@ memory pressure without the CLI package. The CLI uses the same functions for
|
|
|
8
8
|
|
|
9
9
|
What is read, and nothing else:
|
|
10
10
|
- macOS: one ``sysctl`` call (hw.memsize, hw.ncpu, vm.swapusage,
|
|
11
|
-
kern.memorystatus_vm_pressure_level, kern.memorystatus_level
|
|
12
|
-
only when the kernel does not report
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
kern.memorystatus_vm_pressure_level, kern.memorystatus_level,
|
|
12
|
+
vm.compressor_bytes_used); ``vm_stat`` only when the kernel does not report
|
|
13
|
+
the available level.
|
|
14
|
+
- Linux: /proc/meminfo (MemAvailable, swap), /proc/pressure/memory and
|
|
15
|
+
/proc/pressure/cpu (PSI) when present, and the cgroup v2 memory and CPU limits
|
|
16
|
+
(with their pressure files) when the process runs under one.
|
|
17
|
+
- Load average (``os.getloadavg``) on both.
|
|
18
|
+
- Agent processes (full probe only): pid, parent pid, RSS, CPU% and the
|
|
19
|
+
executable name, from ``ps -axo pid=,ppid=,rss=,%cpu=,comm=`` (macOS) or
|
|
20
|
+
/proc/<pid>/stat (Linux). Names are matched against a fixed list and only the
|
|
21
|
+
agent kind, counts, RSS and CPU% are reported: no command line, argument or
|
|
22
|
+
process name is ever printed or stored.
|
|
23
|
+
|
|
24
|
+
Three axes, each with its own level (normal, warn, critical): memory (available
|
|
25
|
+
percent, OS level, PSI), swap (folded into the memory level) and CPU (load per
|
|
26
|
+
CPU over 1 and 5 minutes, CPU PSI on Linux). Every recommendation is monotonic:
|
|
27
|
+
more pressure on any axis never raises a number (tests walk a grid of inputs).
|
|
20
28
|
|
|
21
29
|
Tests (and only tests) pin the reading with CROW_RESOURCES_FIXTURE, the path of a
|
|
22
30
|
JSON raw reading (optionally with an "agents" object); the cache is then bypassed.
|
|
@@ -30,6 +38,8 @@ import math
|
|
|
30
38
|
import os
|
|
31
39
|
from pathlib import Path
|
|
32
40
|
import re
|
|
41
|
+
import shlex
|
|
42
|
+
import shutil
|
|
33
43
|
import subprocess
|
|
34
44
|
import sys
|
|
35
45
|
import time
|
|
@@ -62,7 +72,19 @@ DEFAULTS = {
|
|
|
62
72
|
"criticalAvailablePercent": 7, # available/total below this -> critical
|
|
63
73
|
"psiWarnSomeAvg10": 10.0, # Linux PSI "some avg10" (%) at or above this -> warn
|
|
64
74
|
"psiCriticalFullAvg10": 10.0, # Linux PSI "full avg10" (%) at or above this -> critical
|
|
65
|
-
|
|
75
|
+
# Swap (folded into the memory level). macOS grows its swap files on demand, so a high share
|
|
76
|
+
# of the swap total alone is routine (72% captured on a calm 18 GB machine); the warning also
|
|
77
|
+
# needs swap in use worth a quarter of the RAM. Critical: swap nearly full while available
|
|
78
|
+
# memory is also low, the state that ends in thrashing (macOS) or the OOM killer (Linux).
|
|
79
|
+
"swapWarnUsedPercent": 75, # swap used/total at or above this...
|
|
80
|
+
"swapWarnRamPercent": 25, # ...and swap used/RAM at or above this -> warn
|
|
81
|
+
"swapCriticalUsedPercent": 90, # swap used/total at or above this...
|
|
82
|
+
"swapCriticalAvailablePercent": 25, # ...and available/total below this -> critical
|
|
83
|
+
# CPU: load average per CPU (runnable threads per core; above 1 work queues).
|
|
84
|
+
"loadPerCpuReduce": 1.5, # 1- or 5-min load / cpus above this -> cpu warn (halves agents, jobs, workers)
|
|
85
|
+
"loadPerCpuCritical": 3.0, # 1-min load / cpus at or above this, sustained (5-min above warn) -> cpu critical
|
|
86
|
+
"psiCpuWarnSomeAvg10": 20.0, # Linux CPU PSI "some avg10" (%) at or above this -> cpu warn
|
|
87
|
+
"psiCpuCriticalSomeAvg10": 50.0, # ... at or above this -> cpu critical
|
|
66
88
|
"cacheSeconds": 5, # hooks and status lines reuse a probe this recent
|
|
67
89
|
}
|
|
68
90
|
LIMITS = { # key -> (minimum, maximum)
|
|
@@ -71,7 +93,13 @@ LIMITS = { # key -> (minimum, maximum)
|
|
|
71
93
|
"warnAvailablePercent": (0, 100), "criticalAvailablePercent": (0, 100),
|
|
72
94
|
"psiWarnSomeAvg10": (0, 100), "psiCriticalFullAvg10": (0, 100),
|
|
73
95
|
"loadPerCpuReduce": (0.1, 1000), "cacheSeconds": (0, 300),
|
|
96
|
+
"swapWarnUsedPercent": (0, 100), "swapWarnRamPercent": (0, 1000), "swapCriticalUsedPercent": (0, 100),
|
|
97
|
+
"swapCriticalAvailablePercent": (0, 100), "loadPerCpuCritical": (0.1, 1000),
|
|
98
|
+
"psiCpuWarnSomeAvg10": (0, 100), "psiCpuCriticalSomeAvg10": (0, 100),
|
|
74
99
|
}
|
|
100
|
+
# (warn key, critical key, critical must be <= warn): an inverted pair falls back to both defaults.
|
|
101
|
+
PAIRS = (("warnAvailablePercent", "criticalAvailablePercent", True), ("swapWarnUsedPercent", "swapCriticalUsedPercent", False),
|
|
102
|
+
("loadPerCpuReduce", "loadPerCpuCritical", False), ("psiCpuWarnSomeAvg10", "psiCpuCriticalSomeAvg10", False))
|
|
75
103
|
INTEGER_KEYS = ("reservedMb", "perAgentMb", "perHeavyJobMb", "perWorkerMb", "maxParallelAgents", "maxParallelHeavyJobs")
|
|
76
104
|
MAX_CONFIG = 64 * 1024
|
|
77
105
|
MB = 1024 * 1024
|
|
@@ -120,10 +148,10 @@ def load_config(home):
|
|
|
120
148
|
effective[key] = int(value) if key in INTEGER_KEYS else float(value)
|
|
121
149
|
except (OSError, ValueError, UnicodeDecodeError):
|
|
122
150
|
errors.append("resources.json ilegível ou JSON inválido; padrões usados.")
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
151
|
+
for warn_key, critical_key, below in PAIRS:
|
|
152
|
+
if (effective[critical_key] > effective[warn_key]) if below else (effective[critical_key] < effective[warn_key]):
|
|
153
|
+
errors.append(critical_key + (" maior que " if below else " menor que ") + warn_key + "; os dois padrões foram restaurados.")
|
|
154
|
+
effective[critical_key], effective[warn_key] = DEFAULTS[critical_key], DEFAULTS[warn_key]
|
|
127
155
|
return effective, {"path": str(path), "loaded": loaded, "errors": errors}
|
|
128
156
|
|
|
129
157
|
|
|
@@ -190,6 +218,10 @@ def macos_reading(sysctl_text, vm_stat_text=None):
|
|
|
190
218
|
reading["swapTotalMb"], reading["swapUsedMb"] = parse_swapusage(values.get("vm.swapusage", ""))
|
|
191
219
|
# kern.memorystatus_vm_pressure_level: 1 normal, 2 warning, 4 critical (the level memory_pressure shows).
|
|
192
220
|
reading["osLevel"] = {"1": "normal", "2": "warn", "4": "critical"}.get(values.get("kern.memorystatus_vm_pressure_level"))
|
|
221
|
+
try:
|
|
222
|
+
reading["compressedMb"] = int(values["vm.compressor_bytes_used"]) / MB # memory held by the compressor (report only)
|
|
223
|
+
except (KeyError, ValueError):
|
|
224
|
+
pass
|
|
193
225
|
try:
|
|
194
226
|
reading["cpus"] = int(values["hw.ncpu"])
|
|
195
227
|
except (KeyError, ValueError):
|
|
@@ -240,9 +272,10 @@ def parse_cpu_max(text):
|
|
|
240
272
|
return max(1, -(-quota // period)) if quota > 0 and period > 0 else None
|
|
241
273
|
|
|
242
274
|
|
|
243
|
-
def linux_reading(meminfo_text, psi_text=None, cgroup=None):
|
|
275
|
+
def linux_reading(meminfo_text, psi_text=None, cgroup=None, cpu_psi_text=None):
|
|
244
276
|
"""cgroup: optional dict with the raw cgroup v2 files of the tightest limit:
|
|
245
|
-
max, current, stat (memory.stat), pressure (memory.pressure)
|
|
277
|
+
max, current, stat (memory.stat), pressure (memory.pressure), cpuMax (cpu.max) and
|
|
278
|
+
cpuPressure (cpu.pressure of the level that sets cpu.max). cpu_psi_text: /proc/pressure/cpu."""
|
|
246
279
|
info = parse_meminfo(meminfo_text)
|
|
247
280
|
if "MemTotal" not in info:
|
|
248
281
|
return None
|
|
@@ -273,30 +306,48 @@ def linux_reading(meminfo_text, psi_text=None, cgroup=None):
|
|
|
273
306
|
cpus = parse_cpu_max(cgroup.get("cpuMax"))
|
|
274
307
|
if cpus:
|
|
275
308
|
reading["cpuQuota"] = cpus
|
|
309
|
+
cpu_psi = parse_psi(cgroup.get("cpuPressure") or cpu_psi_text) if (cgroup.get("cpuPressure") or cpu_psi_text) else None
|
|
310
|
+
if cpu_psi and "someAvg10" in cpu_psi:
|
|
311
|
+
reading["cpuPsi"] = {"someAvg10": cpu_psi["someAvg10"]} # "full" is not meaningful for CPU system-wide
|
|
276
312
|
return reading
|
|
277
313
|
|
|
278
314
|
|
|
279
|
-
def
|
|
280
|
-
|
|
315
|
+
def _percent(text):
|
|
316
|
+
try:
|
|
317
|
+
value = float(text.replace(",", "."))
|
|
318
|
+
except ValueError:
|
|
319
|
+
return None
|
|
320
|
+
return value if math.isfinite(value) and value >= 0 else None
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def parse_ps(text, cpu=False):
|
|
324
|
+
"""``ps -axo pid=,ppid=,rss=,comm=`` -> [(pid, ppid, rss_kb, basename, None)]; with cpu,
|
|
325
|
+
``ps -axo pid=,ppid=,rss=,%cpu=,comm=`` -> [(pid, ppid, rss_kb, basename, cpu_percent)]."""
|
|
281
326
|
rows = []
|
|
282
327
|
for line in (text or "").splitlines():
|
|
283
|
-
parts = line.split(None, 3)
|
|
284
|
-
if len(parts) < 4 or not (parts[0].isdigit() and parts[1].isdigit() and parts[2].isdigit()):
|
|
328
|
+
parts = line.split(None, 4 if cpu else 3)
|
|
329
|
+
if len(parts) < (5 if cpu else 4) or not (parts[0].isdigit() and parts[1].isdigit() and parts[2].isdigit()):
|
|
330
|
+
continue
|
|
331
|
+
percent = _percent(parts[3]) if cpu else None
|
|
332
|
+
if cpu and percent is None:
|
|
285
333
|
continue
|
|
286
|
-
rows.append((int(parts[0]), int(parts[1]), int(parts[2]), os.path.basename(parts[
|
|
334
|
+
rows.append((int(parts[0]), int(parts[1]), int(parts[2]), os.path.basename(parts[-1].strip()), percent))
|
|
287
335
|
return rows
|
|
288
336
|
|
|
289
337
|
|
|
290
338
|
def agent_trees(rows):
|
|
291
339
|
"""Agent CLI process trees: roots are agent processes with no agent ancestor; each tree's
|
|
292
|
-
RSS
|
|
293
|
-
MB are returned."""
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
340
|
+
RSS and CPU% include every descendant (node workers, MCP servers, shells). Only kinds,
|
|
341
|
+
counts, MB and CPU% are returned. Rows: (pid, ppid, rss_kb, name[, cpu_percent or None])."""
|
|
342
|
+
rows = [tuple(r) + (None,) * (5 - len(r)) for r in rows]
|
|
343
|
+
parent = {r[0]: r[1] for r in rows}
|
|
344
|
+
rss = {r[0]: r[2] for r in rows}
|
|
345
|
+
cpu = {r[0]: r[4] for r in rows}
|
|
346
|
+
has_cpu = any(r[4] is not None for r in rows)
|
|
347
|
+
kind = {r[0]: AGENT_NAMES.get(r[3].lower()) for r in rows}
|
|
297
348
|
children = {}
|
|
298
|
-
for
|
|
299
|
-
children.setdefault(
|
|
349
|
+
for r in rows:
|
|
350
|
+
children.setdefault(r[1], []).append(r[0])
|
|
300
351
|
|
|
301
352
|
def has_agent_ancestor(pid):
|
|
302
353
|
seen, current = set(), parent.get(pid)
|
|
@@ -307,23 +358,31 @@ def agent_trees(rows):
|
|
|
307
358
|
current = parent.get(current)
|
|
308
359
|
return False
|
|
309
360
|
|
|
310
|
-
by_kind, total = {}, 0
|
|
311
|
-
for
|
|
361
|
+
by_kind, total, total_cpu = {}, 0, 0.0
|
|
362
|
+
for r in rows:
|
|
363
|
+
pid = r[0]
|
|
312
364
|
if not kind.get(pid) or has_agent_ancestor(pid):
|
|
313
365
|
continue
|
|
314
|
-
stack, seen, tree_kb = [pid], set(), 0
|
|
366
|
+
stack, seen, tree_kb, tree_cpu = [pid], set(), 0, 0.0
|
|
315
367
|
while stack:
|
|
316
368
|
current = stack.pop()
|
|
317
369
|
if current in seen:
|
|
318
370
|
continue
|
|
319
371
|
seen.add(current)
|
|
320
372
|
tree_kb += rss.get(current, 0)
|
|
373
|
+
tree_cpu += cpu.get(current) or 0.0
|
|
321
374
|
stack.extend(children.get(current, []))
|
|
322
|
-
entry = by_kind.setdefault(kind[pid], {"count": 0, "rssMb": 0})
|
|
375
|
+
entry = by_kind.setdefault(kind[pid], dict({"count": 0, "rssMb": 0}, **({"cpuPercent": 0.0} if has_cpu else {})))
|
|
323
376
|
entry["count"] += 1
|
|
324
377
|
entry["rssMb"] += round(tree_kb / 1024)
|
|
378
|
+
if has_cpu:
|
|
379
|
+
entry["cpuPercent"] = round(entry["cpuPercent"] + tree_cpu, 1)
|
|
325
380
|
total += tree_kb
|
|
326
|
-
|
|
381
|
+
total_cpu += tree_cpu
|
|
382
|
+
result = {"count": sum(e["count"] for e in by_kind.values()), "rssMb": round(total / 1024), "byKind": by_kind}
|
|
383
|
+
if has_cpu:
|
|
384
|
+
result["cpuPercent"] = round(total_cpu, 1) # % of one CPU, summed (ps convention): 250 = 2.5 CPUs busy
|
|
385
|
+
return result
|
|
327
386
|
|
|
328
387
|
|
|
329
388
|
# --- readers (side effects: one subprocess or a few /proc reads) ----------------------------
|
|
@@ -368,6 +427,7 @@ def cgroup_files():
|
|
|
368
427
|
cpus = parse_cpu_max(cpu_text)
|
|
369
428
|
if cpus and (best_cpu is None or cpus < best_cpu):
|
|
370
429
|
best_cpu, result["cpuMax"] = cpus, cpu_text
|
|
430
|
+
result["cpuPressure"] = _read(directory / "cpu.pressure", 512)
|
|
371
431
|
if directory == root:
|
|
372
432
|
break
|
|
373
433
|
return result
|
|
@@ -378,11 +438,12 @@ def read_machine(platform=None):
|
|
|
378
438
|
platform = platform or sys.platform
|
|
379
439
|
reading = None
|
|
380
440
|
if platform == "darwin":
|
|
381
|
-
text = _run(["sysctl", "hw.memsize", "hw.ncpu", "vm.swapusage", "kern.memorystatus_vm_pressure_level", "kern.memorystatus_level"
|
|
441
|
+
text = _run(["sysctl", "hw.memsize", "hw.ncpu", "vm.swapusage", "kern.memorystatus_vm_pressure_level", "kern.memorystatus_level",
|
|
442
|
+
"vm.compressor_bytes_used"])
|
|
382
443
|
values = parse_sysctl(text)
|
|
383
444
|
reading = macos_reading(text, _run(["vm_stat"]) if "kern.memorystatus_level" not in values else None)
|
|
384
445
|
elif platform.startswith("linux"):
|
|
385
|
-
reading = linux_reading(_read("/proc/meminfo"), _read("/proc/pressure/memory"), cgroup_files())
|
|
446
|
+
reading = linux_reading(_read("/proc/meminfo"), _read("/proc/pressure/memory"), cgroup_files(), _read("/proc/pressure/cpu", 512))
|
|
386
447
|
if reading is None:
|
|
387
448
|
reading = {"platform": platform if platform in ("darwin",) or platform.startswith("linux") else "other",
|
|
388
449
|
"source": "unavailable", "totalMb": None, "availableMb": None, "swapTotalMb": None, "swapUsedMb": None,
|
|
@@ -393,7 +454,10 @@ def read_machine(platform=None):
|
|
|
393
454
|
except (AttributeError, OSError):
|
|
394
455
|
reading["cpus"] = os.cpu_count() or 1
|
|
395
456
|
if reading.get("cpuQuota"):
|
|
396
|
-
|
|
457
|
+
quota = reading.pop("cpuQuota")
|
|
458
|
+
if quota < reading["cpus"]:
|
|
459
|
+
reading["loadCpus"] = os.cpu_count() or reading["cpus"] # load average is host-wide
|
|
460
|
+
reading["cpus"] = quota
|
|
397
461
|
try:
|
|
398
462
|
reading["load"] = [round(x, 2) for x in os.getloadavg()]
|
|
399
463
|
except (AttributeError, OSError):
|
|
@@ -407,7 +471,9 @@ def read_processes(platform=None):
|
|
|
407
471
|
rows, page_kb = [], (os.sysconf("SC_PAGE_SIZE") // 1024 if hasattr(os, "sysconf") else 4)
|
|
408
472
|
try:
|
|
409
473
|
pids = [p for p in os.listdir("/proc") if p.isdigit()]
|
|
410
|
-
|
|
474
|
+
ticks = os.sysconf("SC_CLK_TCK")
|
|
475
|
+
uptime = float((_read("/proc/uptime", 128) or "").split()[0])
|
|
476
|
+
except (OSError, ValueError, IndexError, AttributeError):
|
|
411
477
|
return None
|
|
412
478
|
for pid in pids:
|
|
413
479
|
stat = _read("/proc/" + pid + "/stat", 4096)
|
|
@@ -417,13 +483,18 @@ def read_processes(platform=None):
|
|
|
417
483
|
name = stat[stat.find("(") + 1:stat.rfind(")")]
|
|
418
484
|
fields = stat[stat.rfind(")") + 2:].split()
|
|
419
485
|
try:
|
|
420
|
-
|
|
421
|
-
|
|
486
|
+
# CPU%: time used over the process lifetime (what Linux ps reports as %cpu).
|
|
487
|
+
elapsed = uptime - int(fields[19]) / ticks
|
|
488
|
+
busy = (int(fields[11]) + int(fields[12])) / ticks
|
|
489
|
+
rows.append((int(pid), int(fields[1]), int(statm.split()[1]) * page_kb, name,
|
|
490
|
+
round(100.0 * busy / elapsed, 1) if elapsed > 0 else 0.0))
|
|
491
|
+
except (IndexError, ValueError, ZeroDivisionError):
|
|
422
492
|
continue
|
|
423
493
|
return agent_trees(rows)
|
|
424
494
|
if platform == "darwin" or "bsd" in platform:
|
|
425
|
-
|
|
426
|
-
|
|
495
|
+
# %cpu: macOS ps reports a decaying average over roughly the last minute.
|
|
496
|
+
text = _run(["ps", "-axo", "pid=,ppid=,rss=,%cpu=,comm="], timeout=3.0)
|
|
497
|
+
return agent_trees(parse_ps(text, cpu=True)) if text else None
|
|
427
498
|
return None
|
|
428
499
|
|
|
429
500
|
|
|
@@ -435,12 +506,31 @@ def worst(*levels):
|
|
|
435
506
|
return max(known, key=order.get) if known else None
|
|
436
507
|
|
|
437
508
|
|
|
509
|
+
def swap_level(reading, cfg, available_percent):
|
|
510
|
+
"""Swap axis: {'level', 'usedPercent', 'ramPercent'}, or None without swap."""
|
|
511
|
+
total, used, ram = reading.get("swapTotalMb"), reading.get("swapUsedMb"), reading.get("totalMb")
|
|
512
|
+
if not total or used is None or not ram:
|
|
513
|
+
return None
|
|
514
|
+
used_percent, ram_percent = 100.0 * used / total, 100.0 * used / ram
|
|
515
|
+
# Critical also needs swap worth swapWarnRamPercent of the RAM: a small swap full on a machine
|
|
516
|
+
# with plenty of RAM (common on Linux servers) is not the thrashing state.
|
|
517
|
+
if used_percent >= cfg["swapCriticalUsedPercent"] and ram_percent >= cfg["swapWarnRamPercent"] \
|
|
518
|
+
and available_percent is not None and available_percent < cfg["swapCriticalAvailablePercent"]:
|
|
519
|
+
level = "critical"
|
|
520
|
+
elif used_percent >= cfg["swapWarnUsedPercent"] and ram_percent >= cfg["swapWarnRamPercent"]:
|
|
521
|
+
level = "warn"
|
|
522
|
+
else:
|
|
523
|
+
level = "normal"
|
|
524
|
+
return {"level": level, "usedPercent": round(used_percent, 1), "ramPercent": round(ram_percent, 1)}
|
|
525
|
+
|
|
526
|
+
|
|
438
527
|
def assess(reading, cfg):
|
|
439
|
-
"""
|
|
528
|
+
"""Memory pressure for a raw reading: the worst of the OS level, PSI, available percent and
|
|
529
|
+
swap. ``ram`` is the same without swap (the status line shows swap on its own)."""
|
|
440
530
|
total, available = reading.get("totalMb"), reading.get("availableMb")
|
|
441
531
|
reasons = []
|
|
442
532
|
if not total or available is None:
|
|
443
|
-
return {"level": "unknown", "os": reading.get("osLevel"), "psi": reading.get("psi"),
|
|
533
|
+
return {"level": "unknown", "ram": "unknown", "os": reading.get("osLevel"), "psi": reading.get("psi"), "swap": None,
|
|
444
534
|
"reasons": ["memória não medida nesta plataforma; recomendação conservadora"]}
|
|
445
535
|
percent = 100.0 * available / total
|
|
446
536
|
by_percent = "critical" if percent < cfg["criticalAvailablePercent"] else "warn" if percent < cfg["warnAvailablePercent"] else "normal"
|
|
@@ -460,65 +550,136 @@ def assess(reading, cfg):
|
|
|
460
550
|
psi_level = "normal"
|
|
461
551
|
if reading.get("osLevel") in ("warn", "critical"):
|
|
462
552
|
reasons.append("pressão do sistema " + reading["osLevel"])
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
553
|
+
ram = worst(by_percent, psi_level, reading.get("osLevel"))
|
|
554
|
+
swap = swap_level(reading, cfg, percent)
|
|
555
|
+
if swap and swap["level"] != "normal":
|
|
556
|
+
reasons.append("swap " + str(round(swap["usedPercent"])) + "% em uso (" + str(round(swap["ramPercent"])) + "% da RAM)")
|
|
557
|
+
return {"level": worst(ram, swap["level"] if swap else None), "ram": ram, "os": reading.get("osLevel"), "psi": psi,
|
|
558
|
+
"swap": swap, "availablePercent": round(percent, 1), "reasons": reasons}
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def assess_cpu(reading, cfg):
|
|
562
|
+
"""CPU pressure: load average per CPU (1 and 5 min) and, on Linux, CPU PSI."""
|
|
563
|
+
# The load average counts the whole host: inside a container with a CPU quota it is divided
|
|
564
|
+
# by the host CPUs (loadCpus); the quota's own contention shows in the cgroup cpu.pressure.
|
|
565
|
+
cpus = max(1, int(reading.get("loadCpus") or reading.get("cpus") or 1))
|
|
566
|
+
load, psi = reading.get("load"), reading.get("cpuPsi")
|
|
567
|
+
per1 = per5 = None
|
|
568
|
+
level, reasons = "normal", []
|
|
569
|
+
if load:
|
|
570
|
+
per1, per5 = load[0] / cpus, load[1] / cpus
|
|
571
|
+
if per1 >= cfg["loadPerCpuCritical"] and per5 > cfg["loadPerCpuReduce"]:
|
|
572
|
+
level = "critical"
|
|
573
|
+
elif per1 > cfg["loadPerCpuReduce"] or per5 > cfg["loadPerCpuReduce"]:
|
|
574
|
+
level = "warn"
|
|
575
|
+
if level != "normal":
|
|
576
|
+
reasons.append("carga " + str(load[0]) + "/" + str(load[1]) + " em " + str(cpus) + " CPUs (" + ("%.1f" % per1) + "×/" +
|
|
577
|
+
("%.1f" % per5) + "× por CPU em 1/5 min)")
|
|
578
|
+
if psi and psi.get("someAvg10") is not None:
|
|
579
|
+
by_psi = "critical" if psi["someAvg10"] >= cfg["psiCpuCriticalSomeAvg10"] else \
|
|
580
|
+
"warn" if psi["someAvg10"] >= cfg["psiCpuWarnSomeAvg10"] else "normal"
|
|
581
|
+
if by_psi != "normal":
|
|
582
|
+
reasons.append("PSI cpu some avg10 " + str(psi["someAvg10"]))
|
|
583
|
+
level = worst(level, by_psi)
|
|
584
|
+
if not load and not psi:
|
|
585
|
+
return {"level": "unknown", "loadPerCpu1": None, "loadPerCpu5": None, "psi": None, "reasons": []}
|
|
586
|
+
return {"level": level, "loadPerCpu1": round(per1, 2) if per1 is not None else None,
|
|
587
|
+
"loadPerCpu5": round(per5, 2) if per5 is not None else None, "psi": psi, "reasons": reasons}
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def recommend(reading, pressure, cfg, cpu_pressure=None):
|
|
591
|
+
"""maxParallelAgents, maxParallelHeavyJobs, maxWorkersPerJob and mode for this reading.
|
|
592
|
+
|
|
593
|
+
Monotonic by construction: every number starts from a base that does not depend on load,
|
|
594
|
+
and pressure only divides it (warn on memory/swap or CPU halves once; memory critical: 1 agent,
|
|
595
|
+
no new heavy job; CPU critical: 1 agent, 1 job, 1 worker)."""
|
|
469
596
|
cpus = max(1, int(reading.get("cpus") or 1))
|
|
470
597
|
level = pressure["level"]
|
|
598
|
+
cpu_level = (cpu_pressure or assess_cpu(reading, cfg))["level"]
|
|
471
599
|
reasons = []
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
return {"mode": "reduced" if agents > 1 else "sequential", "maxParallelAgents": agents, "maxParallelHeavyJobs": heavy,
|
|
476
|
-
"maxWorkersPerJob": workers, "budgetMb": None, "reasons": ["sem medição: assuma reduced"]}
|
|
477
|
-
reserve = min(cfg["reservedMb"], reading["totalMb"] * cfg["reservedPercent"] / 100.0)
|
|
478
|
-
budget = max(0.0, reading["availableMb"] - reserve)
|
|
600
|
+
# Reference job count: the CPU-shaped cap, independent of memory and load. Workers per job
|
|
601
|
+
# come from it, so fewer jobs under pressure never mean more workers each.
|
|
602
|
+
ref = max(1, min(cpus // 4, cfg["maxParallelHeavyJobs"]))
|
|
479
603
|
if level == "critical":
|
|
604
|
+
budget = max(0.0, reading["availableMb"] - min(cfg["reservedMb"], reading["totalMb"] * cfg["reservedPercent"] / 100.0))
|
|
480
605
|
return {"mode": "sequential", "maxParallelAgents": 1, "maxParallelHeavyJobs": 0, "maxWorkersPerJob": 1,
|
|
481
606
|
"budgetMb": round(budget), "reasons": ["pressão crítica: sequencial, sem novos jobs pesados"] + pressure["reasons"]}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
607
|
+
if level == "unknown":
|
|
608
|
+
# Never above what a measured calm machine gets (cpus // ref), halved: unknown is conservative.
|
|
609
|
+
agents, heavy, workers, budget = min(2, cfg["maxParallelAgents"]), 1, max(1, (cpus // ref) // 2), None
|
|
610
|
+
reasons.append("sem medição: assuma reduced")
|
|
611
|
+
else:
|
|
612
|
+
reserve = min(cfg["reservedMb"], reading["totalMb"] * cfg["reservedPercent"] / 100.0)
|
|
613
|
+
budget = max(0.0, reading["availableMb"] - reserve)
|
|
614
|
+
memory_agents = int(budget // cfg["perAgentMb"])
|
|
615
|
+
agents = max(1, min(memory_agents, cfg["maxParallelAgents"]))
|
|
616
|
+
memory_heavy = int(budget // cfg["perHeavyJobMb"])
|
|
617
|
+
heavy = max(1, min(memory_heavy, ref))
|
|
618
|
+
workers = max(1, min(cpus // ref, int((budget / ref) // cfg["perWorkerMb"])))
|
|
619
|
+
if memory_agents < cfg["maxParallelAgents"]:
|
|
620
|
+
reasons.append("memória: " + str(round(budget)) + " MB livres além da reserva, " + str(cfg["perAgentMb"]) + " MB por agente")
|
|
621
|
+
if memory_heavy < 1:
|
|
622
|
+
reasons.append("memória abaixo de um job pesado (" + str(cfg["perHeavyJobMb"]) + " MB): no máximo um, sem nada pesado em paralelo")
|
|
623
|
+
if level == "warn":
|
|
624
|
+
reasons += ["pressão de memória warn: paralelismo pela metade"] + pressure["reasons"]
|
|
625
|
+
cpu_reasons = (cpu_pressure or {}).get("reasons") or []
|
|
626
|
+
if cpu_level == "warn":
|
|
627
|
+
reasons += ["pressão de CPU warn: agentes, jobs e workers pela metade"] + cpu_reasons
|
|
628
|
+
if level == "warn" or cpu_level == "warn": # once, whichever axes are in warn
|
|
629
|
+
agents, heavy, workers = max(1, agents // 2), max(1, heavy // 2), max(1, workers // 2)
|
|
630
|
+
if cpu_level == "critical":
|
|
631
|
+
agents, heavy, workers = 1, 1, 1
|
|
632
|
+
reasons += ["pressão de CPU crítica: um agente, um job pesado e um worker por vez"] + cpu_reasons
|
|
501
633
|
# Sizing by free memory at normal pressure is already in the numbers; "reduced" means the
|
|
502
|
-
# machine is struggling (
|
|
503
|
-
|
|
634
|
+
# machine is struggling (memory, swap or CPU), "sequential" that there is room for one agent only.
|
|
635
|
+
struggling = level in ("warn", "unknown") or cpu_level in ("warn", "critical")
|
|
636
|
+
mode = "sequential" if agents <= 1 else "reduced" if struggling else "parallel"
|
|
504
637
|
return {"mode": mode, "maxParallelAgents": agents, "maxParallelHeavyJobs": heavy, "maxWorkersPerJob": workers,
|
|
505
|
-
"budgetMb": round(budget), "reasons": reasons}
|
|
638
|
+
"budgetMb": round(budget) if budget is not None else None, "reasons": reasons}
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def low_priority(platform=None, which=None):
|
|
642
|
+
"""The command prefix that runs a heavy job at low priority on this OS: (argv, text).
|
|
643
|
+
|
|
644
|
+
macOS: QoS clamp "utility" (taskpolicy -c utility) plus nice 10; ``taskpolicy -b`` is the
|
|
645
|
+
stronger option for jobs that can wait (efficiency cores and throttled I/O on Apple silicon).
|
|
646
|
+
Linux: nice 10 plus the lowest best-effort I/O class (ionice -c 2 -n 7) when ionice exists.
|
|
647
|
+
Windows: ``start "" /low /b /wait`` inside cmd. It changes only the job it prefixes, never a
|
|
648
|
+
host limit, and is empty when no known tool exists."""
|
|
649
|
+
platform = platform or sys.platform
|
|
650
|
+
which = which or shutil.which
|
|
651
|
+
if platform.startswith("win"):
|
|
652
|
+
argv = ["cmd", "/c", "start", '""', "/low", "/b", "/wait"]
|
|
653
|
+
return argv, 'cmd /c start "" /low /b /wait'
|
|
654
|
+
argv = []
|
|
655
|
+
if platform == "darwin" and which("taskpolicy"):
|
|
656
|
+
argv += ["taskpolicy", "-c", "utility"]
|
|
657
|
+
if which("nice"):
|
|
658
|
+
argv += ["nice", "-n", "10"]
|
|
659
|
+
if platform.startswith("linux") and which("ionice"):
|
|
660
|
+
argv += ["ionice", "-c", "2", "-n", "7"]
|
|
661
|
+
return argv, shlex.join(argv)
|
|
506
662
|
|
|
507
663
|
|
|
508
664
|
def build(reading, cfg, meta, agents=None, cached=False, age=0):
|
|
509
665
|
pressure = assess(reading, cfg)
|
|
666
|
+
cpu_pressure = assess_cpu(reading, cfg)
|
|
510
667
|
total, available = reading.get("totalMb"), reading.get("availableMb")
|
|
511
668
|
memory = {"totalMb": round(total) if total else None, "availableMb": round(available) if available is not None else None,
|
|
512
669
|
"usedPercent": round(100.0 * (1 - available / total), 1) if total and available is not None else None,
|
|
513
670
|
"swapTotalMb": round(reading["swapTotalMb"]) if reading.get("swapTotalMb") is not None else None,
|
|
514
671
|
"swapUsedMb": round(reading["swapUsedMb"]) if reading.get("swapUsedMb") is not None else None,
|
|
515
672
|
"cgroupLimitMb": round(reading["cgroupLimitMb"]) if reading.get("cgroupLimitMb") else None}
|
|
673
|
+
if reading.get("compressedMb") is not None:
|
|
674
|
+
memory["compressedMb"] = round(reading["compressedMb"])
|
|
516
675
|
load = reading.get("load")
|
|
676
|
+
argv, prefix = low_priority(reading.get("platform") if reading.get("platform") in ("darwin", "linux") else None)
|
|
517
677
|
return {"schema": SCHEMA, "generatedAt": now_iso(), "platform": reading.get("platform"), "source": reading.get("source"),
|
|
518
678
|
"cached": cached, "ageSeconds": int(age), "memory": memory, "pressure": pressure,
|
|
519
|
-
"cpu": {"count": reading.get("cpus"), "load1": load[0] if load else None, "load5": load[1] if load else None,
|
|
520
|
-
|
|
521
|
-
"agents": agents, "recommendation": recommend(reading, pressure, cfg
|
|
679
|
+
"cpu": dict({"count": reading.get("cpus"), "load1": load[0] if load else None, "load5": load[1] if load else None,
|
|
680
|
+
"load15": load[2] if load else None}, **cpu_pressure),
|
|
681
|
+
"agents": agents, "recommendation": recommend(reading, pressure, cfg, cpu_pressure),
|
|
682
|
+
"lowPriorityPrefix": prefix, "lowPriorityArgv": argv, "thresholds": cfg, "config": meta}
|
|
522
683
|
|
|
523
684
|
|
|
524
685
|
# --- cache ----------------------------------------------------------------------------------
|
|
@@ -556,12 +717,14 @@ def valid_reading(reading):
|
|
|
556
717
|
return False # availableMb may be unmeasured (None) while totalMb is known: assess() reports unknown
|
|
557
718
|
if not number(reading.get("cpus"), allow_none=False) or reading.get("osLevel") not in (None, "normal", "warn", "critical"):
|
|
558
719
|
return False
|
|
559
|
-
load
|
|
720
|
+
load = reading.get("load")
|
|
560
721
|
if load is not None and not (isinstance(load, list) and len(load) == 3 and all(number(x, False) for x in load)):
|
|
561
722
|
return False
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
723
|
+
for key in ("psi", "cpuPsi"):
|
|
724
|
+
psi = reading.get(key)
|
|
725
|
+
if psi is not None and not (isinstance(psi, dict) and all(number(v, False) for v in psi.values())):
|
|
726
|
+
return False
|
|
727
|
+
return number(reading.get("compressedMb")) and number(reading.get("loadCpus"))
|
|
565
728
|
|
|
566
729
|
|
|
567
730
|
def write_cache(home, reading):
|
|
@@ -633,76 +796,137 @@ def probe(home, processes=True, use_cache=False, platform=None):
|
|
|
633
796
|
|
|
634
797
|
# --- presentation ---------------------------------------------------------------------------
|
|
635
798
|
|
|
799
|
+
def cpu_level(data):
|
|
800
|
+
return (data.get("cpu") or {}).get("level") or "normal"
|
|
801
|
+
|
|
802
|
+
|
|
636
803
|
def alert(data):
|
|
637
|
-
"""True when the machine needs attention: pressure warn/critical or a
|
|
638
|
-
|
|
804
|
+
"""True when the machine needs attention: memory, swap or CPU pressure warn/critical, or a
|
|
805
|
+
non-parallel recommendation."""
|
|
806
|
+
return data["pressure"]["level"] in ("warn", "critical") or cpu_level(data) in ("warn", "critical") \
|
|
807
|
+
or data["recommendation"]["mode"] != "parallel"
|
|
639
808
|
|
|
640
809
|
|
|
641
810
|
def gb(mb):
|
|
642
811
|
return "?" if mb is None else ("%.1f" % (mb / 1024)).replace(".", ",")
|
|
643
812
|
|
|
644
813
|
|
|
814
|
+
def times_cpu(value):
|
|
815
|
+
return ("%.1f" % value).replace(".", ",") + "×"
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
# Under pressure the hint also carries the conduct rules (crow-behaviour references/resources.md),
|
|
819
|
+
# so a session without the skill loaded still reduces load the same way.
|
|
820
|
+
CONDUCT = (" Reduza a concorrência já, inclusive a planejada; um job pesado por vez, sem iniciar outro enquanto um roda "
|
|
821
|
+
"(deixe terminar o que está perto do fim), em prioridade baixa ({prefix}); não repita suíte/build já verificado "
|
|
822
|
+
"nesta revisão; validação ampla no CI do repositório quando houver, lendo o resultado antes de declarar verificado.")
|
|
823
|
+
|
|
824
|
+
|
|
645
825
|
def hint(data):
|
|
646
826
|
"""One line for a prompt hook's additionalContext, or None when there is nothing to say."""
|
|
647
827
|
if not alert(data):
|
|
648
828
|
return None
|
|
649
|
-
rec, memory = data["recommendation"], data["memory"]
|
|
650
|
-
level =
|
|
829
|
+
rec, memory, pressure = data["recommendation"], data["memory"], data["pressure"]
|
|
830
|
+
level = pressure["level"]
|
|
651
831
|
if level == "unknown":
|
|
652
832
|
head = "[crow-skills] memória: não medida nesta plataforma."
|
|
653
833
|
else:
|
|
654
834
|
head = ("[crow-skills] memória: pressão " + level + ", " + gb(memory["availableMb"]) + " GB livres de " +
|
|
655
|
-
gb(memory["totalMb"]) + " GB
|
|
835
|
+
gb(memory["totalMb"]) + " GB")
|
|
836
|
+
swap = pressure.get("swap")
|
|
837
|
+
if swap and swap["level"] != "normal":
|
|
838
|
+
head += "; swap " + str(int(round(swap["usedPercent"]))) + "% em uso (" + swap["level"] + ")"
|
|
839
|
+
head += "."
|
|
840
|
+
cpu = data.get("cpu") or {}
|
|
841
|
+
if cpu_level(data) in ("warn", "critical"):
|
|
842
|
+
head += " CPU: pressão " + cpu["level"] + (", carga " + times_cpu(cpu["loadPerCpu1"]) + " por CPU"
|
|
843
|
+
if cpu.get("loadPerCpu1") is not None else "") + "."
|
|
656
844
|
if rec["mode"] == "sequential" and rec["maxParallelHeavyJobs"] == 0:
|
|
657
845
|
body = " Recomendação sequential: um agente por vez, não inicie builds/testes/dev servers novos e encerre os que esta sessão abriu e não usa."
|
|
658
846
|
else:
|
|
659
847
|
body = (" Recomendação " + rec["mode"] + ": até " + str(rec["maxParallelAgents"]) + " agente(s) e " +
|
|
660
848
|
str(rec["maxParallelHeavyJobs"]) + " job(s) pesado(s) em paralelo, " + str(rec["maxWorkersPerJob"]) + " worker(s) por job.")
|
|
661
|
-
|
|
849
|
+
prefix = data.get("lowPriorityPrefix")
|
|
850
|
+
conduct = CONDUCT.format(prefix=("`" + prefix + "`") if prefix else "crow-skills resources --exec-prefix")
|
|
851
|
+
return head + body + conduct + " Detalhe: crow-skills resources --json."
|
|
662
852
|
|
|
663
853
|
|
|
664
854
|
def segment(data, unicode=True):
|
|
665
|
-
"""Compact status line/tmux segment, empty when the machine is fine (e.g. 'mem ▲ 85% ×2'
|
|
855
|
+
"""Compact status line/tmux segment, empty when the machine is fine (e.g. 'mem ▲ 85% ×2',
|
|
856
|
+
'cpu ▲ 1.9× swap ▲ 92% ×3'): one part per axis under pressure, then the agents."""
|
|
666
857
|
if not alert(data):
|
|
667
858
|
return ""
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
# The arrow is for real pressure only; "sequential" at normal pressure shows just the numbers.
|
|
671
|
-
arrow = {"warn": "▲" if unicode else "^", "critical": "▲▲" if unicode else "^^"}.get(level)
|
|
859
|
+
pressure = data["pressure"]
|
|
860
|
+
arrows = {"warn": "▲" if unicode else "^", "critical": "▲▲" if unicode else "^^"}
|
|
672
861
|
times = "×" if unicode else "x"
|
|
673
|
-
|
|
674
|
-
|
|
862
|
+
parts = []
|
|
863
|
+
swap = pressure.get("swap") or {}
|
|
864
|
+
cpu = data.get("cpu") or {}
|
|
865
|
+
swap_on = swap.get("level") in arrows
|
|
866
|
+
cpu_on = cpu_level(data) in arrows
|
|
867
|
+
ram = pressure.get("ram", pressure["level"])
|
|
868
|
+
# Memory shows when it is the axis under pressure, or when nothing else explains the alert
|
|
869
|
+
# ("sequential" at normal pressure shows just the numbers, without an arrow).
|
|
870
|
+
if ram in arrows or not (swap_on or cpu_on):
|
|
871
|
+
used = data["memory"]["usedPercent"]
|
|
872
|
+
parts.append("mem " + (arrows[ram] + " " if ram in arrows else "") + (str(int(round(used))) + "%" if used is not None else "?"))
|
|
873
|
+
if cpu_on:
|
|
874
|
+
parts.append("cpu " + arrows[cpu["level"]] + (" " + ("%.1f" % cpu["loadPerCpu1"]) + times if cpu.get("loadPerCpu1") is not None else ""))
|
|
875
|
+
if swap_on:
|
|
876
|
+
parts.append("swap " + arrows[swap["level"]] + " " + str(int(round(swap["usedPercent"]))) + "%")
|
|
877
|
+
return " ".join(parts) + " " + times + str(data["recommendation"]["maxParallelAgents"])
|
|
675
878
|
|
|
676
879
|
|
|
677
880
|
def render(data):
|
|
678
881
|
memory, pressure, rec, cpu = data["memory"], data["pressure"], data["recommendation"], data["cpu"]
|
|
882
|
+
swap = pressure.get("swap")
|
|
679
883
|
lines = ["memória: " + gb(memory["availableMb"]) + " GB livres de " + gb(memory["totalMb"]) + " GB" +
|
|
680
884
|
(" (" + str(int(round(memory["usedPercent"]))) + "% em uso)" if memory["usedPercent"] is not None else "") +
|
|
681
|
-
(" · swap " + gb(memory["swapUsedMb"]) + "/" + gb(memory["swapTotalMb"]) + " GB"
|
|
885
|
+
(" · swap " + gb(memory["swapUsedMb"]) + "/" + gb(memory["swapTotalMb"]) + " GB" +
|
|
886
|
+
(" (" + str(int(round(swap["usedPercent"]))) + "%)" if swap else "") if memory["swapTotalMb"] else "") +
|
|
887
|
+
(" · comprimida " + gb(memory["compressedMb"]) + " GB" if memory.get("compressedMb") else "") +
|
|
682
888
|
(" · limite cgroup " + gb(memory["cgroupLimitMb"]) + " GB" if memory["cgroupLimitMb"] else ""),
|
|
683
889
|
"pressão: " + pressure["level"] + (" (" + "; ".join(pressure["reasons"]) + ")" if pressure["reasons"] else ""),
|
|
684
890
|
"cpu: " + str(cpu["count"]) + (" · carga " + str(cpu["load1"]) + " " + str(cpu["load5"]) + " " + str(cpu["load15"])
|
|
685
|
-
if cpu["load1"] is not None else "")
|
|
891
|
+
if cpu["load1"] is not None else "") +
|
|
892
|
+
(" (" + times_cpu(cpu["loadPerCpu1"]) + " por CPU)" if cpu.get("loadPerCpu1") is not None else "") +
|
|
893
|
+
(" · PSI some " + str(cpu["psi"]["someAvg10"]) if cpu.get("psi") else "") +
|
|
894
|
+
" · pressão " + cpu_level(data)]
|
|
686
895
|
agents = data.get("agents")
|
|
687
896
|
if agents is not None:
|
|
688
|
-
|
|
689
|
-
|
|
897
|
+
def cpu_text(entry):
|
|
898
|
+
return (", " + ("%.0f" % entry["cpuPercent"]) + "% CPU") if entry.get("cpuPercent") is not None else ""
|
|
899
|
+
kinds = ", ".join(k + " " + str(v["count"]) + " (" + gb(v["rssMb"]) + " GB" + cpu_text(v) + ")" for k, v in sorted(agents["byKind"].items()))
|
|
900
|
+
lines.append("agentes: " + str(agents["count"]) + " CLI(s), " + gb(agents["rssMb"]) + " GB RSS" + cpu_text(agents) +
|
|
901
|
+
(" · " + kinds if kinds else ""))
|
|
690
902
|
lines.append("recomendação: " + rec["mode"] + " · até " + str(rec["maxParallelAgents"]) + " agente(s), " +
|
|
691
903
|
str(rec["maxParallelHeavyJobs"]) + " job(s) pesado(s), " + str(rec["maxWorkersPerJob"]) + " worker(s) por job")
|
|
692
904
|
for reason in rec["reasons"]:
|
|
693
905
|
lines.append(" - " + reason)
|
|
906
|
+
if data.get("lowPriorityPrefix"):
|
|
907
|
+
lines.append("prioridade baixa: " + data["lowPriorityPrefix"] + " <comando>")
|
|
694
908
|
for error in data["config"]["errors"]:
|
|
695
909
|
lines.append("config: " + error)
|
|
696
910
|
return "\n".join(lines)
|
|
697
911
|
|
|
698
912
|
|
|
913
|
+
def exec_prefix_data(platform=None):
|
|
914
|
+
argv, prefix = low_priority(platform)
|
|
915
|
+
return {"platform": platform or sys.platform, "lowPriorityPrefix": prefix, "lowPriorityArgv": argv, "text": prefix}
|
|
916
|
+
|
|
917
|
+
|
|
699
918
|
def main(argv=None):
|
|
700
919
|
import argparse
|
|
701
920
|
p = argparse.ArgumentParser(prog="crow_resources", description="Memória, pressão e paralelismo recomendado.")
|
|
702
921
|
p.add_argument("--home")
|
|
703
922
|
p.add_argument("--json", action="store_true")
|
|
704
923
|
p.add_argument("--no-processes", action="store_true")
|
|
924
|
+
p.add_argument("--exec-prefix", action="store_true")
|
|
705
925
|
args = p.parse_args(argv)
|
|
926
|
+
if args.exec_prefix:
|
|
927
|
+
data = exec_prefix_data()
|
|
928
|
+
print(json.dumps(data, ensure_ascii=False) if args.json else data["text"])
|
|
929
|
+
return 0
|
|
706
930
|
home = Path(args.home).expanduser() if args.home else Path.home()
|
|
707
931
|
data = probe(home, processes=not args.no_processes)
|
|
708
932
|
print(json.dumps(data, ensure_ascii=False) if args.json else render(data))
|