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.
|