160 lines
5.0 KiB
Bash
Executable File
160 lines
5.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# 卡若AI · 轻量防卡顿守护(金仓)
|
|
# 仅阈值触发;不删 TM 快照、不碰 state.vscdb、不删用户文件。
|
|
# 用法:
|
|
# bash karuo_perf_guard.sh --once # 执行一轮
|
|
# bash karuo_perf_guard.sh --dry-run # 只报告不动作
|
|
# bash karuo_perf_guard.sh --status # 只看指标
|
|
set -euo pipefail
|
|
|
|
MODE="${1:---once}"
|
|
DRY=0
|
|
LOG="${KARUO_PERF_GUARD_LOG:-$HOME/Library/Logs/karuo_perf_guard.log}"
|
|
MAX_LOG_LINES=2000
|
|
|
|
# 阈值(可用环境变量覆盖)
|
|
ORPHAN_NODE_MIN="${KARUO_ORPHAN_NODE_MIN:-15}"
|
|
REPORTCRASH_PCPU_MIN="${KARUO_REPORTCRASH_PCPU_MIN:-25}"
|
|
REPORTCRASH_ETIME_MIN="${KARUO_REPORTCRASH_ETIME_MIN:-3600}"
|
|
MEM_FREE_MB_MIN="${KARUO_MEM_FREE_MB_MIN:-800}"
|
|
LOAD_MIN_FOR_PURGE="${KARUO_LOAD_MIN_FOR_PURGE:-15}"
|
|
GIT_STUCK_ETIME_MIN="${KARUO_GIT_STUCK_ETIME_MIN:-600}"
|
|
GIT_STUCK_PCPU_MIN="${KARUO_GIT_STUCK_PCPU_MIN:-40}"
|
|
DISK_WARN_PCT="${KARUO_DISK_WARN_PCT:-95}"
|
|
|
|
mkdir -p "$(dirname "$LOG")"
|
|
|
|
rotate_log() {
|
|
[[ -f "$LOG" ]] || return 0
|
|
local n
|
|
n=$(wc -l <"$LOG" | tr -d ' ')
|
|
if [[ "$n" -gt "$MAX_LOG_LINES" ]]; then
|
|
tail -n 500 "$LOG" >"${LOG}.tmp" && mv "${LOG}.tmp" "$LOG"
|
|
fi
|
|
}
|
|
|
|
log() {
|
|
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"
|
|
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >>"$LOG"
|
|
}
|
|
|
|
mem_free_mb() {
|
|
local free_pages page_size
|
|
free_pages=$(vm_stat 2>/dev/null | awk '/Pages free/ {gsub(/\./, "", $3); print $3}' || echo 0)
|
|
page_size=$(sysctl -n hw.pagesize 2>/dev/null || echo 16384)
|
|
echo $((free_pages * page_size / 1024 / 1024))
|
|
}
|
|
|
|
load_avg() {
|
|
sysctl -n vm.loadavg 2>/dev/null | awk '{print int($2)}'
|
|
}
|
|
|
|
disk_used_pct() {
|
|
df /System/Volumes/Data 2>/dev/null | awk 'NR==2 {gsub(/%/, "", $5); print $5}'
|
|
}
|
|
|
|
status_report() {
|
|
local free_mb load disk node_n
|
|
free_mb=$(mem_free_mb)
|
|
load=$(load_avg)
|
|
disk=$(disk_used_pct)
|
|
node_n=$(pgrep -c node 2>/dev/null || echo 0)
|
|
log "status load=$load free_mb≈${free_mb} disk=${disk}% node=${node_n}"
|
|
if [[ "$disk" -gt "$DISK_WARN_PCT" ]]; then
|
|
log "WARN 磁盘>${DISK_WARN_PCT}% — 建议清 Downloads/缓存(守护不会自动删用户文件)"
|
|
fi
|
|
}
|
|
|
|
orphan_codegraph_cleanup() {
|
|
local count
|
|
count=$(pgrep -fc '@colbymchenry/codegraph-darwin-arm64/node -e' 2>/dev/null || echo 0)
|
|
if [[ "$count" -gt "$ORPHAN_NODE_MIN" ]]; then
|
|
log "CodeGraph orphan watchdog=${count} (>${ORPHAN_NODE_MIN}) → cleanup"
|
|
if [[ "$DRY" -eq 0 ]]; then
|
|
pkill -f '@colbymchenry/codegraph-darwin-arm64/node -e' 2>/dev/null || true
|
|
fi
|
|
fi
|
|
}
|
|
|
|
reportcrash_cleanup() {
|
|
local pid pcpu etime comm
|
|
pid=$(pgrep -f '/ReportCrash agent' 2>/dev/null | head -1 || true)
|
|
[[ -z "$pid" ]] && return 0
|
|
pcpu=$(ps -p "$pid" -o %cpu= 2>/dev/null | tr -d ' ' || echo 0)
|
|
etime=$(ps -p "$pid" -o etimes= 2>/dev/null | tr -d ' ' || echo 0)
|
|
comm=$(ps -p "$pid" -o command= 2>/dev/null || true)
|
|
[[ "$comm" != *"ReportCrash agent"* ]] && return 0
|
|
if awk "BEGIN{exit !($pcpu > $REPORTCRASH_PCPU_MIN)}" && [[ "${etime:-0}" -gt "$REPORTCRASH_ETIME_MIN" ]]; then
|
|
log "ReportCrash 卡死 pid=$pid pcpu=${pcpu}% etime=${etime}s → kill"
|
|
if [[ "$DRY" -eq 0 ]]; then
|
|
kill -9 "$pid" 2>/dev/null || true
|
|
fi
|
|
fi
|
|
}
|
|
|
|
feishu_api_dedup() {
|
|
local -a pids=()
|
|
local keep pid
|
|
while IFS= read -r pid; do
|
|
[[ -n "$pid" ]] && pids+=("$pid")
|
|
done < <(pgrep -f 'feishu_api\.py' 2>/dev/null | sort -n || true)
|
|
((${#pids[@]} <= 1)) && return 0
|
|
keep="${pids[0]}"
|
|
for pid in "${pids[@]:1}"; do
|
|
log "重复 feishu_api pid=$pid(保留 $keep)→ kill"
|
|
if [[ "$DRY" -eq 0 ]]; then
|
|
kill "$pid" 2>/dev/null || true
|
|
fi
|
|
done
|
|
}
|
|
|
|
stuck_git_scm_cleanup() {
|
|
local pid pcpu etime cmd
|
|
while read -r pid pcpu cmd; do
|
|
[[ -z "$pid" ]] && continue
|
|
[[ "$cmd" != *git* ]] && continue
|
|
[[ "$cmd" != *"ls-files"* && "$cmd" != *"diff"* && "$cmd" != *"status"* ]] && continue
|
|
etime=$(ps -p "$pid" -o etimes= 2>/dev/null | tr -d ' ' || echo 0)
|
|
if awk "BEGIN{exit !($pcpu > $GIT_STUCK_PCPU_MIN)}" && [[ "${etime:-0}" -gt "$GIT_STUCK_ETIME_MIN" ]]; then
|
|
log "git SCM 长时间高CPU pid=$pid pcpu=${pcpu}% etime=${etime}s → kill"
|
|
if [[ "$DRY" -eq 0 ]]; then
|
|
kill "$pid" 2>/dev/null || true
|
|
fi
|
|
fi
|
|
done < <(ps aux 2>/dev/null | awk '!/awk/ && /\/git / {print $2, $3, substr($0, index($0,$11))}' || true)
|
|
}
|
|
|
|
memory_purge_if_needed() {
|
|
local free_mb load
|
|
free_mb=$(mem_free_mb)
|
|
load=$(load_avg)
|
|
if [[ "$free_mb" -lt "$MEM_FREE_MB_MIN" ]] && [[ "$load" -gt "$LOAD_MIN_FOR_PURGE" ]]; then
|
|
log "内存偏低 free≈${free_mb}MB load=$load → purge"
|
|
if [[ "$DRY" -eq 0 ]]; then
|
|
purge 2>/dev/null || true
|
|
fi
|
|
fi
|
|
}
|
|
|
|
run_once() {
|
|
rotate_log
|
|
status_report
|
|
[[ "$MODE" == "--status" ]] && return 0
|
|
orphan_codegraph_cleanup
|
|
reportcrash_cleanup
|
|
feishu_api_dedup
|
|
stuck_git_scm_cleanup
|
|
memory_purge_if_needed
|
|
}
|
|
|
|
case "$MODE" in
|
|
--once | --dry-run | --status)
|
|
[[ "$MODE" == "--dry-run" ]] && DRY=1
|
|
run_once
|
|
;;
|
|
*)
|
|
echo "用法: $0 [--once|--dry-run|--status]"
|
|
exit 1
|
|
;;
|
|
esac
|