daemon:
- storm detection: after 8 captures in 5min, logs STORM STARTED/ENDED to
events.log with duration and capture count; uses longer cooldown (90s)
during storms to reduce log spam while still capturing periodically
- new capture_device_io_digest(): human-readable table of hot devices with
volume label, %util, r_await, w_await, nr_requests — makes USB vs SATA
and dm-crypt queue depth problems immediately visible
- new ## Block device map section (lsblk) in every snapshot
- new classifiers: dm-crypt request pool exhausted (crypt_alloc_buffer +
blk_mq_get_tag pattern), block MQ tag starvation, container/Waydroid
CPU spike (lxc cgroup PSI); USB stall now names the hot volumes
- events.log lines now include the first classifier diagnosis in brackets
- CAPTURE_DEVICE_IO config option (default 1)
gui:
- PSI io full % metric tile and live graph (more severe than io.some)
- storm indicator in status bar showing duration and capture count
- storm events parsed from events.log and shown inline in Snapshots list
(orange, ⚡ prefix), interleaved by timestamp with snapshot entries
- clicking a storm item shows its details in the detail pane
- orange vertical markers on graphs for storm-start events
- Summary tab opens with storm history section (count, avg/max duration,
captures per storm, last 5 storms)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|---|---|---|
| config | ||
| desktop | ||
| packaging | ||
| src | ||
| systemd | ||
| .gitignore | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
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.
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_logwchans with low PSI io - btrfs metadata pressure —
btrfs_search_slotwithout lock contention - USB storage stall —
usb_sg_wait,scsi_eh,sd_check_events - Memory swap thrash —
folio_wait_bit_commonwith PSI mem - Page-fault stall —
folio_wait_bit_commonwithout 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