Add a ?pi=1 profile, and stop re-deriving search keys per keystroke
Two separate costs, both measured on musicdolphin (Pi 4, 1920x1080 kiosk), where the app was burning ~70% of a core with nothing happening on screen. Per-frame work. The ambient canvas repaints a full-screen gradient plus a particle field every frame, and `usePlaybackClock` pushes a React setState per animation frame into both PlayView and PlayerBar for the whole length of a track. `?pi=1` (lib/ lowPower.ts) makes those cheaper rather than switching them off: the canvas paints a quarter of the pixels at 30fps with a bubble cap, and the clock renders ten times a second - a progress bar advances one pixel every few hundred ms and its label has one-second resolution, so nothing on screen can tell. Only the effects with no cheap version actually go: the backdrop-filter glass blur and the decorative CSS loops. Also drops a redundant full-canvas clearRect that the opaque gradient always covered. Search. normalize() runs a Unicode NFD decomposition, and albumMatches/songMatches called it on every album title and every track title on every keystroke - 4969 of them for a track search, whose answer cannot change until the library does. buildSearchIndex does it once per library payload; a keystroke is now String.includes over strings that already exist. On the real library that is 33ms -> 3.4ms for an eight-letter track query on a laptop, and this runs on a Pi. The same index partitions albums by shelf and pre-sorts each shelf's categories, which App and BrowseView were deriving separately from the same data. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,14 @@ npx tsc --noEmit # strict, noUncheckedIndexedAccess
|
||||
Only the pure modules are tested - the search, the keyboard machine and the time
|
||||
formatting. That is where the behaviour lives; the components are mostly layout.
|
||||
|
||||
`src/lib/search.ts` builds its normalized search keys once per library payload
|
||||
(`buildSearchIndex`) rather than per keystroke. It matters more than it sounds: a real
|
||||
collection is 343 albums and 4969 tracks, and `normalize()` does a Unicode NFD
|
||||
decomposition, so track search used to mean five thousand of those before a single
|
||||
comparison - 4 ms per keystroke on a laptop, and this runs on a Pi. The same build also
|
||||
partitions albums by shelf and pre-sorts each shelf's categories, which `App` and
|
||||
`BrowseView` were otherwise deriving separately from the same data.
|
||||
|
||||
## How it is put together
|
||||
|
||||
The split that matters: **what is playing** comes from the backend, **what you are
|
||||
@@ -52,7 +60,8 @@ are local.
|
||||
| Path | What it is |
|
||||
|---|---|
|
||||
| `src/lib/keyboard.ts` | The key map, as a pure function from a keypress to a list of actions. Testable without a DOM. |
|
||||
| `src/lib/search.ts` | Filtering and the three-level browse hierarchy. The whole library is in memory, so this is instant. |
|
||||
| `src/lib/search.ts` | Filtering and the three-level browse hierarchy. The whole library is in memory, so this is instant - see the note on `buildSearchIndex` below. |
|
||||
| `src/lib/lowPower.ts` | The `?pi=1` profile: what gets cheaper, and by how much. |
|
||||
| `src/lib/covers.ts` | Generated cover art from the album's three colours, and the book-spine treatment. |
|
||||
| `src/hooks/usePlayerState.ts` | The websocket, with reconnect-and-reseed. |
|
||||
| `src/hooks/usePlaybackClock.ts` | Interpolates the 2 Hz position onto animation frames. |
|
||||
@@ -84,6 +93,34 @@ pointer. `F1` shows the same list in-app.
|
||||
`SHIFT+arrows` is the one addition to the mockup: plain arrows were already taken, and
|
||||
seeking is the one thing a real player can do that the mockup could not.
|
||||
|
||||
## The Raspberry Pi profile
|
||||
|
||||
`?pi=1` loads the same app with the two per-frame costs cut down. It exists for
|
||||
musicdolphin - a Pi 4 driving a 1920x1080 kiosk screen, where the app was spending
|
||||
about 70% of a core with nothing happening.
|
||||
|
||||
What it changes, all of it in `src/lib/lowPower.ts`:
|
||||
|
||||
| | Full | `?pi=1` |
|
||||
|---|---|---|
|
||||
| Ambient canvas backing store | 1:1 with the screen | half scale, capped at 1280 px |
|
||||
| Ambient canvas frame rate | display refresh | 30 fps |
|
||||
| Bubbles alive at once | unlimited | 60 |
|
||||
| Progress-bar re-renders | every animation frame | 10 per second |
|
||||
| `backdrop-filter` glass blur | on | off - plain translucent instead |
|
||||
| Decorative CSS animation loops | on | off |
|
||||
|
||||
The principle is *make the expensive thing cheaper before switching it off*. The canvas
|
||||
is the app's whole look, so it stays and paints a quarter of the pixels half as often -
|
||||
on a soft gradient behind content that is close to invisible. Only the effects with no
|
||||
cheap version go: a real backdrop blur, and the decorative loops that carry no
|
||||
information. The aquarium pets stay on even here; they are the reward the typing game
|
||||
pays out, and a handful of `transform` writes is not what is slow.
|
||||
|
||||
The flag is read once at module load from the URL and nowhere else - no persistence, no
|
||||
auto-detection. That is what makes "is this the profile or the hardware?" answerable by
|
||||
editing the address bar. The kiosk gets it from `pi_kiosk_url` in the ansible repo.
|
||||
|
||||
## Parent mode
|
||||
|
||||
`?parentMode=1` opens a settings panel - volume limits, the rotary step, button
|
||||
|
||||
Reference in New Issue
Block a user