Skip to content

Desktop profiling

Applies to the desktop client (crates/desktop) at 0.6.

Measure an optimized binary with symbols. Debug builds and Xvfb presentation timings do not represent the owner’s desktop. Keep automated clicks on Xvfb with a private audio sink unless the owner authorizes physical-display interaction. Check that DPMS says Monitor is On before timing it; NVIDIA presentation with the monitors off waited about one second in a later trace.

Terminal window
cargo rustc -j4 --profile android-dev -p nori-desktop -- \
-C debuginfo=1 -C strip=none -C force-frame-pointers=yes
perf record -e cycles:u -F 99 -g --call-graph dwarf,16384 -p "$NORI_PID" \
-o build/desktop-cpu.data -- sleep 60
perf report -i build/desktop-cpu.data --stdio --no-children --sort comm,dso,symbol
perf script -i build/desktop-cpu.data | stackcollapse-perf.pl | flamegraph.pl \
> build/desktop-cpu.svg

The last two commands use Brendan Gregg’s FlameGraph scripts. Dependency frame pointers require a separate RUSTFLAGS build; the command above adds them only to the desktop crate. NVIDIA’s proprietary libraries have incomplete symbols/unwinding, so inspect flat samples alongside the call graph.

For memory, read /proc/$NORI_PID/smaps_rollup and group smaps RSS by mapping. RSS includes resident code, shared libraries, driver mappings and allocator slack. It is not the live Rust heap. Sample a separate test process with jemalloc’s LD_PRELOAD and MALLOC_CONF=prof:true,prof_active:true,lg_prof_sample:17; dump its live heap with mallctl("prof.dump") while it is still running. jeprof --collapsed --inuse_space BINARY DUMP produces stacks for flamegraph.pl. An exit dump misses allocations released during shutdown. Sampling at 128 KiB gives estimates; stack branches overlap and must not be added together.

Use a private SQLite backup and copied cover/music caches when profiling the owner’s library. Keep the copy private: its database contains credentials. Test processes need their own audio sink and normally their own display, and must not receive provider songs that the owner did not request.

For GPU work, RenderDoc’s Vulkan capture and EventGPUDuration counters measure individual draw/copy events. Their sum excludes presentation and some synchronization. Record image acquisition, painting, encoding, submission and presentation CPU times separately when investigating frame delays.

Linux X11, NVIDIA GTX 960, 1280 × 820/900 virtual window. The physical-display run used the owner’s window and library. These are workload observations, not a fixed memory guarantee or frame-rate test.

MeasurementBeforeAfter
GPU process memory, fixture and repeated pause animation258–262 MiB95–97 MiB
Captured fullscreen pause frame, summed GPU draw/copy events0.474 ms0.375 ms
Sampled decoded-cover allocations, copied owner library45.4 MiB21.3 MiB
Sampled total live heap, copied owner library127.6 MiB102.6 MiB
Desktop decoded-cover cache budget90 MiB56 MiB
Normal-process RSS after pause loop, copied owner library320 MiB285 MiB

The GPU memory change uses wgpu’s MemoryUsage allocation hint. Grid and Home shelf virtualization release offscreen image components. Desktop disables the cover loader’s duplicate decoded cache and keeps its own LRU. Hidden sidebar/player layers and a page covered by an opaque fullscreen player do not render. Top Picks combine four covers into one image to avoid independent GPU pixel rounding.

Home creates album cards only for shelves within the vertical viewport and Top Picks cards only within the horizontal viewport. Offscreen shelves keep their layout space without loading covers, so a long Home page cannot evict visible artwork and trigger repeated downloads while stationary. The desktop test home_keeps_pick_covers checks stable cover requests and loading after scrolling. Cached Top Picks mosaics refresh the usage times of their source covers and keep their completed image visible while a replacement is composed. A source-cover update swaps the mosaic only when the new image is ready; the card’s background stays behind its artwork during asynchronous updates.

Playing Next creates artwork bindings only while its docked panel or fullscreen view is visible. Within an open queue, only rows within the viewport and a 100-pixel margin request covers. Hidden queues and offscreen tracks therefore cannot evict visible Home or miniplayer artwork. The home_keeps_pick_covers regression includes 500 queued tracks, opens both queue views, and scrolls to the last track before closing them.

In a 60-second physical-display interaction trace, the updated app averaged 13.2% of one CPU core and 312–359 MiB RSS. Roughly 75% of sampled user CPU cycles were on the UI thread, 14% on analysis and 10% on the playback engine. This includes pauses between interactions and has no matched physical-display baseline. It does not establish animation frame percentiles or solve the remaining RAM cost.

