Skip to content

Performance overhaul: instant startup, viewport rendering, concurrent event pump; login fixes#371

Merged
LargeModGames merged 11 commits into
mainfrom
perf/optimizations
Jul 13, 2026
Merged

Performance overhaul: instant startup, viewport rendering, concurrent event pump; login fixes#371
LargeModGames merged 11 commits into
mainfrom
perf/optimizations

Conversation

@LargeModGames

Copy link
Copy Markdown
Owner

A performance pass over startup, rendering, and the network/event architecture, plus the login fixes found along the way.

Performance measurements

Measured with headless probes on Windows 11, debug profile (absolute values are inflated vs release, but before/after comparisons on the same profile are valid; the pacing probe is sleep-dominated and profile-independent).

Scenario Before After Change
Search fan-out pacing delay (5 concurrent API calls) 1.04 s 0.02-0.12 ms ~1 s of artificial delay removed from every search, ~250 ms from every artist page
Frame draw, 10k-track table 60.1 ms 2.5 ms ~24x; frame cost now scales with the viewport, not library size
Frame draw, 10k-track table scrolled to row 9500 n/a 2.6 ms deep scroll costs the same as top-of-list
Home screen frame (60 fps animation tick) 6.0 ms 2.8 ms ~2x
Home screen frame, scrolled deep into the changelog 16.0 ms 2.7 ms ~6x; scroll depth no longer affects frame cost
Session-persistence check per tick (5k-track queue) 2.16 ms 9 ns full snapshot now built only when the 3 s save throttle is due
Config saves while holding volume/resize keys 2.08 ms disk write per key repeat 0 per repeat debounced to one save 500 ms after the last change
Redraws per keypress 2 1 tick events sent only when due
Sustained API rate (regression check) 1 per 250 ms 1 per 250 ms unchanged; only bursts of up to 5 start immediately

opt-level = 3 was also measured against the current "s": 19-37% faster frames but +33% binary size on costs already down to 0.06-0.4 ms release, so "s" stays.

What changed

Startup

  • The TUI shows immediately: the update check runs concurrently with auth, and the account probe + librespot session handshake (up to 45 s worst case) moved to a background task behind a live UI.
  • One /me round trip instead of three; playlists publish page 1 right away and fetch the rest (plus rootlist folders) in the background; the Lua VM is only constructed when user scripts exist.

Rendering

  • All table draws format only the visible rows instead of the whole backing collection every frame; the track table gains anchored scrolling.
  • Home caches the changelog (pre-wrapped, sliced by scroll) and the banner gradient instead of rebuilding both per frame. This also un-froze the banner animation, which had been gated on a focus condition that almost never held.

Network / event loop

  • Non-Spotify work (lyrics, cover art, Subsonic/radio/local, telemetry, friends) runs on a concurrent service lane so slow Spotify calls no longer head-of-line-block it; the pump's poll loop is replaced with a blocking bridge thread.
  • API pacing reserves slots without holding the lock across the sleep and allows bursts of 5 (sized to the search fan-out); artist/album metadata gets a small TTL cache; Subsonic/radio/announcement HTTP clients are reused instead of rebuilt per action.
  • Playlists open on Enter immediately with a loading state, and repeated presses dedupe instead of stacking dispatches.

Playback reliability

  • Play requests issued while native streaming is recovering (or still initializing at startup) are parked and replayed once the device is back, instead of being swallowed or routed to the full-screen Error; a load watchdog with an attempt cap tears down zombie sessions.
  • Playlist refreshes preserve folder selection and never publish a truncated list on a partial pagination failure; cover-art and lyrics results are dropped unless they match the current track.

