3.9 KiB
freeze-watcher
A systemd daemon that continuously samples Linux system state and dumps a detailed forensic snapshot whenever freeze-like conditions are detected (D-state spikes, PSI pressure, blocked processes, load avg). Includes a Qt GUI for live metrics and snapshot browsing.
Built to diagnose intermittent system freezes that don't show up in normal resource monitors — specifically the kind where some apps stall but the mouse keeps moving.
What it captures
Each snapshot is a single text file in /var/log/freeze-watcher/snap-YYYYMMDD-HHMMSS.log containing:
loadavg,procs_running,procs_blocked,intr,ctxt- PSI cpu / io / memory pressure (
someandfullaverages) - D-state processes with kernel
wchan(where exactly they're stuck) - Top 15 by CPU and RSS
meminfosummaryiostat -xtsnapshot- Top interrupt sources by total + per-CPU breakdown
- Full
softirqtable - Last 50 dmesg lines
- Last 60s of journal warnings/errors
The wchan column is the key one — that's how you tell btrfs_lock_root_node lock contention from usb_sg_wait USB stalls from folio_wait_bit_common page-fault stalls.
Components
| Path | What |
|---|---|
src/freeze-watcher.sh |
The daemon — samples every 1s, captures on threshold |
src/freeze-monitor-gui |
PySide6 GUI: live metrics + snapshot browser + tray icon |
systemd/freeze-watcher.service |
systemd unit |
desktop/freeze-monitor.desktop |
App launcher entry |
Install
Via Makefile (system-wide install to /usr/local)
sudo make install
sudo make enable
As an Arch package
./packaging/build-package.sh
sudo pacman -U freeze-watcher-*.pkg.tar.zst
sudo systemctl enable --now freeze-watcher.service
Update workflow
When you change something:
# 1. Edit files in src/ or systemd/
# 2. Reinstall
sudo make install
# 3. If the daemon script changed, restart it
sudo systemctl restart freeze-watcher.service
# 4. If the GUI changed, just relaunch it (it's a regular Qt app)
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
Open src/freeze-watcher.sh and edit the constants at the top:
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
GUI
Launch from app menu (search "Freeze Watcher") or freeze-monitor-gui.
- Live PSI / D-state / load metrics with green/orange/red color coding (1s refresh)
- Service status with start/stop buttons (uses
pkexec) - Captured event list — click any row for full snapshot view
- Closes to system tray; quit from the tray menu
- Notification when a new snapshot is captured
Reading a snapshot
The most informative section is D-state processes — the wchan tells you what kernel function each process is blocked in. Common ones:
| wchan | Meaning |
|---|---|
btrfs_lock_root_node |
btrfs metadata lock contention (often qgroups + snapshots) |
btrfs_search_slot |
btrfs tree walk waiting for lock |
btrfs_sync_log |
fsync waiting for transaction commit |
folio_wait_bit_common |
Waiting for memory page (often swap-in) |
usb_sg_wait |
USB device stall |
io_schedule |
Generic I/O wait |
Cross-reference wchan with PSI io and PSI cpu to distinguish lock contention (low PSI) from genuine resource pressure (high PSI).
Future ideas
- Per-cgroup PSI capture
- eBPF tracing for long-running syscalls
- Web UI option (no Qt requirement)
- Auto-prune old snapshots
- Configurable
/etc/freeze-watcher.confinstead of inline thresholds - Integration with desktop notifications via
notify-send - Optional perf-profile capture during event
License
MIT