Xvfb pause loops used about 76–79% of one CPU core. Instrumentation measured median painting at 0.76 ms and presentation at 27.08 ms. Refresh pacing did not improve that test and was discarded; the virtual presentation bottleneck must not be attributed to the physical monitor.

The physical trace also exposed a playback stall: a silent pull after a skip advanced the ring past discarded audio without updating the output’s accounting. This created about ten seconds of phantom device latency. A failing clock-based output test reproduces it; thirty rapid skips on the private copy completed without the watchdog restarting playback after the correction.

A further comparison against 2b4f8c95 used the same private library on Xvfb, one test process at a time, and thirty alternating play/pause actions spaced 650 ms apart. These numbers are from separate normal-process and jemalloc-profiled runs; profiling RSS is not used to measure the saving.

MeasurementBeforeAfter
Sampled total live heap103.1 MiB59.7 MiB
Anonymous resident memory after the loop114.3 MiB81.5 MiB
Normal-process RSS after the loop276.7 MiB223.4 MiB
Normal-process PSS after the loop218.0 MiB164.6 MiB
GPU process memory103 MiB123 MiB
Animation-loop CPU, percent of one core89.6%89.1%

Decoded covers upload directly to Slint’s GPU images on the compositor’s device. This removes the app’s retained CPU pixel copies and Skia’s raster copies. Top Picks copy the four equal square card textures into one texture, without rasterizing or reading pixels back to the CPU. Cover regions in the matched Home screenshots were pixel-identical. GPU cache entries keep the existing byte limits; the GPU memory increase reflects covers now retained there instead of in the CPU heap.

The compositor also omits wgpu’s unused indirect-command validation pipelines: every compositor draw uses a fixed vertex range. Ordinary validation stays enabled. The NVIDIA compiler’s large allocation is still present when Skia builds its rendering pipelines; disabling the unused indirect pipelines does not eliminate that driver allocation.

About 28 MiB of the remaining sampled heap belongs to NVIDIA driver allocations, and about 13 MiB to the playback engine. RSS additionally includes code, shared libraries, driver mappings and allocator slack. Library sharing makes RSS comparisons less stable than the anonymous and live-heap measurements. This batch establishes a RAM reduction, not an animation CPU or frame-rate improvement.

Fresh Slint layer textures were drawn through Skia without being initialized in wgpu’s tracker. The first wgpu read therefore cleared the rendered pixels. The compositor forced another draw to recover, leaving a black frame on each resize. It now initializes fresh attachments through wgpu before Skia draws and transitions existing attachments back from sampling to rendering state. The forced second redraw is removed.

A GPU regression failed with transparent black pixels before the change and passes after it. In Xvfb recordings of repeated resizing on Home and fullscreen, the unchanged central region contained 473 mostly black frames out of 542 before, and zero out of 572 after. Different frame counts reflect recording duration; this checks flashing rather than frame pacing.

Six batches of thirty next/previous actions over already-requested tracks completed without playback stalls. Anonymous resident memory was 90.1, 90.3, 85.0, 85.0, 85.1 and 85.2 MiB, respectively. After warm-up it varied by less than 0.3 MiB across the last four batches. This demonstrates a plateau for that workload, not proof that every path is leak-free.

The custom backend now forwards wheel phases as Slint’s native winit backend does. The public PointerScrolled event marks wheels as canceled, which makes Flickable jump immediately instead of using its wheel deceleration. A virtual-clock regression failed before the phase correction; it checks motion between notches and the final accumulated distance. Precise touchpad phases remain intact. Shelves support mouse dragging, Shift+wheel and arrows. Their Show all links open vertically scrollable, virtualized album or Top Picks grids.

Border-drag comparisons used a private xfwm4 session with compositing disabled on a 1920 × 1080 Xvfb screen. Each run dragged the corner and right border inward and outward on Home and fullscreen. The observed client widths ranged from 1000 to 1280 pixels. Recordings contained no black flashes; the final browsing build had zero mostly black frames out of 601. With the same instrumented optimized build, reducing Linux’s maximum queued frames from two to one changed median whole-frame CPU time from 42.1 to 38.2 ms. Painting remained about 3.4 ms; presentation dominated at 29.6 versus 28.2 ms. Vsync remains enabled. This modest virtual-display improvement does not establish smooth physical-display resizing or a CPU/memory reduction.

