178 lines
6.9 KiB
Markdown
178 lines
6.9 KiB
Markdown
# 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 (`some` and `full` averages)
|
|
- D-state processes with kernel `wchan` (where exactly they're stuck)
|
|
- Top 15 by CPU and RSS
|
|
- `meminfo` summary
|
|
- `iostat -xt` snapshot
|
|
- Top interrupt sources by total + per-CPU breakdown
|
|
- Full `softirq` table
|
|
- 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.
|
|
|
|
## Configuration
|
|
|
|
Edit `/etc/freeze-watcher.conf`. After changes:
|
|
|
|
```
|
|
sudo systemctl restart freeze-watcher.service
|
|
```
|
|
|
|
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
|
|
|
|
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).
|
|
|
|
## Auto-classification
|
|
|
|
Each captured snapshot is automatically pattern-matched against known freeze signatures and gets a `## Diagnosis` section at the top:
|
|
|
|
- **btrfs lock contention** — `btrfs_lock_root_node` / `btrfs_sync_log` wchans with low PSI io
|
|
- **btrfs metadata pressure** — `btrfs_search_slot` without lock contention
|
|
- **USB storage stall** — `usb_sg_wait`, `scsi_eh`, `sd_check_events`
|
|
- **Memory swap thrash** — `folio_wait_bit_common` with PSI mem
|
|
- **Page-fault stall** — `folio_wait_bit_common` without mem pressure (overly aggressive swappiness)
|
|
- **CPU oversubscription** — high PSI cpu, few D-state procs
|
|
- **NIC IRQ saturation** — recent NET_RX softirq concentrated on one CPU (computed against a rolling 5-minute baseline)
|
|
- **Genuine I/O bottleneck** — high PSI io.full
|
|
|
|
If no signature matches, the snapshot says "uncertain — review manually".
|
|
|
|
## Cross-snapshot summary
|
|
|
|
The GUI's **Summary** tab aggregates patterns across all captured snapshots:
|
|
- Top diagnoses (which freeze types occur most)
|
|
- Top kernel wchans (where processes get stuck)
|
|
- Top processes seen in D-state
|
|
- Top CPU consumers at capture time
|
|
- Captures by hour-of-day distribution
|
|
|
|
Useful for spotting "qBittorrent appears in 80% of freezes" type patterns.
|
|
|
|
## Live graphs
|
|
|
|
The GUI's **Live graphs** tab shows the last hour of load, PSI io/cpu, and D-state count as scrolling line graphs. Red dashed lines mark snapshot capture events so you can correlate spikes with the captured forensics.
|
|
|
|
## Manual capture
|
|
|
|
Click "📸 Capture Now" in the GUI status bar (or in the tray menu) to trigger a snapshot immediately, regardless of whether thresholds are crossed. Useful when you suspect something is brewing but it hasn't tripped the alarms.
|
|
|
|
## JSON / HTML output
|
|
|
|
Set `OUTPUT_JSON=1` and/or `OUTPUT_HTML=1` in `/etc/freeze-watcher.conf` to write structured sidecars next to each text snapshot:
|
|
|
|
- **JSON** (`snap-*.json`) — fully structured: PSI, dstate procs, kernel stacks, memory, top procs, cgroup PSI, softirqs, etc. Easy to consume programmatically.
|
|
- **HTML** (`snap-*.html`) — self-contained styled report with color-coded metrics, collapsible sections, and clickable kernel stacks per D-state proc. Useful for sharing or pasting into a bug report.
|
|
|
|
You can also convert any existing snapshot manually:
|
|
|
|
```
|
|
freeze-snapshot-convert /var/log/freeze-watcher/snap-20260510-214207.log --json --html
|
|
```
|
|
|
|
The GUI's "Open in browser" button on the Snapshots tab opens the HTML sidecar of the selected snapshot (generating it on demand if missing).
|
|
|
|
## Future ideas
|
|
|
|
- Web UI option (no Qt requirement)
|
|
- Optional perf-profile capture during event
|
|
- HTML/JSON snapshot output mode
|
|
- Snapshot annotations (user notes per capture)
|
|
- Anonymized export for sharing
|
|
|
|
## License
|
|
|
|
MIT
|