Watch
1
0
Fork
You've already forked souveraine
0
souveraine/.handoff-from-phone/quickshell-idle-power/TASK.md

135 lines
7 KiB
Markdown
Raw Normal View History

---
# quickshell-idle-power — lazy-load panels + consolidate polling processes
**Status:** 2026-07-21 — scoped, not started. Two quick wins already landed
separately (see "Already done" below); this doc covers the two larger sweeps
(#3 LazyLoader coverage, #4 process consolidation) to be worked tomorrow.
**Config:** `qs -c souveraine` → symlinks into `~/souveraine-surfaces/quickshell/`
(the `~/.config/quickshell/souveraine/` tree is symlinks; edit the real tree).
Quickshell 0.3.0. Composed of three layers:
- `modules/ services/ panelFamilies/`**souveraine** (owned, edit freely)
- `ii-base/`**borrowed ii/Caelestia tree** (treat
as replaceable; keep coupling to souveraine singletons OUT of here)
- `ii-phone/`, `ii-previous/` → phone snapshot / previous deploy
## Why (measured)
At a 49-min uptime, `qs` had burned **149 CPU-sec** and held **560 MB RSS
(15% of RAM)** on the Pixel 3 — second only to the active claude session. The
display idle layer (`services/IdleCoordinator.qml`) is already excellent
(state machine Active→Dimmed→Lock→Suspend→Asleep→Waking, native IdleMonitor
with respectInhibitors, brightness dim, hypridle screen-off). The gap is that
**the idle machine dims the screen but does not quiet the widgets** — timers
tick and processes poll regardless of visibility. These two sweeps attack the
steady-state CPU (over-eager construction) and RSS (per-widget processes).
Quickshell FAQ guidance backing this:
- "The main thing you can do to reduce memory usage is to use Loaders."
- Loader for Item-derived components; LazyLoader otherwise.
- "Using a process per widget will use significantly more memory than using
a single process."
Ref: https://quickshell.org/docs/v0.3.0/guide/faq/
## Already done (2026-07-21, do NOT redo)
1. `ii-base/services/DateTime.qml` — uptime Timer was `interval: 10` (reloading
/proc/uptime off disk 100×/sec). Changed to `interval: 60000` +
`triggeredOnStart: true`. The clock itself is `SystemClock` (event-driven),
was never the problem.
2. `ii-base/modules/ii/background/widgets/visualizer/VisualizerWidget.qml`
the wave visualizer's 33ms (30fps) repaint Timer was `running: root.style
=== "wave"`; now `running: parent.visible` so it stops repainting when the
canvas isn't shown. (Considered a `Window.visibility` gate too, but that
needs `import QtQuick.Window` in the borrowed tree — deferred; `parent.visible`
is dependency-free and sufficient.)
Both verified: qs hot-reloaded without QML errors, pid stayed alive.
---
## Task 3 — Lazy-load the heavy panels (RSS lever)
**Current:** 9 LazyLoader vs 123 Loader across 683 QML files → a lot is
eagerly constructed at startup. Top-level panel modules under
`ii-base/modules/ii/` that are candidates (not visible at boot):
cheatsheet overlay overview sidebarLeft sidebarRight wallpaperSelector
sessionScreen screenTranslator regionSelector mediaControls polkit
notificationPopup onScreenKeyboard dock
**Do:**
- For each panel not shown at boot, wrap its instantiation in a `LazyLoader`
(non-Item / window-like → LazyLoader; Item-derived → Loader). Load on the
signal/state that reveals it (e.g. sidebar open toggle, GlobalStates flag),
unload after.
- Especially: the anime/booru sidebar (`ii-base/modules/ii/sidebarLeft/anime/`)
and cheatsheet — heavy, rarely open.
- Verify each panel still opens with no first-open UI-thread stall (LazyLoader
loads sync if shown before background load finishes — see LazyLoader docs).
**Watch:** keep LazyLoader wrapping in the layer that OWNS the panel. Don't add
souveraine-singleton dependencies into ii-base while doing this.
**Measure before/after:** `ps -eo pid,pmem,rss,comm | grep qs` (RSS) at a fresh
launch, before opening any panel.
## Progress 2026-07-22 (task 4 partially landed)
Landed live + promoted to the laptop repo (souveraine tree):
- GlobalStates.qml (souveraine override): new bool — the
shared cross-layer idle surface the doc prescribed.
- services/IdleCoordinator.qml: publishes displayActive on every state
transition (Active/Waking = true).
- ii-base/services/ResourceUsage.qml + NetworkTraffic.qml: repeat pollers
gated (+ ).
- VisualizerWidget.qml: cava watchdog gated on , cava torn down when either goes false.
- SystemInfo.qml inspected: its timers are one-shot startup probes, left
alone. Notifications.qml: no repeat poller found, left alone.
One hot-reload during editing tripped the known
"lockscreen surfaces without active lock" FATAL (reload race with sessiond,
not a QML error); the service restart loaded clean. CPU-delta measurement
while locked still pending (baseline at /tmp/qs-idle-baseline-*).
Task 3 note from code reading: most panels ALREADY gate content internally on
GlobalStates.*Open via Loaders; their IpcHandlers live inside the
panel files, so adding at the
PanelLoader level would unload the very handler that opens them — do NOT do
that. The real task-3 work is converting the -gated panels
(Overview line 23, SidebarLeft lines 89/181) to internal Loaders,
one panel at a time. Overview deliberately deferred until the 2026-07-21
search-fix has device acceptance.
## Task 4 — Consolidate per-widget processes (CPU + RSS lever)
**Current:** ~48 `running: true` Process instances (souveraine 14, ii-base 32,
ii-phone 2). FAQ: one process feeding many widgets ≫ cheaper than N processes.
**Do:**
- Audit `ii-base/services/*.qml` for multiple widgets independently spawning
processes for the SAME data. Known pollers to review:
ResourceUsage.qml, NetworkTraffic.qml, SystemInfo.qml, Notifications.qml,
plus /proc readers. Route each data source through ONE Singleton service
that widgets bind to (the `services/` layer already does this well — the
duplication lives in the borrowed ii-base tree).
- Gate long-poll services on idle state where a stale value is fine while
dimmed/locked: e.g. `running: IdleCoordinator.state === IdleCoordinator.Active`
for background stats/resources/network widgets. NOTE: IdleCoordinator is a
souveraine singleton — if gating an ii-base widget on it, do the gating in a
souveraine wrapper/override, not by importing it into ii-base. Alternatively
expose an idle bool via the existing `GlobalStates`/config surface that both
layers already share.
- The cava process behind the visualizer (VisualizerWidget.qml watchdog, 2s)
is a good candidate to also stop when the background isn't visible.
**Measure before/after:** cumulative CPU: `ps -eo pid,times,comm | grep qs`
after a fixed idle interval (e.g. leave locked 10 min, compare CPU-sec delta).
## Acceptance
- Fresh-launch RSS meaningfully below ~560 MB.
- `qs` CPU-sec growth while the screen is off/locked approaches zero (today
it keeps climbing from always-on timers/pollers).
- No panel regressions: every lazy-loaded panel opens without a stall; every
consolidated service still updates its widgets live when Active.