docs GitHub

Architecture

Performance

Vyre runs all day on your own machines, so idle must cost almost nothing. The budgets are principle 8 of the Specification ("Light by default"). A change that breaks one is a bug, like a failing test. This page lists the budgets, how to check them, and what was measured.

The budgets

WhatBudget
vyred idleunder 0.9% of one core (p95) and under 1.8% sustained; under 60 MB of heap after a full GC; no polling faster than once a minute when nothing is happening. Resident size is reported, and gates (150 MB settled, 200 MB at startup) only on the Node the box ships
Lumen hiddenunder 0.2% CPU, no GPU use, under 250 MB resident for all its processes
Lumen shown and idleunder 2% CPU; wakes in under 100 ms
Vyre app web build in a background tabno timers faster than a minute; the event stream only
Heavy work (indexing, embedding, curation)low priority, yields, pauses on battery and when you are active, never blocks a hook or Lumen
Memory that grows with the corpusbounded and measured

Check vyred with scripts/perf-check

npm run perf-check          # or: node scripts/perf-check

It takes about 65 seconds and exits 0 when every budget holds, 1 when one breaks. CI runs it after npm test (.github/workflows/test.yml), and scripts/release-check.sh runs it unless you pass --skip-perf.

What it does:

  1. Makes a throwaway VYRE_HOME (never your real ~/.vyre) and seeds it with the fixture corpus plus a synthetic corpus of about 20,000 turns, so Recall has real work.
  2. Starts a real vyred as a child process at lower priority (os.setPriority, best effort), with a timer hook (scripts/lib/timer-hook.mjs) that records every setInterval and setTimeout the process registers, including inside modules.
  3. Waits for the startup indexing pass to finish, then samples CPU and resident memory every 1.5 seconds for 60 seconds of true idle: no requests.
  4. Deletes the temp home and stops vyred, pass or fail.

What it gates on:

CheckBudget
CPU, p95 of the samplesunder 0.9%
CPU, sustained: the highest mean over any 5 consecutive samples (7.5 s)under 1.8%
Heap used after a full GC at the end of the idle window (scripts/lib/gc-hook.mjs, on every Node)under 60 MB
RSS, settled: the highest of the last 8 samples, once they sit within 3 MB (the window runs on to 120 s until they do), on the Node the box shipsunder 150 MB
RSS, startup peak: the highest in the first 30 s from spawn, indexing included, on the Node the box shipsunder 200 MB
Fastest recurring timer (a setInterval, or a setTimeout seen 3 or more times)60 s or slower

Mean and max CPU, mean and max RSS, and RSS after the GC are printed but do not gate. After the startup indexing pass V8 keeps its heap (about 160 MB on the synthetic corpus) for some 20 s before it gives it back and settles near 90 MB, so the idle budget reads the settled size and the startup peak has its own budget. That holds on Node 22, the Node in box/Dockerfile. Node 24 does not give the heap back while idle: the same commit settles at 186 to 260 MB with 74 to 111 MB of heap in use that a GC would free, and only about 20 MB live. So the RSS budgets gate on the Node major the box image ships and print as informational on any other, and the heap after a forced GC gates everywhere, so a real leak still fails on every Node. The output also prints the host's load average, so you can tell contention from a regression.

Why Why p95 and a sustained window, not the maximum?

One noisy ps sample on a busy machine should not fail the run. An earlier version gated on the maximum and failed on a single sample out of 40 while the mean was under 0.25%. The sustained check still catches a loop that costs 1 to 2% the whole minute, which p95 alone could miss.

The check turns off recall.vectors in its temp config. The embedding model downloads weights on first use and then works in the background for minutes on a large backlog, which does not fit a one-minute idle check and needs the network. Embedding cost is not covered by perf-check.

Measured

All numbers are from the first audit, 26 Sep 2026, on a shared macOS development machine.

vyred idle (perf-check)

On a host with load average 23.6 to 26.2 (other test suites running):

MetricResultBudget then
CPU p950.00%under 0.5%
CPU sustained0.13%under 1%
RSS mean55.6 MBunder 150 MB
RSS max100.8 MBunder 150 MB
Fastest recurring timernone under 60 s60 s or slower

An earlier run gated on raw max CPU and failed on a single sample out of 40 (0.7 to 2.7%, and 8.58% at load average 22 to 26) while the mean was 0.08 to 0.23%. That was host contention.

What the audit changed

WhereFoundNow
core/watchers/index.jsthe scheduler ticked every 15 sTICK_MS is 60 s
core/recall/dense.jsthe vector index had no cap: 1,553 bytes a chunk, about 57 to 59 MB at 37,000 to 40,000 chunksmaxChunks, default 50,000 (about 78 MB), evicting the oldest sessions first
core/recall/index.jsa transcript scan every 5 minuteskept: a stat-only pass over 600 sessions costs 8 to 70 ms
core/memory/index.jssuspected re-derive on every eventalready debounced (SETTLE_MS 250 ms) and coalesced; about 2 s per 100,000 turns
core/computers/index.jsa sweep every 5 s whether or not a computer was in usesweeps every sweepMs (5 s) only while something is checked out or waiting to freeze, else every idleSweepMs (60 s)

The switchboard, gate, vault, learn and system modules and the Harness are driven by events and hooks. The event stream's 15-second heartbeat runs only while a client is connected. Since the audit, Recall also indexes one session about 1.5 s after a turn ends (turn.completed or thread.started), so Chat can follow a terminal session. That work is driven by events and does nothing while no session is running. The numbers above predate it.

Lumen

These numbers are for the Electron Lumen, since retired. The native Lumen (local/capsule/native) is measured in CI by scripts/capsule-native-check.mjs, against budgets of under 60 MB resident and under 0.1% CPU while hidden, and a wake under 50 ms.

MeasureResultBudget
Hidden RSS, all processes212 to 233 MB (238 MB with the hotkey helper)under 250 MB
Hidden CPU, main process, vyred up0.061%under 0.2%
Hidden CPU, main process, vyred down0.210% overall, settling near 0.18% after about 90 sunder 0.2%
Warm wake, show() to painted framemean 38.0 ms (34.5 to 39.6 ms, 5 samples)under 100 ms
Warm open, end to end18 to 50 msunder 100 ms

Before the fixes, hidden RSS was 301 to 324 MB (the network service and GPU now run in the main process, with no spare renderer), and hidden CPU with vyred down was 0.791% (a health check every 3 s and a stream reconnect every 1.5 s, now backed off to 60 s and 30 s while hidden). The clipboard watcher (a Swift helper, polling the pasteboard every 750 ms) measured 0.016%.

Wake timing leaves out the hotkey's own double-tap window (about 450 ms, by design) and the first show after launch (about 721 ms).

The app and Glass

  • apps/app/screens/glass/state.ts lets go of the screen when the tab is hidden, keeping the last frame, and asks for a fresh ticket when the tab is visible again.
  • core/glass/ has no timers. core/computers/glass.js sends a 30-second keepalive per open viewer, which exists only while someone is watching.
  • apps/app/screens/onboarding/SetupScreen.tsx polls every 3 s while you wait on a step. That is a foreground wizard you are looking at, so it is outside the background-tab budget.

Not measured yet

  • Embedding and curation CPU, and pausing on battery.
  • A quiet, dedicated CI runner baseline for perf-check.
  • Lumen's hidden CPU budget covers all its processes together; the numbers above are per process.

Where to go next

  • Specification: principle 8 and the rest of the rules.
  • Testing: the test suite perf-check runs beside.