native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
Shell
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231#!/bin/bash# Measure Prowl's steady-state CPU and attribute it to the agent-detection and# SwiftUI-flush paths, alongside the baseline recorded in# docs-ai/032-performance-hardening/004-agent-detection-steady-state.md.## A CPU percentage means nothing without its workload, so every run records the# agent mix and the host load average next to the per-symbol attribution. Treat# a sample taken above roughly one runnable process per core as unusable:# starvation inflates every number, and it inflates them unevenly.set -euo pipefailumask 077
# Set PROWL_PID when several Debug app instances are running or an exact process# has already been selected for an isolated measurement.usage_error() { echo "$1" >&2 exit 64}
require_positive_integer() { local name=$1 local value=$2 case "$value" in '' | *[!0-9]*) usage_error "$name must be a positive integer." ;; esac [ "$value" -gt 0 ] || usage_error "$name must be greater than zero."}
resolve_prowl_pid() { if [ -n "${PROWL_PID:-}" ]; then require_positive_integer PROWL_PID "$PROWL_PID" kill -0 "$PROWL_PID" 2>/dev/null || { echo "PROWL_PID $PROWL_PID is not running." >&2 return 1 } printf '%s\n' "$PROWL_PID" return fi
local matches local count matches=$(ps -Ao pid=,comm= | awk '/\/Prowl Debug\.app\/Contents\/MacOS\/ProwlApp$/ { print $1 }') count=$(printf '%s\n' "$matches" | awk 'NF { count += 1 } END { print count + 0 }') case "$count" in 0) echo "Prowl Debug is not running." >&2 return 1 ;; 1) printf '%s\n' "$matches" ;; *) printf 'Several Prowl Debug processes are running (%s). Set PROWL_PID explicitly.\n' "$matches" >&2 return 1 ;; esac}
# Prints the socket path the prowl CLI connects to, as ProwlSocket.defaultPath does.prowl_cli_socket() { if [ -n "${PROWL_CLI_SOCKET:-}" ]; then printf '%s\n' "$PROWL_CLI_SOCKET" return fi local preferred="$HOME/Library/Application Support/com.onevcat.prowl/cli.sock" # sockaddr_un.sun_path is 104 bytes on Darwin, including the NUL terminator. if [ "$(printf '%s' "$preferred" | wc -c)" -lt 104 ]; then printf '%s\n' "$preferred" else local tmp=${TMPDIR:-$(getconf DARWIN_USER_TEMP_DIR)} printf '%s\n' "${tmp%/}/prowl-cli.sock" fi}
# Prints why the CLI does not answer for process $1, or nothing when it does. Debug# and Release apps share the default socket path and only one app serves it, so a# CLI answer can describe another app than the measured one.cli_socket_mismatch() { local pid=$1 local socket socket=$(prowl_cli_socket) if ! lsof -a -U -p "$pid" -Fn 2>/dev/null | grep -Fxq "n$socket"; then printf 'CLI socket %s is not served by pid %s\n' "$socket" "$pid" fi}
# Runs a prowl query into a file, unless the CLI would answer for another process.query_cli() { local out=$1 shift if [ -n "$SOCKET_MISMATCH" ]; then jq -cn --arg reason "$SOCKET_MISMATCH" '{ok: false, reason: $reason}' > "$out" else prowl "$@" 2>/dev/null > "$out" || printf '{"ok":false}\n' > "$out" fi}
PID=$(resolve_prowl_pid)SOCKET_MISMATCH=$(cli_socket_mismatch "$PID")
# Each run gets its own directory so earlier samples stay comparable.# ~/Library/Logs is where macOS keeps user-visible diagnostics, so runs survive a# reboot and Console.app can see them. $TMPDIR is wrong for this: it is cleared# periodically and on reboot, which silently discards the baseline a later run is# meant to be compared against. ~/Library/Caches would be worse still, since the# system may evict it under disk pressure.RUN_ID="$(date +%Y%m%d-%H%M%S)-$$"OUT="${PROWL_MEASURE_DIR:-$HOME/Library/Logs/Prowl/measurements}/$RUN_ID"mkdir -p "$OUT"
# Keep the console output and the on-disk record identical.exec > >(tee "$OUT/summary.txt") 2>&1
echo "pid: $PID (up $(ps -o etime= -p "$PID" | tr -d ' '))"echo "load: $(uptime | sed 's/.*load averages*: //') cores: $(sysctl -n hw.ncpu)"echo
echo "=== agent mix ==="# Working agents drive the expensive detection path, so the mix is needed to# compare two runs honestly.query_cli "$OUT/agents.json" agents --jsonjq -r 'if .ok then "total=\(.data.agents|length) " + (.data.agents|group_by(.status)|map("\(.[0].status)=\(length)")|join(" ")) else "CLI unavailable" + (if .reason then " (\(.reason))" else "" end) end' \ < "$OUT/agents.json" 2>/dev/null || echo "CLI unavailable"echo
echo "=== pane visibility ==="query_cli "$OUT/panes.json" list --jsonjq -r ' if .ok then .data.items as $items | "total=\($items | length)" + " visible=\(if $items | all(.pane | has("visible")) then $items | map(select(.pane.visible)) | length else "unknown" end)" + " focused=\($items | map(select(.pane.focused)) | length)" + " tabs=\($items | map(.tab.id) | unique | length)" + " selected_tabs=\($items | map(select(.tab.selected) | .tab.id) | unique | length)" + " worktrees=\($items | map(.worktree.id) | unique | length)" else "CLI unavailable" + (if .reason then " (\(.reason))" else "" end) end' < "$OUT/panes.json" 2>/dev/null || echo "CLI unavailable"echo
echo "=== process CPU (20 s) ==="top -l 21 -s 1 -pid "$PID" -stats pid,cpu 2>/dev/null \ | grep -E "^ *$PID" | awk '{print $2}' | tail -20 > "$OUT/cpu.txt"python3 - "$OUT/cpu.txt" <<'PY'import statistics as s, sysv = [float(x) for x in open(sys.argv[1]).read().split()]if v: print(f"mean={s.mean(v):.1f}% median={s.median(v):.1f}% min={min(v):.1f}% max={max(v):.1f}%")PYecho
echo "=== stack sample (20 s) ==="sample "$PID" 20 -f "$OUT/sample.txt" >/dev/null 2>&1python3 - "$OUT/sample.txt" <<'PY'import re, sys
lines = open(sys.argv[1]).read().splitlines()hdr = []for i, l in enumerate(lines): m = re.match(r'^\s+(\d+) (Thread_\w+.*)$', l) if m: hdr.append((i, int(m.group(1)), m.group(2).strip()))if not hdr: print("no thread blocks parsed") raise SystemExit
# (symbol, pre-fix % of one core) — baseline was 20 agents: 1 working, 2 blocked.syms = [ ('detectAgentState', 15.88), ('AgentSessionResolver.resolve(', 13.02), ('resolveUncached', 12.98), ('bestMatch', 11.38), ('normalize(', 6.70), ('transcriptStrings', 2.47), ('recentCandidates', 1.57), ('stepTransactionFlush', None), ('GraphHost.flushTransactions()', None), ('flushTransactions', None), ('addGlyph', None), ('rebuildRow', None), ('wyhash', None), ('RepositorySectionView', None), ('SidebarActiveAgentsOverlay', None),]tot = {s: 0 for s, _ in syms}# Each row is an independent substring total, so a general symbol would also count# the frames a more specific row already reports. Exclude those, and say so.excluded = {'flushTransactions': ('GraphHost.flushTransactions()',)}labels = {'flushTransactions': 'flushTransactions (excluding GraphHost)'}
def matches(s, sym): return s in sym and not any(x in sym for x in excluded.get(s, ()))
for idx, (i, _t, _desc) in enumerate(hdr): end = hdr[idx + 1][0] if idx + 1 < len(hdr) else len(lines) ent = [] for l in lines[i + 1:end]: m = re.search(r'(\d+) (\S.*)$', l) if m: ent.append((m.start(1), int(m.group(1)), m.group(2))) # Sum only the shallowest occurrences so nested frames are not double counted. for s, _ in syms: counted, mind = [], None for d, c, sym in ent: if matches(s, sym): if mind is None or d <= mind: counted.append(c) mind = d tot[s] += sum(counted)
W = hdr[0][1]print(f"window={W} samples/thread; 1 core = {W} samples\n")print(f"{'%core':>7} {'was':>7} symbol")for s, base in syms: pct = 100 * tot[s] / W was = f"{base:.2f}%" if base is not None else " - " print(f"{pct:6.2f}% {was:>7} {labels.get(s, s)}")
det = 100 * tot['detectAgentState'] / Wif det > 0: print("\ncomposition of detectAgentState (load-independent):") for s, base in [('AgentSessionResolver.resolve(', 82), ('bestMatch', 72), ('normalize(', 42)]: share = 100 * tot[s] / max(tot['detectAgentState'], 1) print(f" {s:32s} {share:5.1f}% (was {base}%)")PY
echoecho "run dir: $OUT"echo " sample.txt / cpu.txt / summary.txt / agents.json / panes.json"