A physical-display back-navigation failure was followed by a reboot. The previous boot’s kernel log records NVIDIA Xid 13 and Xid 31 (GPU memory read fault), attributed to the same nori-desktop process as Slint’s failed Vulkan texture import. This is a GPU fault, not just a UI exception. Slint’s Vulkan import wraps a borrowed image handle; its Skia image does not retain the wgpu texture that owns it. Linux now supplies raster cover images and mosaics so Skia owns their GPU uploads. The direct-upload memory figures above describe the earlier implementation; Linux restores CPU cover copies in this fix. Other platforms retain their existing upload path. The one-frame latency trial is reverted, and the horizontal shelf scrollbar is removed.

The compositor now keeps the physical window dimensions from resize events. Winit’s X11 inner_size() makes a synchronous XGetGeometry request; drawing and pointer routing no longer repeat that request. Resize events update the latest dimensions, and layer layout and surface reconfiguration run once before drawing rather than on every intermediate resize event. Software Vulkan navigation checks isolate the NVIDIA GPU, but cannot prove hardware-specific fault recovery or 60 fps physical-display resizing.

Ten overview/back cycles passed with software Vulkan. One bounded Xvfb check of both collection views on NVIDIA also passed, with no new kernel Xids during that run. Software-Vulkan corner and side dragging produced zero mostly black frames out of 661, and the GPU pixel regression passed. These are bounded checks, not a guarantee that every NVIDIA rendering path is fault-free.

Owned raster covers and allocator retention

Section titled “Owned raster covers and allocator retention”

Linux keeps each decoded cover in an owned Skia raster image shared with Slint. This removes the second retained Slint pixel buffer while preserving ownership through GPU uploads. Raster Top Picks also share their source images and retain the surface snapshot instead of reading it back into another buffer. Each of the three Skia contexts has a 16 MiB resource-cache budget.

glibc’s dynamic mmap threshold let large freed cover and analysis buffers accumulate in arenas. The Linux GNU desktop sets a 256 KiB mmap threshold and 128 KiB trim threshold at startup, allowing freed large buffers to return to the OS. This does not limit allocator arenas or audio threads.

A physical X11 comparison against 098f878f used optimized binaries, normal glibc allocation, the same copied library and queue, the fullscreen player and sixty consecutive skips. Before each run, the private database was restored and its queue position reset to zero. Both runs used the same already-requested music cache and private audio sink; jemalloc profiling was disabled.

MeasurementPushed buildUpdated build
RSS before skipping251.7 MiB211.4 MiB
RSS after 60 skips388.7 MiB308.8 MiB
Anonymous resident memory after 60 skips256.0 MiB171.1 MiB

The final two updated measurements, after 50 and 60 skips, were 304.2 and 308.8 MiB RSS. A prior pacing trial ended at 289.4 MiB; asynchronous analysis and buffer lifetime affect the snapshot. This is a bounded workload result, not a promise that every path is leak-free. Native libraries, NVIDIA allocations, retained covers and playback buffers still account for substantial memory.

A longer run played through sixty consecutive skips, then three batches of sixty alternating previous/next actions in the same process. RSS after each alternating batch was 268.3 MiB, with 143.4 MiB anonymous memory. The released background-analysis buffers explain the drop from 292.4 MiB at the end of that run’s initial sixty skips. This checks retention over already-visited tracks; it does not establish a bound for every library or navigation path.

On composited X11, drawing uses supported Immediate presentation and coalesces requests at the monitor refresh interval. The window manager handles display synchronization; drawing does not wait on vblank for every ordinary frame. Other display paths retain AutoVsync. There is no frame timer when no redraw is pending. A clock-based test checks coalescing and immediate idle recovery.

An unrecorded corner/right-border drag on Home and fullscreen averaged 21.3% of one CPU core for the pushed build, 17.8% for the pacing trial and 19.9% after correcting resize recovery. These single comparisons are not CPU guarantees. Observed client-size changes still had a median interval around 33.5 ms in both builds; neither one queued frame nor an experimental X11 resize-sync patch improved it. Both experiments were discarded. A final physical-display recording had zero mostly black frames out of 331, and the GPU pixel regression passed, but this does not establish 60 fps live resizing. Issue #33 remains open for the remaining resize and memory work.

A lightweight xterm reference accepted about 60 size updates per second on the physical desktop (median observed interval 16.7 ms), so the 30 Hz Nori result is not a universal xfwm4 resize limit. The reference used eight-pixel pointer steps to exceed terminal cell-size constraints; Nori’s drag used four-pixel steps. Both delivered motion at 60 Hz with geometry sampling on a separate thread. Vulkan swapchain retirement in wgpu 30 calls vkDeviceWaitIdle to wait for outstanding presentation before releasing resources. Painting was usually about 2 ms; acquire/reconfiguration and presentation each reached roughly 12–16 ms in the physical resize trace.

