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