Login fixes

  • First-run auth no longer hangs on a blocking callback server; the startup wizard now uses the same async server as the in-TUI login (fixes Login hangs on startup. callback server never binds to port 8989 #364).
  • First-run double login completes without a restart: the native-streaming consent now waits for the Web API login's callback server to release port 8989 before binding it.

Verification

  • cargo fmt, cargo clippy -D warnings (CI feature set, all-sources, and default features), full test suite green throughout (419 tests on the CI set at HEAD).
  • Live-tested: fresh-profile first run (both OAuth consents complete in one run), startup behavior Play/Pause with deferred init, playlist opens during startup, queue playback.

…ounce config saves

- Send tick events only when due instead of after every input, so a keypress
  no longer triggers two full redraws
- Reserve an API pacing slot without holding the lock across the sleep and
  allow bursts of 5, so concurrent fan-outs (search, artist page) start
  immediately instead of 250ms apart; sustained rate is still 1 per 250ms
- Debounce config saves from volume/resize/shuffle keys (flush 500ms after
  the last change and on exit) instead of a synchronous read/parse/serialize/
  write per key repeat
Tables now format just the rows that fit on screen instead of the entire
backing collection every frame (10k-track frame draw: 58ms -> 2.5ms debug).
Album view no longer deep-clones the full album per frame, and the playing-
row lookup borrows the playback context instead of cloning it.

The track table also keeps its scroll position anchored: the cursor moves
within the visible rows and the view only scrolls once the cursor reaches
the top visible row when going up. Mouse clicks use the anchored offset so
row mapping stays exact.
…ner animation gating

- The changelog cache now hands out an Arc and the Home draw borrows the
  cached span text instead of deep-cloning every styled line each frame
- The banner gradient object is cached on the three theme colors it is
  built from; glyph spans borrow the static banner text instead of
  allocating a String per cell
- The fast animation tick now engages whenever the Home screen is
  displayed instead of only when its block has keyboard focus, which was
  almost never the case, so the banner gradient visibly animates again
All changelog lines are now wrapped to the area width when the cache is
built (headers and plain paragraphs relied on Paragraph wrap before), and
the Home draw slices out only the visible rows instead of using
Paragraph::scroll, which re-composed every line above the offset each
frame. Frame cost no longer grows with scroll depth. Includes tests for
the scroll row mapping and scrolling past the end.
…play

- Run non-Spotify IoEvents (lyrics, cover art, Subsonic/radio/local, telemetry) on a concurrent service lane so slow Spotify calls no longer head-of-line-block them; replace the try_recv/sleep poll with a blocking bridge thread
- Show the UI immediately; run the update check, token validation, and librespot session init as background tasks, and reuse one /me response instead of three startup round trips
- Publish playlists page 1 right away and fetch remaining pages + rootlist folders in the background; run full-pagination sorts as detached tasks
- Open playlists on Enter immediately with a loading state and dedupe repeated open dispatches
- Stash play requests while native streaming recovers and replay them afterward; stop swallowing transfer/activate errors and gate the playing state on a real event; park presses with no usable backend behind a status message instead of the Error screen
- Cache album/artist metadata (TTL LRU); reuse HTTP clients for Subsonic, radio, and announcements; fetch top-artists mix concurrently
- Throttle decoded-source seek during drags; skip the per-tick session snapshot until a save is due; precompute sort keys and lowercase friend names; cache help rows; read shuffle/repeat state without cloning the playback context
Address the pre-PR review of the concurrent event pump / deferred startup:

- native load watchdog gains an attempt cap with a growing window;
  Loading/Preloading feed it and Unavailable disarms, ending the teardown loop
- pending_playlist_open cleared at every auth-gate/invalid-id drop
- cover-art and lyrics writes gated on the currently desired key/identity
- deferred streaming init refreshes playlists so rootlist folders reconcile
- playlist refresh preserves folder selection and restores the previous
  complete list on a partial pagination failure instead of publishing a stub
- streaming OAuth runs before raw mode, off the reactor via spawn_blocking;
  deferred path stays cache-only and only wipes creds on genuine auth rejection
- TrackChanged clears the parked request; replay gains a 30s recency gate
- OAuth callback accept() errors are tolerated up to a cap
- per-playlist in-flight guard collapses concurrent sort re-paginations
On a fresh first run the Web API and streaming logins run back-to-back
on the same callback port, so the second consent could fail until a
restart. Retry-bind the port before launching the streaming OAuth flow.
@LargeModGames LargeModGames merged commit 93f7eab into main Jul 13, 2026
15 checks passed
@LargeModGames LargeModGames deleted the perf/optimizations branch July 13, 2026 12:49
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.

Login hangs on startup. callback server never binds to port 8989

1 participant