The trace also found 166 abandoned acquisitions versus 187 completed frames. The window could resize during configuration’s wait, making the cached dimensions outdated before acquisition. The old error path configured the same dimensions again and returned without requesting a draw. On Outdated, the backend now re-reads the actual native size after the wait, updates layout and acquires once with those dimensions. Another Outdated or Timeout leaves a paced redraw pending; Timeout does not rebuild the surface. This reduced abandoned draws to 50 and increased completed frames to 277 in the same instrumented drag workload. A second X11 resize-sync trial still had a long first-drag pause and was discarded. This improves recovery during movement without claiming 60 fps resizing.

Linux now renders all three Slint layers through one Skia OpenGL context. The same WGSL glass and lyrics-focus shaders are compiled to desktop GLSL and GLSL ES at build time. The sidebar and player retain their live blur. Other platforms keep the wgpu renderer.

Skia owns uploaded cover textures, and Top Picks mosaics are composed on the GPU before the next frame. A single context shares fonts and its 16 MiB resource budget across layers. There are no borrowed Vulkan cover handles. The native X11 visual is selected from EGL’s compatible opaque configurations. Custom presentation uses the compositor’s retained layer textures rather than querying unused native buffer history. Uniform uploads rename their storage to avoid waiting for a previous pass. Linux coalesces pending draws at the monitor refresh interval, including display paths where swap interval does not provide pacing; idle windows have no frame timer.

Resize changes update page width, panel position and fullscreen artwork immediately. Animations are attached to the panel’s opening width and the artwork’s selected position, so opening or closing still animates without animating every window-size change. The software regression fails on the old layout with 288,456 differing pixels between the immediate and settled snapshots.

A physical X11 comparison against d9b68a2d used the same copied database, already-requested music cache, private audio sink, optimized build profile and sixty skips. Each run restored the private database and queue position before starting. These are ordinary glibc measurements, without heap profiling enabled.

MeasurementPrevious rendererShared OpenGL renderer
RSS before skipping218.5 MiB201.4 MiB
RSS after 60 skips299.6 MiB241.0 MiB
Anonymous resident memory after 60 skips178.9 MiB126.2 MiB

The updated measurements after 50 and 60 skips were 239.7 and 241.0 MiB RSS. This checks this workload, rather than establishing a universal memory bound. Shared libraries and audio analysis still contribute to the process’s memory.

The OpenGL trace’s median swap time was about 0.06 ms. Fast 60 Hz corner/right-border input still produced variable drawing times, including frames over 16.7 ms, and active CPU measurements varied substantially between runs. A renderer-owned X11 child surface did not improve them and was discarded. This change fixes layout animation during resize and reduces cover retention; it does not establish Chrome-like 60 fps resizing. Issue #33 remains open for that remaining work.

GPU pixel regressions passed on physical X11 and on a 1920×1080 Xvfb display using OpenGL ES, covering resized buffers, GPU cover ownership, opaque mosaic seams, and blur smoothness and orientation. The macOS wgpu handoff regression also passed. A private Xvfb control check exercised panels, fullscreen, twenty play/pause actions, thirty next/previous actions and advancing audio. The control check used a 960×600 window to keep software rendering cheap; hardware resize measurements used the physical display.

The desktop seek timer now uses subpixel updates; large seeks/skips and incoming volume changes animate, while small playback increments do not keep a whole layer animating every frame. Hidden fullscreen/compact layouts keep their last displayed position until opened. Android’s shared Rust seek pacer draws quarter-pixel steps and still becomes idle while paused or off screen.

A separate local Navidrome fixture on the physical X11/NVIDIA display, at 1280 × 820, used an optimized android-dev build with symbols, a private audio sink and generated music. After startup settled, five-second samples measured 0.4% of one CPU core paused, 10.2% playing on Home and 12.0% in fullscreen. The current fixture song was 80 seconds long. RSS was 128–144 MiB and anonymous RSS 47–54 MiB. The fixture has no cover images; these are observations, not a matched CPU improvement or a memory guarantee for the owner’s library. Two subsequent skips and play/pause completed.

After beta4, the playlist sidebar creates rows only around its viewport. Previously, layout evaluated cover bindings for offscreen playlists too. A long playlist list could displace visible covers from the bounded cache and request them again on the next cover revision, even with the window still. The regression test uses 500 playlists, checks that offscreen covers are not requested, verifies stable pixels and cached covers across redraws, and scrolls to the last playlist.