nbshell keeps the bar, wallpaper, notification popups, OSD, runtime services,
and IPC handlers resident. Larger surfaces that are not useful in the
background—such as display settings, the process list, dashboard, notes, and
the wallpaper picker and unified Library—are created only while open and
destroyed after closing. Launcher window, clipboard, and calculator providers
reuse state already held by the shell. File search starts a capped fd process
only for an explicit @ query. System reports, demo capture, and video exports
are CLI jobs and leave no resident helper behind.
Shell motion is event-driven. It animates compositor-friendly properties such
as opacity, scale, position, and list offsets only while a visible state is
changing. There is no animation daemon, continuous canvas, or wallpaper
thumbnail conversion job. Settings → Appearance → Motion offers reduced,
standard, and expressive; reduced motion resolves all shared durations to
zero.
The systemd service also configures jemalloc for Quickshell:
MALLOC_CONF=thp:never,narenas:4,dirty_decay_ms:3000
Quickshell on Arch links against jemalloc. Limiting arenas and preventing
small allocations from using transparent huge pages substantially reduces
idle private memory. A user can override MALLOC_CONF in a later systemd user
drop-in if a different allocator policy is required.
The installed service sets NBSHELL_DISABLE_HOT_RELOAD=1, so a production
session does not keep file watches for the deployed QML tree. A manual
nbshell start leaves hot reload enabled for development.
Resident services prefer compositor, D-Bus, and file notifications over child process polling. Optional phone and tailnet services are instantiated by their configured widget or an explicit IPC call rather than every session. Display hot-plugs refresh immediately from Quickshell's screen signal; a five-minute fallback covers output-property changes that do not change the screen list. Media positions and Cava samples run only while a loaded UI consumes them.
Use proportional set size rather than RSS when comparing shell revisions. Qt and graphics libraries map many shared pages that RSS counts in every process:
pid=$(systemctl --user show nbshell.service -p MainPID --value)
grep -E '^(Rss|Pss|Pss_Anon|AnonHugePages):' /proc/$pid/smaps_rollupMeasure after the shell has been idle for several seconds. Open and close each surface once, wait again, and repeat the measurement to distinguish the clean startup baseline from caches retained after normal use. Performance numbers are diagnostics rather than release-test thresholds because GPU drivers, screen count, plugins, and enabled modules materially change them.
The isolated Wayland fixture can compare synchronous panel creation with an
asynchronous experiment. It requires Umbriel, Quickshell, Bubblewrap,
dbus-run-session, grim, Python Pillow, and an accessible DRM render node.
Run the variants sequentially from the same source tree, using fresh output
paths:
python3 tests/wayland-lifecycle.py --compositor /usr/local/bin/umbriel \
--render-node /dev/dri/renderD128 --output /tmp/nbshell-panel-sync \
--cycles 20 --settle-seconds 0 --panel-profile
python3 tests/wayland-lifecycle.py --compositor /usr/local/bin/umbriel \
--render-node /dev/dri/renderD128 --output /tmp/nbshell-panel-async \
--cycles 20 --settle-seconds 0 --panel-profile --panel-asyncThe experiment changes only the private shell copy. Both outputs contain a
panel-profile.json, screenshots, and logs. Compare baselineQmlSha256 before
comparing results. The fixture opens Settings, Modules, Agents, Dashboard, and
the Plugin Manager, destroys each panel after closing, and requires exactly
one first-swap event per new panel instance.
The three measurements answer different questions:
qtFirstSwapMs: in-process open request to that panel's first QtframeSwappedcallback. This may still be an initial transparent frame; it does not measure physical display scanout.visibleReadbackUpperBoundMs: request through screenshot capture, PNG decoding, and central-region pixel comparison. It confirms visible output but includes substantial IPC/readback overhead. The fixed pixel threshold is intended for the default 800×600 fixture, not cross-resolution comparisons.maxHeartbeatGapMs: largest Qt timer gap during the request/readback window. It includes render scheduling and unrelated shell activity, so it does not attribute blocked CPU time to a particular component.
On 2026-09-08, 20 openings per panel and variant on the same source produced these p95 values in the private software-rendered 800×600 fixture (milliseconds):
| Panel | First Qt swap, sync / async | Heartbeat gap, sync / async |
|---|---|---|
| Settings | 10 / 11 | 20 / 18 |
| Modules | 15 / 16 | 21 / 23 |
| Agents | 15 / 15 | 17 / 18 |
| Dashboard | 34 / 30 | 29 / 25 |
| Plugin Manager | 13 / 12 | 17 / 17 |
Keep synchronous loading: the experiment did not establish a consistent improvement. Screenshot upper bounds were lower for several async panels, but include measurement overhead and sequential-run host-load differences. These small samples are diagnostic evidence, not a general performance promise or a release threshold. Raw cycles include the first opening, allowing it to be inspected separately from subsequent openings.
nbshell can optionally enable systemd-oomd for graphical applications:
nbshell memory-guard setup
nbshell memory-guard status
nbshell memory-guard removeThe shell and its Umbriel resume guard run in session.slice; applications
remain in app.slice. When enabled, systemd-oomd may reclaim an application
cgroup after app.slice stays above 60% memory pressure for 20 seconds, with
swap exhaustion as a second backstop. This protects the interactive desktop
without adding a resident nbshell process. The setting is opt-in because an
OOM decision can close an application that has not saved its work.
remove deletes only nbshell's app-slice policy. It deliberately leaves the
shared systemd-oomd daemon enabled in case another desktop component uses it.