- Dock drag-to-reorder for pinned apps (insertion gap, quick-slide vs dwell) - Fullscreen detection: scan all windows via HyprlandData.windowList - IdleCoordinator, GlobalStates, Session.qml updates - Deploy script, qmldir, settings, wallpaper, visualizer fixes - sessiond server, memory module updates Co-Authored-By: Claude <noreply@anthropic.com>
135 lines
7 KiB
Markdown
135 lines
7 KiB
Markdown
---
|
||
# 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.
|