0.2.0: config file, auto-prune, notify-send, cgroup PSI, kernel stacks, bpftrace, tray tooltip
This commit is contained in:
parent
d41f861802
commit
41dcb0f577
7 changed files with 206 additions and 39 deletions
11
Makefile
11
Makefile
|
|
@ -2,6 +2,7 @@ PREFIX ?= /usr/local
|
||||||
BINDIR := $(PREFIX)/bin
|
BINDIR := $(PREFIX)/bin
|
||||||
APPDIR := $(PREFIX)/share/applications
|
APPDIR := $(PREFIX)/share/applications
|
||||||
SYSTEMDDIR := /etc/systemd/system
|
SYSTEMDDIR := /etc/systemd/system
|
||||||
|
CONFDIR := /etc
|
||||||
LOGDIR := /var/log/freeze-watcher
|
LOGDIR := /var/log/freeze-watcher
|
||||||
LOG_GROUP ?= $(shell id -gn)
|
LOG_GROUP ?= $(shell id -gn)
|
||||||
|
|
||||||
|
|
@ -12,6 +13,13 @@ install:
|
||||||
install -Dm755 src/freeze-monitor-gui $(DESTDIR)$(BINDIR)/freeze-monitor-gui
|
install -Dm755 src/freeze-monitor-gui $(DESTDIR)$(BINDIR)/freeze-monitor-gui
|
||||||
install -Dm644 systemd/freeze-watcher.service $(DESTDIR)$(SYSTEMDDIR)/freeze-watcher.service
|
install -Dm644 systemd/freeze-watcher.service $(DESTDIR)$(SYSTEMDDIR)/freeze-watcher.service
|
||||||
install -Dm644 desktop/freeze-monitor.desktop $(DESTDIR)$(APPDIR)/freeze-monitor.desktop
|
install -Dm644 desktop/freeze-monitor.desktop $(DESTDIR)$(APPDIR)/freeze-monitor.desktop
|
||||||
|
@if [ ! -e "$(DESTDIR)$(CONFDIR)/freeze-watcher.conf" ]; then \
|
||||||
|
install -Dm644 config/freeze-watcher.conf $(DESTDIR)$(CONFDIR)/freeze-watcher.conf; \
|
||||||
|
echo "Installed default config at $(CONFDIR)/freeze-watcher.conf"; \
|
||||||
|
else \
|
||||||
|
install -Dm644 config/freeze-watcher.conf $(DESTDIR)$(CONFDIR)/freeze-watcher.conf.new; \
|
||||||
|
echo "Existing config preserved; new template at $(CONFDIR)/freeze-watcher.conf.new"; \
|
||||||
|
fi
|
||||||
install -dm775 -g $(LOG_GROUP) $(DESTDIR)$(LOGDIR)
|
install -dm775 -g $(LOG_GROUP) $(DESTDIR)$(LOGDIR)
|
||||||
@echo "Installed. Run 'make enable' to start the service."
|
@echo "Installed. Run 'make enable' to start the service."
|
||||||
|
|
||||||
|
|
@ -20,7 +28,8 @@ uninstall:
|
||||||
rm -f $(DESTDIR)$(BINDIR)/freeze-monitor-gui
|
rm -f $(DESTDIR)$(BINDIR)/freeze-monitor-gui
|
||||||
rm -f $(DESTDIR)$(SYSTEMDDIR)/freeze-watcher.service
|
rm -f $(DESTDIR)$(SYSTEMDDIR)/freeze-watcher.service
|
||||||
rm -f $(DESTDIR)$(APPDIR)/freeze-monitor.desktop
|
rm -f $(DESTDIR)$(APPDIR)/freeze-monitor.desktop
|
||||||
@echo "Uninstalled. Logs at $(LOGDIR) preserved."
|
rm -f $(DESTDIR)$(CONFDIR)/freeze-watcher.conf.new
|
||||||
|
@echo "Uninstalled. Config at $(CONFDIR)/freeze-watcher.conf and logs at $(LOGDIR) preserved."
|
||||||
|
|
||||||
enable:
|
enable:
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
|
|
|
||||||
37
README.md
37
README.md
|
|
@ -64,20 +64,33 @@ sudo systemctl restart freeze-watcher.service
|
||||||
|
|
||||||
If you're using the Arch package: `./packaging/build-package.sh && sudo pacman -U *.pkg.tar.zst` will rebuild and upgrade in place.
|
If you're using the Arch package: `./packaging/build-package.sh && sudo pacman -U *.pkg.tar.zst` will rebuild and upgrade in place.
|
||||||
|
|
||||||
## Tuning thresholds
|
## Configuration
|
||||||
|
|
||||||
Open `src/freeze-watcher.sh` and edit the constants at the top:
|
Edit `/etc/freeze-watcher.conf`. After changes:
|
||||||
|
|
||||||
```
|
```
|
||||||
PSI_IO_THRESHOLD=40 # io pressure avg10 % to trigger
|
sudo systemctl restart freeze-watcher.service
|
||||||
PSI_CPU_THRESHOLD=50 # cpu pressure avg10 % to trigger
|
|
||||||
DSTATE_THRESHOLD=5 # processes in D state
|
|
||||||
BLOCKED_THRESHOLD=5 # procs_blocked from /proc/stat
|
|
||||||
LOAD_THRESHOLD=20 # 1-min load average
|
|
||||||
COOLDOWN=30 # seconds between captures
|
|
||||||
SAMPLE_INTERVAL=1 # seconds between samples
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
| Variable | Default | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `PSI_IO_THRESHOLD` | 40 | io pressure avg10 % to trigger |
|
||||||
|
| `PSI_CPU_THRESHOLD` | 50 | cpu pressure avg10 % to trigger |
|
||||||
|
| `DSTATE_THRESHOLD` | 5 | processes in uninterruptible sleep |
|
||||||
|
| `BLOCKED_THRESHOLD` | 5 | procs_blocked from /proc/stat |
|
||||||
|
| `LOAD_THRESHOLD` | 20 | 1-min load average |
|
||||||
|
| `SAMPLE_INTERVAL` | 1 | seconds between samples |
|
||||||
|
| `COOLDOWN` | 30 | min seconds between captures |
|
||||||
|
| `MAX_SNAPSHOTS` | 200 | auto-delete oldest beyond this count (0 disables) |
|
||||||
|
| `MAX_SNAPSHOT_AGE_DAYS` | 30 | auto-delete older than this (0 disables) |
|
||||||
|
| `NOTIFY_USERS` | "" | comma-separated users to notify-send on capture |
|
||||||
|
| `NOTIFY_URGENCY` | normal | low / normal / critical |
|
||||||
|
| `CAPTURE_KERNEL_STACKS` | 1 | dump /proc/[pid]/stack for D-state procs |
|
||||||
|
| `CAPTURE_BPFTRACE` | 0 | run bpftrace one-shot (requires bpftrace) |
|
||||||
|
| `CAPTURE_CGROUP_PSI` | 1 | walk cgroups for top per-cgroup PSI |
|
||||||
|
|
||||||
## GUI
|
## GUI
|
||||||
|
|
||||||
Launch from app menu (search "Freeze Watcher") or `freeze-monitor-gui`.
|
Launch from app menu (search "Freeze Watcher") or `freeze-monitor-gui`.
|
||||||
|
|
@ -105,13 +118,9 @@ Cross-reference `wchan` with PSI io and PSI cpu to distinguish lock contention (
|
||||||
|
|
||||||
## Future ideas
|
## Future ideas
|
||||||
|
|
||||||
- Per-cgroup PSI capture
|
|
||||||
- eBPF tracing for long-running syscalls
|
|
||||||
- Web UI option (no Qt requirement)
|
- Web UI option (no Qt requirement)
|
||||||
- Auto-prune old snapshots
|
|
||||||
- Configurable `/etc/freeze-watcher.conf` instead of inline thresholds
|
|
||||||
- Integration with desktop notifications via `notify-send`
|
|
||||||
- Optional perf-profile capture during event
|
- Optional perf-profile capture during event
|
||||||
|
- HTML/JSON snapshot output mode
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
||||||
31
config/freeze-watcher.conf
Normal file
31
config/freeze-watcher.conf
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# freeze-watcher configuration
|
||||||
|
# Sourced by /usr/local/bin/freeze-watcher.sh at startup.
|
||||||
|
# Lines are bash variable assignments. Restart the service after editing:
|
||||||
|
# sudo systemctl restart freeze-watcher.service
|
||||||
|
|
||||||
|
# --- thresholds ----------------------------------------------------------
|
||||||
|
# A snapshot is captured when ANY of these is exceeded.
|
||||||
|
PSI_IO_THRESHOLD=40 # io pressure avg10 % to trigger
|
||||||
|
PSI_CPU_THRESHOLD=50 # cpu pressure avg10 % to trigger
|
||||||
|
DSTATE_THRESHOLD=5 # processes in uninterruptible sleep
|
||||||
|
BLOCKED_THRESHOLD=5 # procs_blocked from /proc/stat
|
||||||
|
LOAD_THRESHOLD=20 # 1-min load average
|
||||||
|
|
||||||
|
# --- timing --------------------------------------------------------------
|
||||||
|
SAMPLE_INTERVAL=1 # seconds between samples
|
||||||
|
COOLDOWN=30 # min seconds between captures
|
||||||
|
|
||||||
|
# --- retention -----------------------------------------------------------
|
||||||
|
MAX_SNAPSHOTS=200 # auto-delete oldest beyond this count (0 = no cap)
|
||||||
|
MAX_SNAPSHOT_AGE_DAYS=30 # auto-delete older than this (0 = no age cap)
|
||||||
|
|
||||||
|
# --- notifications -------------------------------------------------------
|
||||||
|
# Send a desktop notification when a snapshot is captured.
|
||||||
|
# Comma-separated list of users to notify (must be logged into a graphical session).
|
||||||
|
NOTIFY_USERS="" # e.g. "luna" or "luna,alice"
|
||||||
|
NOTIFY_URGENCY=normal # low, normal, critical
|
||||||
|
|
||||||
|
# --- capture options -----------------------------------------------------
|
||||||
|
CAPTURE_KERNEL_STACKS=1 # dump /proc/[pid]/stack for D-state procs (free, very useful)
|
||||||
|
CAPTURE_BPFTRACE=0 # run bpftrace one-shot during capture (requires bpftrace)
|
||||||
|
CAPTURE_CGROUP_PSI=1 # walk /sys/fs/cgroup for top PSI cgroups
|
||||||
|
|
@ -1,16 +1,22 @@
|
||||||
# Maintainer: luna <anassaeneroi@pm.me>
|
# Maintainer: luna <anassaeneroi@pm.me>
|
||||||
pkgname=freeze-watcher
|
pkgname=freeze-watcher
|
||||||
pkgver=0.1.0
|
pkgver=0.2.0
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Continuously samples system state and snapshots detailed diagnostics when freeze conditions are detected"
|
pkgdesc="Continuously samples system state and snapshots detailed diagnostics when freeze conditions are detected"
|
||||||
arch=('any')
|
arch=('any')
|
||||||
url="https://github.com/luna/freeze-watcher"
|
url="https://github.com/luna/freeze-watcher"
|
||||||
license=('MIT')
|
license=('MIT')
|
||||||
depends=('bash' 'systemd' 'python' 'pyside6' 'sysstat' 'procps-ng' 'util-linux')
|
depends=('bash' 'systemd' 'python' 'pyside6' 'sysstat' 'procps-ng' 'util-linux' 'libnotify' 'sudo')
|
||||||
optdepends=('xdg-utils: open log directory from GUI')
|
optdepends=(
|
||||||
|
'xdg-utils: open log directory from GUI'
|
||||||
|
'bpftrace: kernel stack sampling during capture (CAPTURE_BPFTRACE=1)'
|
||||||
|
)
|
||||||
source=("$pkgname-$pkgver.tar.gz::file://$PWD/../$pkgname-$pkgver.tar.gz")
|
source=("$pkgname-$pkgver.tar.gz::file://$PWD/../$pkgname-$pkgver.tar.gz")
|
||||||
sha256sums=('SKIP')
|
sha256sums=('SKIP')
|
||||||
backup=('etc/systemd/system/freeze-watcher.service')
|
backup=(
|
||||||
|
'etc/systemd/system/freeze-watcher.service'
|
||||||
|
'etc/freeze-watcher.conf'
|
||||||
|
)
|
||||||
|
|
||||||
package() {
|
package() {
|
||||||
cd "$srcdir/$pkgname-$pkgver"
|
cd "$srcdir/$pkgname-$pkgver"
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ trap "rm -rf $tmp" EXIT
|
||||||
|
|
||||||
# Stage source as a tarball (PKGBUILD expects it as $pkgname-$pkgver.tar.gz)
|
# Stage source as a tarball (PKGBUILD expects it as $pkgname-$pkgver.tar.gz)
|
||||||
mkdir "$tmp/$PKGNAME-$PKGVER"
|
mkdir "$tmp/$PKGNAME-$PKGVER"
|
||||||
cp -r src systemd desktop Makefile README.md LICENSE "$tmp/$PKGNAME-$PKGVER/" 2>/dev/null || true
|
cp -r src systemd desktop config Makefile README.md LICENSE "$tmp/$PKGNAME-$PKGVER/" 2>/dev/null || true
|
||||||
tar czf "$tmp/$PKGNAME-$PKGVER.tar.gz" -C "$tmp" "$PKGNAME-$PKGVER"
|
tar czf "$tmp/$PKGNAME-$PKGVER.tar.gz" -C "$tmp" "$PKGNAME-$PKGVER"
|
||||||
|
|
||||||
cp packaging/PKGBUILD "$tmp/PKGBUILD"
|
cp packaging/PKGBUILD "$tmp/PKGBUILD"
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,7 @@ class FreezeMonitor(QMainWindow):
|
||||||
|
|
||||||
# Tray (created before timer so tick() can use it)
|
# Tray (created before timer so tick() can use it)
|
||||||
self.tray = QSystemTrayIcon(self.style().standardIcon(QStyle.SP_ComputerIcon), self)
|
self.tray = QSystemTrayIcon(self.style().standardIcon(QStyle.SP_ComputerIcon), self)
|
||||||
|
self.tray.setToolTip("Freeze Watcher")
|
||||||
menu = QMenu()
|
menu = QMenu()
|
||||||
show_act = QAction("Show window", self)
|
show_act = QAction("Show window", self)
|
||||||
show_act.triggered.connect(self.showNormal)
|
show_act.triggered.connect(self.showNormal)
|
||||||
|
|
@ -198,13 +199,19 @@ class FreezeMonitor(QMainWindow):
|
||||||
self.metrics["Blocked"].set_value(blocked, 3, 6, "")
|
self.metrics["Blocked"].set_value(blocked, 3, 6, "")
|
||||||
self.metrics["Load (1m)"].set_value(int(load), 8, 16, "")
|
self.metrics["Load (1m)"].set_value(int(load), 8, 16, "")
|
||||||
|
|
||||||
if service_active():
|
active = service_active()
|
||||||
|
if active:
|
||||||
self.status_label.setText("● Service: ACTIVE")
|
self.status_label.setText("● Service: ACTIVE")
|
||||||
self.status_label.setStyleSheet("padding:8px; font-weight:bold; color:#5cdb95;")
|
self.status_label.setStyleSheet("padding:8px; font-weight:bold; color:#5cdb95;")
|
||||||
else:
|
else:
|
||||||
self.status_label.setText("● Service: STOPPED")
|
self.status_label.setText("● Service: STOPPED")
|
||||||
self.status_label.setStyleSheet("padding:8px; font-weight:bold; color:#ff5555;")
|
self.status_label.setStyleSheet("padding:8px; font-weight:bold; color:#ff5555;")
|
||||||
|
|
||||||
|
self.tray.setToolTip(
|
||||||
|
f"Freeze Watcher — {'active' if active else 'stopped'}\n"
|
||||||
|
f"PSI io {int(psi_io)}% / cpu {int(psi_cpu)}% — D:{dstate} blk:{blocked} load:{load:.2f}"
|
||||||
|
)
|
||||||
|
|
||||||
# Auto-refresh events list when a new file appears
|
# Auto-refresh events list when a new file appears
|
||||||
current = set(p.name for p in LOG_DIR.glob("snap-*.log")) if LOG_DIR.exists() else set()
|
current = set(p.name for p in LOG_DIR.glob("snap-*.log")) if LOG_DIR.exists() else set()
|
||||||
if current != self._known_events:
|
if current != self._known_events:
|
||||||
|
|
|
||||||
|
|
@ -2,23 +2,116 @@
|
||||||
# Continuously samples system state. Writes a detailed snapshot when
|
# Continuously samples system state. Writes a detailed snapshot when
|
||||||
# anomaly thresholds are crossed (D-states, PSI pressure, blocked procs).
|
# anomaly thresholds are crossed (D-states, PSI pressure, blocked procs).
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
LOG_DIR=/var/log/freeze-watcher
|
LOG_DIR=/var/log/freeze-watcher
|
||||||
|
CONFIG=/etc/freeze-watcher.conf
|
||||||
|
|
||||||
|
# --- defaults (overridden by /etc/freeze-watcher.conf) -------------------
|
||||||
|
PSI_IO_THRESHOLD=40
|
||||||
|
PSI_CPU_THRESHOLD=50
|
||||||
|
DSTATE_THRESHOLD=5
|
||||||
|
BLOCKED_THRESHOLD=5
|
||||||
|
LOAD_THRESHOLD=20
|
||||||
|
COOLDOWN=30
|
||||||
|
SAMPLE_INTERVAL=1
|
||||||
|
MAX_SNAPSHOTS=200
|
||||||
|
MAX_SNAPSHOT_AGE_DAYS=30
|
||||||
|
NOTIFY_USERS=""
|
||||||
|
NOTIFY_URGENCY=normal
|
||||||
|
CAPTURE_KERNEL_STACKS=1
|
||||||
|
CAPTURE_BPFTRACE=0
|
||||||
|
CAPTURE_CGROUP_PSI=1
|
||||||
|
|
||||||
|
[ -f "$CONFIG" ] && . "$CONFIG"
|
||||||
|
|
||||||
mkdir -p "$LOG_DIR"
|
mkdir -p "$LOG_DIR"
|
||||||
|
|
||||||
# Thresholds
|
|
||||||
PSI_IO_THRESHOLD=40 # io pressure avg10 % to trigger
|
|
||||||
PSI_CPU_THRESHOLD=50 # cpu pressure avg10 % to trigger
|
|
||||||
DSTATE_THRESHOLD=5 # processes in D state
|
|
||||||
BLOCKED_THRESHOLD=5 # procs_blocked from /proc/stat
|
|
||||||
LOAD_THRESHOLD=20 # 1-min load average
|
|
||||||
COOLDOWN=30 # seconds between captures
|
|
||||||
SAMPLE_INTERVAL=1 # seconds between samples
|
|
||||||
|
|
||||||
last_capture=0
|
last_capture=0
|
||||||
|
captures_since_prune=0
|
||||||
|
|
||||||
|
# Send a desktop notification to each configured user.
|
||||||
|
# Daemon runs as root; we sudo into the user's session for DBus access.
|
||||||
|
notify_users() {
|
||||||
|
[ -z "$NOTIFY_USERS" ] && return 0
|
||||||
|
local body="$1"
|
||||||
|
local IFS=,
|
||||||
|
for user in $NOTIFY_USERS; do
|
||||||
|
user=$(echo "$user" | tr -d '[:space:]')
|
||||||
|
[ -z "$user" ] && continue
|
||||||
|
local uid
|
||||||
|
uid=$(id -u "$user" 2>/dev/null) || continue
|
||||||
|
sudo -u "$user" \
|
||||||
|
DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$uid/bus" \
|
||||||
|
notify-send -u "$NOTIFY_URGENCY" -a "freeze-watcher" \
|
||||||
|
"Freeze captured" "$body" 2>/dev/null &
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Delete old snapshots based on MAX_SNAPSHOTS and MAX_SNAPSHOT_AGE_DAYS.
|
||||||
|
prune_snapshots() {
|
||||||
|
if [ "${MAX_SNAPSHOT_AGE_DAYS:-0}" -gt 0 ]; then
|
||||||
|
find "$LOG_DIR" -maxdepth 1 -name 'snap-*.log' -type f \
|
||||||
|
-mtime "+$MAX_SNAPSHOT_AGE_DAYS" -delete 2>/dev/null
|
||||||
|
fi
|
||||||
|
if [ "${MAX_SNAPSHOTS:-0}" -gt 0 ]; then
|
||||||
|
local files
|
||||||
|
files=$(ls -1t "$LOG_DIR"/snap-*.log 2>/dev/null | tail -n "+$((MAX_SNAPSHOTS+1))")
|
||||||
|
[ -n "$files" ] && echo "$files" | xargs -r rm -f
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
capture_kernel_stacks() {
|
||||||
|
echo "## Kernel stack traces of D-state procs"
|
||||||
|
ps -eo pid,state --no-headers | awk '$2=="D"{print $1}' | while read -r pid; do
|
||||||
|
local comm
|
||||||
|
comm=$(cat "/proc/$pid/comm" 2>/dev/null) || continue
|
||||||
|
echo "--- pid=$pid comm=$comm ---"
|
||||||
|
cat "/proc/$pid/stack" 2>/dev/null | head -20
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
capture_cgroup_psi() {
|
||||||
|
echo "## Top cgroups by PSI io.some avg10"
|
||||||
|
{
|
||||||
|
for f in /sys/fs/cgroup/io.pressure \
|
||||||
|
/sys/fs/cgroup/*/io.pressure \
|
||||||
|
/sys/fs/cgroup/*/*/io.pressure; do
|
||||||
|
[ -r "$f" ] || continue
|
||||||
|
local v
|
||||||
|
v=$(awk '/^some/{for(i=1;i<=NF;i++) if($i~/^avg10=/){gsub(/avg10=/,"",$i); print $i; exit}}' "$f" 2>/dev/null)
|
||||||
|
[ -z "$v" ] && continue
|
||||||
|
[ "$v" = "0.00" ] && continue
|
||||||
|
printf "%s\t%s\n" "$v" "$(dirname "$f" | sed 's|/sys/fs/cgroup||')"
|
||||||
|
done
|
||||||
|
} 2>/dev/null | sort -rn | head -10
|
||||||
|
echo
|
||||||
|
echo "## Top cgroups by PSI cpu.some avg10"
|
||||||
|
{
|
||||||
|
for f in /sys/fs/cgroup/cpu.pressure \
|
||||||
|
/sys/fs/cgroup/*/cpu.pressure \
|
||||||
|
/sys/fs/cgroup/*/*/cpu.pressure; do
|
||||||
|
[ -r "$f" ] || continue
|
||||||
|
local v
|
||||||
|
v=$(awk '/^some/{for(i=1;i<=NF;i++) if($i~/^avg10=/){gsub(/avg10=/,"",$i); print $i; exit}}' "$f" 2>/dev/null)
|
||||||
|
[ -z "$v" ] && continue
|
||||||
|
[ "$v" = "0.00" ] && continue
|
||||||
|
printf "%s\t%s\n" "$v" "$(dirname "$f" | sed 's|/sys/fs/cgroup||')"
|
||||||
|
done
|
||||||
|
} 2>/dev/null | sort -rn | head -10
|
||||||
|
}
|
||||||
|
|
||||||
|
capture_bpftrace() {
|
||||||
|
command -v bpftrace >/dev/null 2>&1 || { echo "bpftrace not installed"; return; }
|
||||||
|
echo "## bpftrace: top kernel stacks for io_schedule waits over 2s"
|
||||||
|
timeout 3 bpftrace -e '
|
||||||
|
kprobe:io_schedule { @[kstack(5)] = count(); }
|
||||||
|
interval:s:2 { exit(); }
|
||||||
|
' 2>/dev/null | tail -40
|
||||||
|
}
|
||||||
|
|
||||||
capture_snapshot() {
|
capture_snapshot() {
|
||||||
local reason="$1"
|
local reason="$1"
|
||||||
local now=$(date +%s)
|
|
||||||
local file="$LOG_DIR/snap-$(date +%Y%m%d-%H%M%S).log"
|
local file="$LOG_DIR/snap-$(date +%Y%m%d-%H%M%S).log"
|
||||||
{
|
{
|
||||||
echo "=== FREEZE EVENT TRIGGERED: $reason ==="
|
echo "=== FREEZE EVENT TRIGGERED: $reason ==="
|
||||||
|
|
@ -35,9 +128,17 @@ capture_snapshot() {
|
||||||
echo "[$k]"; cat /proc/pressure/$k 2>/dev/null
|
echo "[$k]"; cat /proc/pressure/$k 2>/dev/null
|
||||||
done
|
done
|
||||||
echo
|
echo
|
||||||
echo "## D-state processes (with kernel wchan = where they're stuck)"
|
if [ "${CAPTURE_CGROUP_PSI:-1}" = "1" ]; then
|
||||||
|
capture_cgroup_psi
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
echo "## D-state processes (with kernel wchan)"
|
||||||
ps -eo state,pid,user,wchan:25,comm,args --no-headers | awk '$1=="D"' | head -30
|
ps -eo state,pid,user,wchan:25,comm,args --no-headers | awk '$1=="D"' | head -30
|
||||||
echo
|
echo
|
||||||
|
if [ "${CAPTURE_KERNEL_STACKS:-1}" = "1" ]; then
|
||||||
|
capture_kernel_stacks
|
||||||
|
echo
|
||||||
|
fi
|
||||||
echo "## Top 15 by CPU"
|
echo "## Top 15 by CPU"
|
||||||
ps -eo pid,user,pcpu,pmem,state,comm --no-headers --sort=-pcpu | head -15
|
ps -eo pid,user,pcpu,pmem,state,comm --no-headers --sort=-pcpu | head -15
|
||||||
echo
|
echo
|
||||||
|
|
@ -56,6 +157,10 @@ capture_snapshot() {
|
||||||
echo "## softirq"
|
echo "## softirq"
|
||||||
cat /proc/softirqs
|
cat /proc/softirqs
|
||||||
echo
|
echo
|
||||||
|
if [ "${CAPTURE_BPFTRACE:-0}" = "1" ]; then
|
||||||
|
capture_bpftrace
|
||||||
|
echo
|
||||||
|
fi
|
||||||
echo "## Recent dmesg (last 50 lines)"
|
echo "## Recent dmesg (last 50 lines)"
|
||||||
dmesg -T | tail -50
|
dmesg -T | tail -50
|
||||||
echo
|
echo
|
||||||
|
|
@ -63,24 +168,19 @@ capture_snapshot() {
|
||||||
journalctl --since "60 seconds ago" -p warning --no-pager 2>&1 | tail -40
|
journalctl --since "60 seconds ago" -p warning --no-pager 2>&1 | tail -40
|
||||||
} > "$file" 2>&1
|
} > "$file" 2>&1
|
||||||
echo "$(date -Iseconds) Captured snapshot to $file (reason: $reason)" >> "$LOG_DIR/events.log"
|
echo "$(date -Iseconds) Captured snapshot to $file (reason: $reason)" >> "$LOG_DIR/events.log"
|
||||||
|
notify_users "$reason"
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "$(date -Iseconds) freeze-watcher started (pid $$)" >> "$LOG_DIR/events.log"
|
echo "$(date -Iseconds) freeze-watcher started (pid $$)" >> "$LOG_DIR/events.log"
|
||||||
|
prune_snapshots
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
now=$(date +%s)
|
now=$(date +%s)
|
||||||
|
|
||||||
# PSI pressure - parse "some avg10=X.XX"
|
|
||||||
psi_io=$(awk '/^some/{for(i=1;i<=NF;i++) if($i~/^avg10=/){gsub(/avg10=/,"",$i); print int($i+0.5); exit}}' /proc/pressure/io 2>/dev/null)
|
psi_io=$(awk '/^some/{for(i=1;i<=NF;i++) if($i~/^avg10=/){gsub(/avg10=/,"",$i); print int($i+0.5); exit}}' /proc/pressure/io 2>/dev/null)
|
||||||
psi_cpu=$(awk '/^some/{for(i=1;i<=NF;i++) if($i~/^avg10=/){gsub(/avg10=/,"",$i); print int($i+0.5); exit}}' /proc/pressure/cpu 2>/dev/null)
|
psi_cpu=$(awk '/^some/{for(i=1;i<=NF;i++) if($i~/^avg10=/){gsub(/avg10=/,"",$i); print int($i+0.5); exit}}' /proc/pressure/cpu 2>/dev/null)
|
||||||
|
|
||||||
# D-state count
|
|
||||||
dstate=$(ps -eo state --no-headers | grep -c "^D")
|
dstate=$(ps -eo state --no-headers | grep -c "^D")
|
||||||
|
|
||||||
# /proc/stat blocked
|
|
||||||
blocked=$(awk '/^procs_blocked/{print $2}' /proc/stat)
|
blocked=$(awk '/^procs_blocked/{print $2}' /proc/stat)
|
||||||
|
|
||||||
# Load average
|
|
||||||
load1=$(awk '{print int($1+0.5)}' /proc/loadavg)
|
load1=$(awk '{print int($1+0.5)}' /proc/loadavg)
|
||||||
|
|
||||||
triggered=""
|
triggered=""
|
||||||
|
|
@ -93,6 +193,11 @@ while true; do
|
||||||
if [ -n "$triggered" ] && [ $((now - last_capture)) -ge "$COOLDOWN" ]; then
|
if [ -n "$triggered" ] && [ $((now - last_capture)) -ge "$COOLDOWN" ]; then
|
||||||
capture_snapshot "$triggered" &
|
capture_snapshot "$triggered" &
|
||||||
last_capture=$now
|
last_capture=$now
|
||||||
|
captures_since_prune=$((captures_since_prune+1))
|
||||||
|
if [ "$captures_since_prune" -ge 20 ]; then
|
||||||
|
prune_snapshots &
|
||||||
|
captures_since_prune=0
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sleep "$SAMPLE_INTERVAL"
|
sleep "$SAMPLE_INTERVAL"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue