0.2.72 - Magic button, Next up fixes

This commit is contained in:
ponzischeme89
2026-08-17 11:41:36 +12:00
parent 36cb1324fd
commit 12b27c77c3
2655 changed files with 5435 additions and 254 deletions
+135
View File
@@ -0,0 +1,135 @@
# Performance: the Wholphin review, and what came of it
An engineering note on how Memby's performance work compares with
[Wholphin](https://github.com/damontecres/Wholphin), which techniques were taken from it,
and what the licence requires. It is a record of decisions, not a tutorial — the reasoning
behind each individual mechanism lives beside the code in `CLAUDE.md`.
## Licensing position
Memby is GPL-2.0. Wholphin is GPL-2.0. Adapting Wholphin source into Memby is therefore
permitted outright, provided the combined work remains under GPLv2 and attribution is
preserved. Both obligations are met in `NOTICE`, which names Wholphin, names the Memby
files adapted from it, records that Memby's modifications are its own, and notes that
Wholphin's own profiling code descends from Jellyfin Android TV.
Two practical rules were followed:
- **No verbatim copying.** Every technique below was reimplemented against Memby's own
architecture. That is a deliberate choice rather than a licence requirement — Memby's
repository is dual-path (gateway and direct-to-Emby) and its player is a single
`PlayerActivity` with a pre-roll, credits pane and trailer chain, so ported code would
have needed more reshaping than rewriting.
- **Attribution attaches to techniques, not only to code.** `PlayerEngine.kt` and
`TrickplayPreview.kt` carry in-file attribution because their *design* is Wholphin's even
though the source is not. Anything reproduced independently is marked below so the
distinction stays legible.
## Comparison
Wholphin's speed comes from a small number of decisions rather than from micro-optimisation,
and Memby had already absorbed most of them before this review:
| Technique | Wholphin | Memby |
| --- | --- | --- |
| One shared OkHttp connection pool across API, artwork and media | yes | yes — `data/remote/HttpStack.kt` |
| Media bytes pulled through the app's own HTTP client | yes | yes — `PlayerEngine.streamClient`, adapted |
| Constant-bitrate seeking for containers with no usable seek table | yes | yes — `PlayerEngine.extractorsFactory`, adapted |
| Seek-preview thumbnails from a server-side index | tile sheets | BIF, independently implemented — `data/Trickplay.kt` |
| Cached home rendered before the network answers | yes | yes — `HomeCache`, per profile |
| Server-side artwork resizing rather than full-resolution downloads | yes | yes — `maxWidth`/`quality` on both paths |
| Request de-duplication and single-flighting | yes | yes — repository-wide, including focus prefetches |
| Speculative resolution while a card is focused | partial | yes — two-wave warm, metadata then detail page |
| Pre-opened connection to the media host | n/a (same host as API) | **new, see below** |
The last row is the one real divergence, and it is architectural rather than a matter of
discipline. In Wholphin the API and the media stream are the same Jellyfin host, so the
connection is already warm by the time anybody presses Play. In Memby's gateway mode they
are two different machines: everything except video goes to the gateway, and video
direct-plays from Emby. Wholphin gets that warm connection for free; Memby had to be told
to open one.
## Bottleneck identified
Memby's own measurements (Chromecast with Google TV, NAS gateway, `MembyPlayback` trace)
had already established the shape of a resume: `prepare()` → first frame is over 90% of it,
stream negotiation ~120 ms, app startup ~365 ms. Within that, the largest single term was
cold versus warm connection to Emby — **the same file at the same seek position took
4177 ms on the first playback of a session and 2004 ms on the second.** The first playback
pays DNS, the TCP handshake, the TLS handshake and Emby's file open, none of which the
second pays.
Two things that look like causes and are not: subtitle auto-selection costs 20200 ms, and
the seek itself about 900 ms.
## What was implemented
**`data/remote/StreamWarmer.kt`** — opens a connection to the machine video comes from
before any video is requested. It works only because of the shared pool: a connection opened
by an unrelated HEAD request is the one ExoPlayer picks up later, so nothing is handed over
but the address.
The design constraints that shaped it are recorded in `CLAUDE.md`; in brief, it warms the
*host* and never the title (warming the film's first byte would put a delivery for a title
nobody watched into somebody's server history, which is the same objection that stops this
app warming `PlaybackInfo` on focus); it remembers the address across process restarts, since
on the gateway path a resolved stream URL is the only thing that ever names Emby and the
slowest playback was always the first one after a cold start; it is warmed both at
application start and when card focus settles on a playable item; and every failure is
silent, because it is a hint whose worst case is the behaviour that existed before it.
This is reproduced independently. Wholphin has no equivalent — it does not have the problem.
**`performance/StartupTrace.kt`** — the launch and navigation timings the playback trace did
not cover: `home_visible`, `first_row_visible`, `home_interactive` as cumulative launch
milestones, and `detail_visible` as a repeatable span. Debug-only, and every entry point
returns before allocating on a release build. Milestones are recorded from `LaunchedEffect`s
rather than composable bodies, so measuring the launcher cannot itself be a reason the
launcher recomposes.
**`PlaybackTrace.AUDIO_START`** — recorded from a media3 `AnalyticsListener`, which is the
only interface reporting the moment the audio position genuinely begins advancing. It is
worth having beside `first_frame` rather than inferred from it: a bitstreamed surround track
waits on the receiver to lock to the format, and a track with no hardware decoder falls back
to software. Either leaves a picture running silently for a moment, which a viewer reads as
broken rather than slow, and which no video-side mark would show.
## What was reviewed and deliberately left alone
Most of the brief's checklist was already satisfied, and saying so is more useful than
changing working code:
- **Artwork** is already resized server-side (`maxWidth` and `quality` on both the gateway
proxy and direct Emby paths), cached in a memory cache sized at 25% and a 128 MB disk
cache, decoded to hardware bitmaps, and fetched over the shared pool with crossfade off.
- **Home startup** already renders `HomeCache` before the network answers, decodes that
cache off the main thread, skips unchanged writes, and refreshes sections in parallel.
- **Compose** is already disciplined in the ways that matter here: `HomeViewModel` exposes
three `distinctUntilChanged` projections rather than one state object, animated values are
read only in draw and layout lambdas, and lists deduplicate keys rather than folding in an
index.
- **Request cancellation and de-duplication** are already repository-wide, with focus
prefetches single-flighted on the repository's own scope precisely so a cancelled prefetch
does not abort the request a press is about to want.
- **Returning from playback** already restores tab, season, grid offset and focused band via
`ui/detail/DetailPosition.kt`, and deliberately re-requests the item because the episode
just watched is now watched.
No obsolete workaround was found that the new work makes redundant; the connection warm adds
a capability rather than replacing a compensation.
## Measuring it
```
adb logcat -s MembyStartup MembyPlayback
```
`MembyStartup` prints launch milestones as `stage=cumulative(+delta)` and detail spans as
`detail_visible=Nms`. `MembyPlayback` prints one `event=first_frame` line per launch carrying
the full stage breakdown.
The connection warm is a network effect and cannot be measured by a unit test. The honest
comparison is first-playback-of-a-session against second, on one television, server and
title — the pairing the original 4177/2004 ms figure came from. `:benchmark`'s
`PlaybackBenchmark` measures the same span off trace sections when a signed-in television is
available; it needs a real Emby, which is why it is excluded from ordinary CI.