Skip to content

feat: auto-hibernate inactive instances to reclaim memory#10

Open
Teamingzooper wants to merge 1 commit into
feat/module-devtoolsfrom
feat/instance-hibernate
Open

feat: auto-hibernate inactive instances to reclaim memory#10
Teamingzooper wants to merge 1 commit into
feat/module-devtoolsfrom
feat/instance-hibernate

Conversation

@Teamingzooper

Copy link
Copy Markdown
Owner

Summary

Big win for users with 8+ instances open: an opt-in setting that destroys the embedded WebContentsView for any non-active instance that's been idle longer than a configured threshold (default 60 min when enabled, range 5–480 min). The instance keeps its sidebar entry, gets a 💤 indicator, and re-activating it rebuilds the view from scratch.

Branch base: `feat/module-devtools` — fifth and final in the stack. Merge order: #6#7#8#9 → this.

Why

Each embedded WebContentsView is a full Chromium tab — easily 100–300 MB of RAM each. A user with 8 instances (work WhatsApp, personal WhatsApp, work Slack, personal Slack, Telegram, Messenger, Teams, Instagram) is sitting at 1–2 GB just for Nexus's webviews, even when they're only actively using one. Hibernation can reclaim most of that.

What you give up by enabling it

When an instance hibernates, its WebContentsView is destroyed. The instance loses:

  • Open conversation scroll position
  • Composing-message draft text (if the provider didn't save it to localStorage — most do, but not guaranteed)
  • Any uncommitted in-page state

Hence opt-in by default. The toggle is in Settings → General → Performance with a clear description of the tradeoff.

Design choices

  • Active instance is never hibernated — only background instances qualify.
  • Sweep runs every 60s with a wall-clock comparison. Cheap and good-enough.
  • Range 5–480 min clamped on both ends. 5 min minimum prevents thrash; 8h ceiling is just a sanity bound.
  • `lastActiveAt` seeded on `ensure()` — a freshly-created instance has a "young" age and won't be hibernated in the same tick.
  • No `view:created` swallow when waking — the existing event still fires alongside a new `instance:woken`, so downstream consumers that already watch view-creation keep working.

Architecture

Same main → bus → IPC → renderer pattern as the rest of this stack:

  • New bus events: `instance:hibernated`, `instance:woken`.
  • New IPC channels: `nexus:instance:hibernated`, `nexus:instance:woken`, `nexus:prefs:set-hibernate-minutes`.
  • New renderer store slice: `hibernatedInstances: Record<id, true>`.
  • Sidebar pulls from the slice and renders the 💤 conditionally next to the instance name.
  • Settings → General → Performance: a toggle + numeric input (only shown when toggle is on).

Test plan

  • `npm run typecheck` — clean
  • `npm run test` — 178/178 passing
  • `npm run build` — renderer + main both clean
  • Manual smoke: enable hibernate at 5 min in Settings → Performance. Open WhatsApp, switch to Telegram, leave for 6 min. Watch the WhatsApp sidebar row: a 💤 should appear. Click WhatsApp — the page reloads ("slow first activation"), 💤 disappears.
  • Manual smoke: with hibernate enabled, make sure the currently-active instance does NOT get hibernated even if it's been on screen for an hour. (Activate, leave, return — no 💤 on the active one.)
  • Manual smoke: disable hibernate via the toggle. Verify previously-hibernated instances don't re-trigger and that no new hibernations happen on the timer.

🤖 Generated with Claude Code

Adds an opt-in setting that destroys the embedded WebContentsView for
any non-active instance idle longer than a configured threshold (5–480
minutes). The instance keeps its sidebar entry, gets a 💤 indicator,
and re-activating rebuilds the view from scratch (slow first
activation accepted as the cost of the freed memory).

Big win for users with 8+ instances — each Chromium tab eats real
RAM, and a hibernated instance frees most of that.

Implementation:

- New `hibernateAfterMinutes?: number` on appStateSchema (range 5–480,
  undefined/0 = disabled).
- SettingsService.setHibernateAfterMinutes() with normalisation.
- ViewService tracks lastActiveAt: Map<id, ms> seeded on ensure(),
  updated on every activate(). A new 60-second interval timer runs
  runHibernationSweep(): iterates views, tears down any with
  (id !== activeId && lastActiveAt < now - minutes*60_000). Emits
  bus event 'instance:hibernated'.
- ensure() checks a hibernatedIds set; if the id was previously
  hibernated, emits 'instance:woken' on the recreated view.
- destroy() (manual remove) clears both lastActiveAt and hibernatedIds.
- IpcService forwards both new bus events as nexus:instance:hibernated
  and nexus:instance:woken to the main renderer window.
- preload exposes onInstanceHibernated, onInstanceWoken, and
  setHibernateAfterMinutes.
- Renderer store tracks hibernatedInstances: Record<id, true>.
- Sidebar renders a 💤 next to hibernated instance names.
- Settings → General gains a Performance section with a toggle (default
  off; defaults to 60 min when first enabled) and a numeric input.

The default is OFF — opt-in to avoid surprising users who don't realise
their open conversation could lose scroll/draft state when its view is
destroyed. The active instance is never hibernated.

178/178 unit tests pass, typecheck + build clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants