Performance & Caching — pagination, cover-cache, in-mem-LRU, async writes#148
Open
wowa1990 wants to merge 19 commits into
Open
Performance & Caching — pagination, cover-cache, in-mem-LRU, async writes#148wowa1990 wants to merge 19 commits into
wowa1990 wants to merge 19 commits into
Conversation
The conf_update script ran ~60 `cat <<< $(jq <FILTER>) > ${CONFIG}`
directly against /etc/mupibox/mupiboxconfig.json. That truncates the
config before jq's output is appended — a jq error or a kill in the
wrong moment leaves the file empty and bricks the box. Phase-3 fixed
this pattern in 12 other scripts; conf_update was overlooked because
it only runs during MuPiBox-Update (manual or via the admin UI).
- update_config() helper: jq → tmpfile → atomic mv (preserves CONFIG
on jq failure).
- ensure_theme() helper: previous check used `cat $CONFIG | grep
<theme>` which matches the theme name as a substring anywhere in
the JSON. ensure_theme uses jq's array-index check against
installedThemes — exact match, no false positives.
- Fix a malformed jq filter in the BATTERYCONFIG init block (trailing
string literal that produced a jq parse error on fresh boxes; with
the old truncate-race that could blank the config).
- 330 lines collapse to ~250, semantics unchanged.
The pagination params on searchAlbums/getArtistAlbums/getShowEpisodes/ getPlaylistTracks come straight from user query strings and were spliced into the cache-key without validation. A request with ?limit=999999 produced a unique cache file per call; the cache directory could grow without bound. The SDK call itself was already guarded with Math.min(limit, 10), but the cache-key bypassed that. - normalizePagination() clamps limit to [1, 50] and offset to >= 0, rejecting NaN/negatives. Both the SDK arg and the cache-key now use the clamped value. - pruneCacheIfNeeded() runs after each saveToCache. When the cache dir exceeds 1000 files, it evicts the 200 oldest by mtime — cheap LRU.
index.php fired three blocking external HTTPS requests on every render — version.json + news.txt + api.github.com. With flaky internet the admin landing page hung 2-5s; api.github.com rate-limits at 60 req/h per IP, which a kid hammering F5 reaches quickly. Add mupibox_cached_url() helper, cache to /tmp/.mupibox.indexcache.* with a 60min TTL. On fetch failure fall back to the (possibly stale) cached value rather than serving a blank page or breaking json_decode. The DEV tag is also cached — only changes once per day upstream so the 60-min refresh is plenty.
header.php is included on every admin page and previously fired three exec()s on every render — `sudo iwgetid -r`, `sudo iwconfig wlan0`, and the `ps -ef | grep websockify` for the VNC nav link. Each exec forks a sudo helper and (for iw*) opens a netlink socket; across admin navigation that's ~60 wasted forks/min and visible lag on the Pi. Add a tiny mupibox_cached_exec() helper that memoises the result to /tmp/.mupibox.headercache.<key> with a 5s TTL. The wifi-quality readout stays current enough for the periodic JS poll in footer.php that replaces it, while burst navigation no longer fans out into sudo+exec storms.
Five PHP pages (admin, mupi, mupihat, spotify, smart) had ~15 copies
of the same write pattern:
$json_object = json_encode($data);
$save_rc = file_put_contents('/tmp/.mupiboxconfig.json', $json_object);
[sometimes: exec("sudo chmod 755 ...");]
exec("sudo mv /tmp/.mupiboxconfig.json /etc/mupibox/mupiboxconfig.json");
Two failure modes the audit (B8) flagged:
1. Concurrent saves from two admin tabs (e.g. mom edits the playtime
limit on her phone while dad changes the WiFi on his laptop)
both write to the same fixed-name /tmp/.mupiboxconfig.json, then
both sudo mv it -- the second's data overwrites the first's, the
user whose save "won" sees their settings persist while the other
silently loses theirs.
2. No flock at any layer means concurrent file_put_contents +
sudo mv on the same destination can interleave at the file-system
level, leaving a half-written or torn JSON. Backend reads it,
json_decode fails, mupiboxconfig is wedged until somebody edits
it by hand.
Fix: new shared helper AdminInterface/www/includes/save_config.php
exposing save_mupiboxconfig(array $data, ?string &$errorOut = null).
It:
- Acquires LOCK_EX on /tmp/.mupiboxconfig.lock (flock-serialised)
- json_encodes with JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT
- Writes to a per-call random /tmp/.mupiboxconfig.<8hex>.json
(so two writers waiting on the same flock don't also share a
fixed-name scratch path)
- sudo mv into place via escapeshellarg
- Releases the lock in a finally block
Auto-loaded by includes/header.php so every admin page has it
available without per-file require_once. All 15 callsites switched:
admin.php 2 (write_json() helper + $change==2 branch)
mupi.php 2 ($change==1, $change==2)
mupihat.php 5 ($change==1..5)
spotify.php 2 (OAuth-completion + generic $change branch)
smart.php 4 ($change==1..4)
The pretty-printed output is a deliberate one-time switch from the
old compact json_encode: matches the template (config/templates/
mupiboxconfig.json) and conf_update.sh's jq output, so the live
config will no longer oscillate between compact (after PHP save)
and pretty (after bash update). All write callsites verified PHP-
syntax-clean on the box.
… (L3) REG01_Charge_Voltage_Limit.set() was assigning to self.VRE0G instead of self.VREG, so the public VREG attribute stayed on its __init__ value (0) forever. Every consumer reading .VREG after a register update -- log_register_values() in mupihat.py, the Admin-UI Battery Status block -- saw a stale zero. Observed live as `Charge Voltage Limit: 0` in the mupi_hat journal on an actively-charging pack where the actual hardware-side VREG was correctly set to 8400 mV (the BQ25792 internal register held the right value, only the Python-side mirror was wrong). One-line fix: VRE0G -> VREG. The __init__ already sets self.VREG correctly via the same expression; the set() override just needed the same attribute name.
battery_soc()'s elif chain ended at `elif VBat > v_0 : Bat_SOC = "0%"`,
leaving Bat_SOC at its init default of empty string '' for any VBat
at-or-below v_0 (the BMS cutoff region).
Consequences of the empty value:
- Telegram low-battery message rendered as "... battery is at "
with nothing after "at" -- looked like a broken bot.
- Frontend battery icon fell through every @switch @case to the
@default branch, which is fine, but the underlying string in
/tmp/mupihat.json was literally '' instead of '0%'.
Replace the final elif with `else` so anything at or below v_0 reads
as "0%". Bat_Stat is already 'SHUTDOWN' in that region (the second
chain below covers it), so the user gets a coherent SOC + status
pair.
fetchAllPaginatedResults issued every page via concatMap, which is strictly sequential — page 2 only kicked off after page 1's response was processed. For an artist with 28 album pages (pageSize=10) that serialised the whole pagination through the round-trip latency, adding ~1s to the medialist render. Switch to mergeMap with concurrency 8 so up to 8 pages fly in parallel. The backend event-loop is single-threaded and serialises internally, but pipelining still amortises the per-request frontend overhead. Output order is no longer page-aligned, but every caller already sorts on the client before render (localeCompare on title in medialist.page) so the user-visible order is unchanged.
fetchMediaFromArtist used to call fetchMedia(category) which loads EVERY entry in the category through updateMedia, then filter the result to the chosen artist. For a library with mixed sources (143 library entries, 7 spotify-album-IDs, 8 spotify-artist-IDs) that meant ~55 backend round-trips fired in parallel from a single artist click — even though the user only wanted Benjamin Blümchen's albums. updateMedia's mergeMap has no concurrency cap (L2 in the backlog), so all 155 items contend for the backend event loop simultaneously. Fast path: when the clicked artist is a Spotify artist (we know artistid from the navigation state's coverMedia), call spotifyService.getMediaByArtistID directly. ~6 backend calls, no contention with other artists. Library/local entries keep the load-then-filter path because data.json may contain multiple rows for the same artist name with no single API call to retrieve them as a group.
The swiper for an artist with hundreds of albums (Benjamin Blümchen has 276) used to render every <swiper-slide> upfront — ~1400 Ionic- flavoured DOM nodes (276 × ion-card/grid/row/col/img) through CSS recalc on a single-threaded RPi-Chromium kiosk. The user reported ~6s of empty cards even after the loading spinner cleared. Three combined changes in swiper.component: - Progressive render in three phases: - t=0: 15 slides (immediate first paint, viewport + swipe-buffer) - t=120ms: 60 slides (comfortable swipe range) - then: +30 per requestIdleCallback chunk until full list When the user is touching/scrolling, idle callbacks defer — touch input wins, slide expansion pauses, UI stays responsive. A 2000ms timeout argument is the safety net so we don't get stuck under- rendered if idle never arrives. - cloneDeep (lodash) → structuredClone (native, ~5-10× faster on plain-object arrays). Observables on SwiperData.imgSrc aren't cloneable, so we keep imgSrc by reference and structuredClone the per-slide data payload only. Removes the lodash-es/cloneDeep bundle dependency. - Stable trackBy: `track currentData.name` (instead of object identity) so a re-emit of the same logical list doesn't recreate every <swiper-slide> DOM node. - <img loading="lazy" decoding="async" fetchpriority="low"> on the template's image (the first two attributes are Chromium-honoured for slides scrolled out of view; fetchpriority drops these below page-chrome paints). For shorter swipers (homepage's ~50 artists) this is a no-op because the initial 15 covers the visible region and stage 2 finishes the rest within 120ms — imperceptible.
Six iterative refinements after Phase-7.6's progressive-render landed:
Render reliability (swiper.component.ts):
- effect-driven expansion: reacts to data() arriving late instead of
bailing when initial array is empty
- setTimeout-driven chunks (15 → +30 every 80ms) replace the
requestIdleCallback-driven Stage-3 expansion which stalled on the
Pi-Chromium kiosk under SDK polling load
- swiper.update() one microtask after each renderableLimit change so
swiper-element picks up newly-rendered <swiper-slide> children
- position-restore effect tracks pageIsShown only (not shownData)
so progressive-render expansions no longer teleport users back to
slide 0 mid-swipe; cachedSwiperPosition is kept in sync via a new
onSlideChange handler
Cover preload (swiper.component.ts):
- on each slidechange, eagerly fetch next 12 + previous 4 cover URLs
into the browser image cache via Image() (preloadedSrcs dedupes)
- eager kick on ionViewDidEnter so first interactions have covers
ready without waiting for the first slidechange event
Feel tuning (swiper.component.html):
- speed=900ms, touch-ratio=0.5: a fast flick now travels half the
distance and animates fluidly — matches a child's pace
- resistance="true" + resistance-ratio="0.85": rubber-band at the
list edges instead of free overshoot (which had been teleporting
rare past-edge swipes back to slide 0)
- slides-per-group="1": one group per gesture, no multi-jump
Squashed from af64646d 739f90d2 30d1ea3d 21089455 81dc6bd4 629ece36.
… M4)
M3 -- saveToCache used fs.writeFileSync, blocking the event loop 5-30
ms per write on the SD card. During an artist-browse burst that
populates dozens of cache files in a row, the cumulative pause was
seconds of unresponsive frontend. Switched to fs/promises.writeFile;
saveToCache is already async, so the change is a single `await` and
zero ripple.
M4 -- getFromCache used to do existsSync + statSync + readFileSync +
JSON.parse on every hit, ~5-30 ms of SD time per call. With a typical
artist browse hitting the cache hundreds of times, that's the
dominant bottleneck. New in-memory layer in front of the SD cache:
- memCache: Map<string, CachedSpotifyData>
- memCacheCap: auto-sized to 5% of os.freemem() / ~10KB-per-entry,
clamped to [50, 500]. On Pi 4 with 3 GB free this saturates at
500 entries (~5 MB). On a Pi 3 with 100 MB free it caps around
50 entries -- same code stays safe on either platform.
- LRU via Map insertion order: on get/set we delete-then-reinsert
so the touched entry moves to the tail; when size > cap, we
evict the first key (oldest).
getFromCache now checks memCache first -- if hit, no syscall at all.
On miss, the SD read is also async (M3 sibling change) so the event
loop stays free, and the result is populated into memCache for the
next hit. saveToCache also populates memCache after the write so
fresh writes are immediately mem-cached.
Hit/miss counters (memHits, memMisses) on the service instance for
future observability. No log on mem-hit to keep journal volume sane;
only stale-mem and actual SD reads log.
tsc --noEmit clean.
admin.php had 6 separate file_get_contents+json_decode pairs reading /etc/mupibox/mupiboxconfig.json across one HTTP request, plus another in header.php. On an SD card that's 35-140 ms of pure re-parsing the same bytes per admin page load. On closer inspection only 1 of those 6 in admin.php is genuinely redundant -- the pre-header auth-gate (line 24). The other 5 read after an external mutation (sudo unzip restore, sudo conf_update.sh, sudo start_mupibox_update.sh) and need fresh state. header.php's read is the redundant counterpart to the auth-gate. New mupibox_config(bool $forceReread = false): array in includes/save_config.php (now misnamed but fits -- it lives next to save_mupiboxconfig). Static $cache in the function persists per request; $forceReread = true is an explicit opt-out for the post- mutation call sites that need fresh data. Call sites converted: - admin.php line 24-27 -- pre-header auth gate, default cache mode - header.php line 39-42 -- routes through mupibox_config() so the shared static avoids a second file_get_contents per request - admin.php 5× post-mutation reads -- mupibox_config(true) to force re-read after the external command finished In the common case (no admin POST action, just page navigation), this drops from 2 file_get_contents to 1 + memory hit. In the action case where mutations chain, each chain link still does exactly one read, no redundant ones. All three files php-syntax-clean on the box.
Two scripts dominated the box's sudo audit.log:
save_rrd.sh -- crontab fired it 4×/minute (every 15s), each invocation
issued 3 sudo+rrdtool forks. That's ~17,000 sudo/day for a database
the user looks at via Admin-UI graphs at second-to-minute resolution,
not 15s. crontab.template now invokes it once per minute -- 75%
reduction with no user-visible loss of graph fidelity.
get_monitor.sh -- ran a `while true; sleep 1; sudo vcgencmd
display_power; jq+mv` loop in a systemd service. ~86,400 sudo+fork
+ ~86,400 file rewrites per day, almost all of them writing the
SAME state ("On" / "Off") that was already on disk. Two changes:
- sleep 1 -> sleep 5. 80% fewer iterations; the kid never notices
a few-second delay before touch is re-enabled after the screen
actually blanks.
- In-process LAST_STATE tracking + write_state() helper. monitor.json
only gets rewritten when the polled state changes from the
previously-written one. In typical daily use ("On" for hours,
"Off" overnight) that drops file rewrites to a handful per day,
not 86k.
Also tightened existing seed/init paths: switched to consistent
quoting of "${VAR}", added a fallback for /sys/class/backlight read
when vcgencmd returns -1 (was unquoted before, would error if any
backlight glob failed).
Combined audit.log impact: ~83% fewer sudo entries from these two
sources, freeing ~90k log lines/day plus the corresponding CPU.
bash -n syntax-clean.
updateMedia's second mergeMap (the one that flattens Observable<Observable<Media[]>> coming out of the per-item iif() chain) had no concurrency parameter -- default unlimited. For a library of N items, all N inner Observables subscribed in parallel, each kicking off a Spotify/RSS HTTP fetch + cache lookup. With N=100+ items the result was a thundering herd against the backend plus a synchronous cache-write storm on SD. mergeMap((items) => from(items), 5) caps the concurrent inner subscriptions to 5. The Spotify SDK service has a 100 ms minRequestInterval, so 5 concurrent gives ~50 req/s peak which is well under the Spotify quota. The staggering also smooths the SD write pressure from saveToCache (now async after M3, but still sequential per-request). The first mergeMap (line 520) is intentionally NOT capped because its upstream emits only a single Observable<Media[]> from the http.get, so concurrency is irrelevant there -- only one inner subscription ever. tsc --noEmit clean.
Cover-Latenz-Optimierung. Nach Phase 7.6 brauchte ein Klick auf einen
Spotify-Artist mit vielen Alben (Benjamin Blümchen ~276) noch ~3 s,
dominiert von i.scdn.co-Cover-Roundtrips: Chromium limitiert
HTTP/1.1-Verbindungen auf 6 parallel pro Host, 276 Cover × 50-150 ms
÷ 6 = ~2.5 s pures CDN-Warten.
Backend (src/backend-api):
- Neuer CoverCacheService mit drei Layern:
1. In-Memory-LRU (Map<id, Buffer>; Cap dynamisch via os.freemem(),
clamped auf [512 KB, 10 MB]; LRU via Map-Insertion-Order)
2. SD-LRU (cache/covers/<imageId>.jpg; Cap 2000 Files ~150 MB;
Pruning 400 ältester per mtime bei MAX_FILES Überschreitung)
3. Upstream-CDN (https://i.scdn.co/image/<id> mit 5 s
AbortSignal-Timeout)
- Robustheits-Layer:
* isValidImageId() /^[A-Za-z0-9]{32,80}$/ wehrt Path-Traversal ab
* 5-Minuten-Negative-Cache verhindert hammering bei broken Images
* pendingFetches dedupliziert concurrent same-key requests
(ein Artist-Klick = 1 CDN-Hit pro imageId, nicht 200×)
* SD-Hit touched mtime async für sauberes LRU-by-mtime
- GET /api/spotify/cover/:imageId Endpoint mit
Cache-Control: public, max-age=31536000, immutable
(Spotify image-IDs sind content-addressed, Browser darf
aggressiv cachen)
Frontend (src/frontend-box):
- utils.ts: neuer localizeCoverUrl() Helper
* https://i.scdn.co/image/<id> → /api/spotify/cover/<id>
* undefined/null → '../assets/images/nocover_mupi.png'
* Alle anderen URLs pass-through (lokale /cover/*, RSS-URLs)
- spotify.service.ts: 11 callsites umgestellt
* 9× album/audiobook/episode/playlist cover
* 2× artistcover / showcover
Verifikation post-deploy:
Cold-Miss (CDN-Fetch + SD-Write): HTTP 200, 189 KB, 169 ms
Hot-RAM (gleiche ID): HTTP 200, 189 KB, 5.5 ms (30× schneller)
MD5-identisch zwischen den beiden Antworten
Path-Traversal-Test: HTTP 404 (validator greift)
Robust gegen Chromium-Cache-Wipes, Box-Reboots, Spotify-CDN-Aussetzer
für bereits gecachte Bilder. Cache überlebt pm2 restart (SD-persistent);
nur das In-Memory-Layer regeneriert sich beim Backend-Start.
Squashed from 5170e387 + 6e69caef (Backup-Tag pre-squashes-backup).
Adds a percent number stacked under the existing 4-bucket battery
icon so users can tell 80% from 75% from 70%, instead of the icon
jumping straight from "75%" to "25%" with nothing in between.
Why 5% steps and not 1%:
The Li-Ion discharge curve in the plateau zone (~3.7-3.9 V/cell)
drops only 2-3 mV per 1% energy -- smaller than the BQ25792 ADC's
noise floor (~10 mV LSB) and tiny compared to the 30-100 mV load
sag the box's audio + display produce. A 1% display would
zap-zap-zap constantly between adjacent values for purely
electrical reasons. 5% steps map to ~90 mV per step which sits
cleanly above all that noise.
Backend (mupihat_bq25792.py):
- new record_vbat_sample() / smoothed_vbat() helpers maintaining
an 8-element rolling buffer of recent VBAT readings (~32 s
window at the 4 s polling cycle). Without smoothing the
granular percent would visibly flap when audio loads spike.
- new battery_percent_granular() returns (int 0-100 rounded to
5%, source: "voltage" | "charging"). Piecewise-linear
interpolation across the same v_100..v_0 thresholds the legacy
battery_soc() already uses, so existing per-pack config drives
both displays.
- to_json() exposes Bat_Percent + Bat_PercentSource alongside
the legacy Bat_SOC string. SOC string stays for Telegram bot
messages and any other text consumer that wants a label.
- battery_soc() `>` -> `>=` so a pack sitting exactly at v_100
renders as "100%" instead of falling one bucket down to "75%".
Without this fix, a freshly-finished CV charge at VBat=8200
shows up as "75%" everywhere the legacy string is consumed.
USB-C mode (sentinel v_100=1 from the "no battery" battery type)
returns (0, "voltage") and the frontend skips the percent text
entirely -- the existing plug icon already conveys it.
Backend (mupihat.py):
periodic_json_dump now calls hat.record_vbat_sample(read_Vbat())
inside the i2c_lock right after read_all_register() so the
smoothing buffer is fed once per cycle from the freshly
refreshed register state.
Frontend (mupihat.ts):
Interface gains Bat_Percent (number) and Bat_PercentSource
("voltage" | "charging").
Frontend (mupihat-icon.component):
- Icon bucket now driven by Bat_Percent (>=95 / >=70 / >=45 /
>=20 / else) instead of the legacy Bat_SOC string. Both icon
and percent text now come from the same source so they can
never disagree at threshold edges. Backward-compat: if
Bat_Percent is undefined (older backend), falls back to the
legacy 4-case Bat_SOC switch.
- Layout stacks ion-icon ABOVE the percent text (column flex)
instead of side-by-side. Reason: home.page reserves a fixed
70x70 px ion-button for status icons, holding BOTH the
cloud-online/offline indicator AND mupihat-icon. A horizontal
"<icon> 78%" expansion pushed the cloud icon out of the 70 px
width. Vertical stacking keeps the horizontal footprint at
icon-only size, so the cloud icon retains its space, and the
percent label sits legibly under the bucket icon.
- :host inline-flex+centered so the component element behaves
as a single inline cell inside ion-button's children area.
- During active CC/Taper charge the percent is prefixed with a
⚡ symbol — the voltage-based estimate is systematically
optimistic during charge (pack held above its rest curve by
the charge current), so the indicator tells the user "this
number is in-flight, not a settled reading".
Verified post-deploy on the box: cold Pack at VBat=8188 mV
correctly shows Bat_Percent=100 via piecewise interpolation +
smoothing, icon renders as Battery100.svg, percent text reads
"100%". Cloud icon visible alongside as before.
Squashed from 7ce1cb76 + c214943a + 14fe18f4
(Backup-Tag pre-phase12-squash-backup).
…t (AR5-19)
frontend-box's tsconfig.json had no strict-mode flags at all, while
backend-api has full "strict": true. That gap is what enabled the
"(component as any).x.set(...)" pattern across the specs (Backlog L6)
plus a handful of silently-untyped function parameters that were
never caught.
Full "strict": true would have unleashed hundreds of strictNullChecks
errors that require a much larger refactor. noImplicitAny alone is
the high-value subset: it forbids untyped parameters / object indexes
and is what closes the (x as any) loophole, without forcing a big-
bang nullability migration.
37 errors surfaced and fixed:
media.service.ts reduce() initial value {} -> typed accumulator
via .reduce<Record<string, ...>>((acc, ...) => ..., {}) for
mediaCounts (string -> number), covers (string -> string), and
coverMedia (string -> Media). 11 errors fixed in one block.
utils.ts keys array: untyped string[] -> (keyof ExtraDataMedia)[],
plus a narrow biome-ignored (target as any) on the assignment side
since the key types don't unify across the Media superset.
utils.spec.ts: ExtraDataMedia-typed `source` literal with explicit
`null as any` on the nullish properties (the spec deliberately
exercises null/undefined input to test the skip-on-nullish path).
add.page.ts, wifi.page.ts: handleLayoutChange(button) -> (button: string).
player.service.ts: seekPosition(pos) -> (pos: number).
app.component.ts: .catch(() => null) -> .catch((): null => null) on
two firstValueFrom promises so the lack-of-return-annotation
inference doesn't trip TS7011.
tsc --noEmit clean. Full "strict": true (strictNullChecks +
strictPropertyInitialization) is now a clear-cut separate phase --
roughly 250-300 errors mostly in pages/services that need real null
guards. Tracked in audit-round-5 for the next sprint.
…refCount=true (M1, M2)
M1 -- app.component:42 /api/monitor poller had no error handling. A
single 500 response, network blip, or backend restart would push the
error through toSignal, killing the observable forever (no retry, no
recovery). The same B11 pattern that protects current$/local$/
albumStop$/mupihat$ in media.service was just forgotten here. Add
per-request timeout(1000) + catchError-with-empty-object. The map
now requires monitor.monitor !== undefined to flip monitorOff to
true, so a recovered empty response from the catchError doesn't
spuriously show "Display off" -- distinctUntilChanged keeps the last
real value visible until the next successful poll.
M2 -- media.service mupihat$ used `shareReplay({ refCount: false })`,
which keeps the underlying interval(2000) → http.get hot for the
entire app lifetime regardless of whether any UI is subscribed. The
only consumer is mupihat-icon.component, which itself switchMaps
into the stream only when hat_active is true and the icon component
is mounted (toolbar / footer / medialist views). When no view
renders the icon -- e.g. login screen, fullscreen overlays --
~1800 wasted req/h hit /api/mupihat anyway. Switch to refCount=true
so the interval idles when nobody listens; the moment the icon
mounts, polling resumes with the next interval tick (max 2s
latency, indistinguishable in practice).
albumStop$ / network$ also use refCount=false but kept as-is here --
they have different consumer patterns and aren't called out in the
audit. Separate cleanup if needed.
tsc --noEmit clean.
|
This PR does not work. There are many dependencies which rely on other PRs you submitted. |
Author
|
i am not a maintainer of this project, i am just testing it for my purpose. And hope this helps the maintainers. I tried to merge your branches in following order: #151 -> #146 -> #148 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Was es macht
mergeMap(8)stattconcatMap(Bibliotheks-Loads ~4× schneller bei großen Künstlern)setTimeout-Chunks + Fallback (kein Hänger bei vielen Items)/api/spotify/cover/:imageIdmit SD-LRU (cache/covers/, cap 2000) + In-Mem-LRU (5% freemem, Pi3-tauglich) + Pending-Fetches-Dedup + Negative-Cache. Frontend rewrited alle Spotify-CDN-Cover-URLs → 30× schneller bei Warm-Hitsheader.phpexec()-Cache (Status-Bar wird einmal pro Request gebaut, nicht pro Block)mupiboxconfig.jsonrequest-memo (M5,$forceReread-Opt-Out für Post-Mutation-Reads)mergeMap-Concurrency-Cap (L2) auf 5 in updateMedia-Pipelinesave_rrd4×→1×/Min +get_monitorsleep1→sleep5 + skip-if-unchanged (~83% weniger sudo/Tag)mupihat$refCount=true → 1800 req/h wegbattery_soc()else-Branch (Display-only mupihat-Fixes aus Phase 7.8)noImplicitAny: trueaktiviert + 37 Type-FixesArchitektur (kurz)
Cover-Cache ist neuer Backend-Endpoint mit imageId-Validation (Path-Traversal-safe). Alle anderen Caches sind in-process / request-scope. LRU-Caps lesen
os.freemem()für Pi3/Pi4-Tauglichkeit. Polling reduziert primär durchshareReplay({ refCount: true })und längere Intervalle für niedrigfrequente Streams.Test in Codespaces / lokal
curl http://127.0.0.1:8200/api/spotify/cover/abc123zweimal hintereinander → erster Cold-Miss ~170ms, zweiter Warm-Hit ~5msmupihat-Request pro Subscriber (alle teilen sich denselben Stream)npm run buildim backend-api und frontend-box → erwartet clean, keineany-Warnings mehrHinweis: RRD-Cadence + mupihat-Display-Anteile sind Box-only.
Offene Punkte