Wolfskin TV: faster, quieter, smoother

Measured on your Xiaomi box with Wolfskin (Opt) 1.1.0-133 and from tank, on 2026-11-06. This page has the options, the plan, and live prototypes of the loading and motion. Each prototype sits next to a recording of the real app doing the same thing today.

The short version
Prototypes loop. "Replay together" restarts the recording and the prototype in step. Counters are ms after the screen first reacts to the key.

1. Where the time goes today

Per screen, from the first frame that reacts to the key until the screen stops changing

Method: on-device screenrecord (it only emits frames on change), scene-change detection with ffmpeg, logcat (OkHttp and ActivityManager) and Conductarr's own request log for the same seconds. "Settled" is the last visible change. Raw clips and sheets are in data/tv-spike-motion-speed/.

ScreenFirst contentSettledWhat arrives lateRepeat visit
Cold start (am start -W TotalTime, 4 runs)583 / 538 / 438 / 453 ms to first framepicker at +0.3–0.4 s (DNS warm) or +1.65 s (DNS cold)Spinner while Users/Me then System/Info/Public run one after the other (ServerRepository.kt:104,107)same, bimodal on the 60 s DNS cache
Who's watching → Homehero text 135 ms430 msshelf tiles flat grey until 400 ms, then all images pop370 ms
Home → Discover tabcontent 270 ms (dims first)700 msposters pop in over 0.4 s600 ms
Movies grid230 ms (blank tiles)750 msposters fill row by row500 ms
Shows grid280 ms640 msposters—
Title, movie in library (American Gangster)430 ms (text)2.69 sbackdrop 470 ms; cast portraits 2.7 s (initials until then)450 ms total
Title, Discover pick (Donnie Brasco)980 ms (blank skeleton until then)1.53 sbackdrop 1.37 s, cast 1.5 s—
Title, series (Adults)620 ms3.42 sratings and "Ready for you · by Mindel" land at 3.4 s and push the overview down—
Person page (Martin Sheen)620 ms720 msfilmography posters—
Back from title / series / person260 / 300 / 450 ms
Settings220 ms220 ms—
Coming in sheet74 ms74 ms (from cache)posters refresh silently

Frame timing per interaction (dumpsys gfxinfo, ART status speed)

InteractionFramesJankyp50p90p99Main thread slow / GPU slow
Home rows scroll (down 7, up 6, right 5), run 1 / 2149 / 11538.9 % / 34.8 %44 / 46 ms69 / 6589 / 10143 / 52 · 27 / 35
Movies grid scroll (down 8, up 8), run 1 / 2180 / 15943.9 % / 32.1 %30 / 30 ms53 / 4693 / 7330 / 69 · 20 / 48
Discover scroll14630.1 %34 ms577334 / 35
Tab switch ×415818.4 %28 ms6515015 / 29
Title open + back ×310333.0 %48 ms11315029 / 34
Who's watching → Home1553.3 %61 ms1171337 / 8
Open title (movie, first time)2060.0 %57 ms1502009 / 12
Back from person1275.0 %93 ms1501508 / 9

Frame phases on the Shows grid (framestats, 68 frames): record p50 0.5 / p90 10.3 ms, GPU issue p50 4.3 / p90 8.9 ms, GPU complete p50 14.3 / p90 21.2 ms, total p50 25.6 / p90 41.4 ms. The app renders at 1920×1080 and the box outputs 3840×2160 at 59.94 Hz. Today's grid is worse than the earlier Opt measurement (17.7 % janky), which was taken before the spacing and focus-ring builds.

Network, from tank (10 runs each; cold = new connection, warm = kept-alive)

CallBytesCold p50 / p95Warm p50 / p95Cache headers
Jellyfin UserViews (called 4–5× per launch)2.3 KB65 / 166 ms46 / 51 msnone
Home Resume (20)1.4 KB58 / 6630 / 41none
Home NextUp (20)6.2 KB71 / 9147 / 52none
Home Items/{{id}} per series (sequential)4.0 KB43 / 6024 / 28none
Home library shelf (20)18.7 KB85 / 11674 / 84none
Movies grid page (200)51 KB68 / 14861 / 64none
Title episodes with MediaSources + People96.8 KB142 / 185122 / 169none
Title episodes, lean fields6.5 KB26 / 6511 / 12none
Grid poster, no size (today, LibraryReadFacade.kt:86)626 KB26 / 308 / 9max-age 1 y, immutable, ETag
Grid poster fillWidth 256×384 q9631.6 KB19 / 1312 / 4same
Grid poster 256×384 q80 WebP11.2 KB19 / 1181 / 2same
Backdrop 1280 q96 / q80 WebP91 / 31 KB19 / 252 / 2same
TMDB poster w780 (today, ConductarrProjection.kt:9)131 KB128 / 14449 / 70max-age 1 y
TMDB poster w342 / w18541 / 15 KB100 / 7631 / 30max-age 1 y

Box-side DNS (ping timing, 6 samples 70 s apart): uncached jelly.justinvisser.org 490–1390 ms, wolfskin-tv 460–490 ms, image.tmdb.org 60–1170 ms; cached 40–50 ms. The box uses Private DNS dns.adguard.com. The LAN resolver 192.168.1.2 answers uncached names in 136–160 ms and cached in 0 ms. The A record behind both hosts has a 60 s TTL. Cold launches 1 and 3 were more than 60 s after the previous lookup, and 2 and 4 were within it.

Conductarr TV routes, server-side (Conductarr request log, 22 h, 200/304)

Routenp50p95Body p50Why
activity/titles/{{tmdb}}/{{type}} (Ready for you)452856 ms5002 ms1.7 KBRe-reads the held title twice, the Seerr requests and a Jellyfin GET per episode; no cache (activity/detail.go:21, request_evidence.go:45, users.go:262). The 5002 ms p95 is most likely the TV giving up: its read timeout is 5 s (ConductarrSession.kt:53), so on the slowest titles the line never appears
catalogue/titles/{{tmdb}}/{{type}} (detail)511198 ms3217 ms11.9 KBHeld-title read lists every Arr library unpaged, then Seerr details, then ratings; sequential (catalogue/detail.go:14-17, library_detail.go:81)
catalogue/tv/{{tmdb}}/seasons/{{n}}76 / 17 (304)1084 / 2282 ms2664 ms1 KBSame held-title read; a 304 still renders and hashes the full body first (read_validators.go)
…/ratings265147 ms449 ms0.5 KB6 h server cache, 300 titles
…/enrichment · …/related · …/cast46 · 42 · 26103 · 124 · 33 ms465 · 261 · 783 · 14 · 12.5 KBenrichment uncached
recommendations feed4216 ms40 ms204 KB, uncompressedno gzip/br on the TV door (Caddyfile.base:316-333, serve.go:140)
activity (polled every 30 s)713 × 30452 ms83 ms62.5 KB when 200
operator · profiles · dismissals · options · display12–25 ms30–93 mscheap; display is called ≈1 400×/day, once per poster

Every TV request also costs Conductarr an uncached Jellyfin /Users/Me to check the token (clients/jellyfin/token.go:42). Every authenticated response is Cache-Control: no-store (authority.go:32). The TV client revalidates with its own ETag store instead.

Caches the app has today

CacheWhereBehaviourObserved
Wolfskin image loader (Coil 3)WolfskinImages.kt:46-6164 MiB memory, 128 MiB disk, 4 parallel reads, crossfade offrepeat visits paint art from memory; first grid visit downloads 626 KB per poster (no size)
Upstream Wholphin image loaderCoilConfig.kt:80-101separate memory + 200 MB diska second copy of the same art in memory
Conductarr JSONFileCacheStore, noBackupFilesDir/conductarrsaved copy shown first, then revalidated with ETag; no memory layertitle re-open: every ON_OPEN route re-fetched (cast 304, detail 875 ms again)
Home, library, title, person snapshotsfilesDir/wolfskin-home, -library-cache, -titledisk JSON, painted firstrepeat visits 370–500 ms instead of 430–750
HTTP cache (OkHttp)none—Jellyfin JSON never cached; images rely on Coil only
DNSsystem only60 s TTL through Private DNS1.5 s launch spinner when expired

2. Load states

Today: a spinner on launch, flat grey blocks on Home, the grids and title pages, and a blank title page until Conductarr answers on Discover picks. Images snap in because crossfade is off in both loaders. Nothing shows a "loading" sentence; that rule already holds.

OptionExpected gain (measured basis)EffortRiskWhat you would seeShips in
Picker from the saved people list, verify in the backgroundRemoves the 1.5 s spinner on every DNS-cold launch (2 of 4 measured)SLow. A revoked session must still land on the recovery pickerThe launcher fades straight into Who's watchingTV
Shaped skeletons that breathe (opacity only, staggered)No speed change; replaces the flat grey and spinner statesSLow. One alpha layer per rowCard-shaped tiles gently pulsing until the art landsTV
Art fades in (120–160 ms) instead of poppingRemoves the pop on every first visitXSLow. Keep it off for memory-cache hits so repeat visits stay instantPosters settle instead of flickering inTV
Reserve space for late fields (ratings, Ready for you, genres, overview)No layout jump at 3.4 s on series pagesSLowThe line fades into a gap that was already thereTV
Title page paints what the card already knewFirst content 430–980 ms → about 60 msMMedium. Card data must match detail (year, runtime, ratings)Title, year, ratings and Play at once, over a colour wash of the posterTV
Shimmer sweep skeletonsNoneSHigh on this GPU: a moving gradient over every card, every frameLight sweeps across the tiles. Not recommendedTV

See it: launch

Today, DNS cold (2 of 4 launches): 1.5 s spinner

Today, DNS warm

After: no spinner, people from the saved list

See it: skeleton styles

Pick a style. Content lands at 900 ms in each, about the Movies grid's first visit.

Today: Movies grid, first visit

Options

3. Motion and transitions

Today's motion tokens: focus scale 1.08 over 140 ms, ring and caption fades of 140 ms, a nav blob spring, and 160–180 ms row scrolls. There are no screen transitions or hero cross-fades, and reduced motion is defined but nothing reads it (WolfskinTokens.kt:218). At 30–46 ms per frame, a 140 ms tween is drawn in 3–4 frames. Tick box frame rate at the top to see that.

OptionExpected gain (measured basis)EffortRiskWhat you would seeShips in
Staged reveal on screen entry (hero copy rises 16 px, rows rise 24 px, staggered 30–40 ms, 180 ms ease-out)Perceived: art arrives into motion instead of popping. Cost: translate + alpha on one layer per rowSLow. Stagger must not delay focusHome and Discover settle like a curtain rather than tiles snappingTV
Hero cross-fade (backdrop 320 ms, copy hands off)Removes the hard cut when the hero changesSMedium. Two 1080p bitmaps for 320 ms; the GPU is already at 14–21 ms/frame, so fade the backdrop only and keep the scrim staticThe next pick dissolves inTV
Focus: 180 ms ease-out, caption rises 10 pxSame frame cost as today; the curve hides dropped frames betterXSLowFocus glides rather than ticksTV
Title entry: grid fades 160 ms, page builds from the poster colourHides the 470 ms backdrop fetchMMedium (see load states)No black gap between grid and titleTV
Shared-element poster → title sheetNone measurableLHigh. Compose shared elements across NavHost destinations on this GPU; the movie page has no poster sheet when a backdrop existsThe card flies into place. Not recommended nowTV
Honour reduced motion (system animator scale 0 or a Wolfskin toggle)AccessibilitySLowFades only, ≤100 ms; the toggle at the top shows itTV

See it: Who's watching → Home

Today: grey tiles, then art pops at 400 ms

After

See it: tab switch and row reveal (Home → Discover)

Today

After

See it: title page entry

Today: American Gangster, first open (cast at 2.7 s)

Today: Donnie Brasco from Discover, blank until 980 ms

After: Apocalypse Now from the same grid

See it: a late line on a series page

Today: Adults, the readiness line lands at 3.4 s and pushes the text down

After

See it: focus move

Tick box frame rate to compare at 25 fps.

Today, real grid

Focus along a row

See it: hero cross-fade

Today (Discover hero)

Hero advances

4. Caching

OptionExpected gain (measured basis)EffortRiskWhat you would seeShips in
Persisted DNS answers (OkHttp Dns: serve the last address at once, refresh behind)Removes 0.45–1.35 s on launch and on the first TMDB image after any 60 s pauseSLow. Fall back to the system lookup if the cached address failsNo spinner on launch; first cast photos in ≈0.6 s instead of 2.7 s [inference: DNS is the cast delay]TV
Warm the connections at process start (Jellyfin, Conductarr door, TMDB) in parallelOverlaps DNS + TLS with the 450 ms first frameXSLowSame as above, smallerTV
Memory layer over the Conductarr disk cacheSkips disk read and JSON decode on back and re-open (title re-open settles at 450 ms today; ~250 expected) [inference]SLowBack and re-open feel instantTV
Stale-while-revalidate for ON_OPEN title routes (skip refetch if validated in the last 5 min)Re-opening a title stops costing the server 875–2 900 msSLow. Requests and readiness may be 5 min old; keep requests on FORCENothing visible; less churnTV
Prefetch the focused card's title page (detail JSON + backdrop) after 350 ms of dwellTitle first content 430 ms → about 60 ms; backdrop already decodedMMedium. Bandwidth: at most one title per dwell; cancel on moveOpening a title is instantTV
Pre-decode last-known Home art while Who's watching is upHome settles at 430 ms → about 250 ms with no popsSLow. Memory: 6 backdrops ≈ 21 MiB of the 64 MiBHome appears wholeTV
One image loader instead of twoFrees a second memory cache (Coil default ≈ 25 % of the heap) [inference]MMedium. Upstream screens use their ownFewer re-downloads after big screensTV
OkHttp HTTP cache for Jellyfin JSONSmall. Jellyfin sends no cache headers, so only conditional revalidation is possibleSLowNothing visibleTV

5. Backend retrieval

OptionExpected gain (measured basis)EffortRiskWhat you would seeShips in
Cache the held-title read in Conductarr (Arr library list, queue, history) for 30–60 s, shared across routesTitle detail 1.2 s → ≈0.1 s, season 1.1 s → ≈0.1 s, readiness 2.9 s → <1 s [inference from the code path]MMedium. Freshness of "Ready" after an import; invalidate on Arr webhook (already received)Series pages settle in under a second; no late lineConductarr release
Readiness: drop the duplicate held-title read and the per-episode Jellyfin GETsThe 5 s tail on readinessMMedium"Ready for you" appears with the pageConductarr release
Compress the TV door (Caddy encode zstd gzip)Feed 204 KB → ~25–35 KB [inference: JSON typically 6–8×], activity 62 KB → ~8 KBXSLowDiscover opens a little faster on Wi-FiCaddy change (server-docs)
304 before rendering (keep a per-route version, not a hash of the full body)Season 304 2.3 s → msMMediumNothing visible except speedConductarr release
Cache the token check (/Users/Me) for 60 s per tokenRemoves a Jellyfin round trip from every TV request (~10–40 ms each, 6–10 per title page)SLow. Revocation lag 60 sNothing visibleConductarr release
Batch display reads (one call for a row of posters)≈1 400 display calls/day → a few dozenMLowComing in and Discover rows fill togetherConductarr release + TV
Size the grid posters (fillWidth=256&fillHeight=384&quality=80&format=Webp)626 KB → 11 KB per poster (57×); 85 films 53 MB → 1 MBXSLowMovies and Shows fill at once on first visitTV
Ask TMDB for w342 posters, not w780131 KB → 41 KB per Discover posterXSLow. 342 px for a 256 px cardDiscover rows fill soonerTV
Lean episode fields (drop MediaSources/MediaStreams/People from the list; fetch them on Play)96.8 KB → 6.5 KB per season, 142 → 26 msSMedium. The episode row shows file chipsSeries pages settle soonerTV
Fewer launch calls: UserViews 4–5× and LiveTv/Recordings/Folders 2× per launch≈250 ms of serial callsSLowNothing visibleTV

6. Kotlin and Compose

OptionExpected gain (measured basis)EffortRiskWhat you would seeShips in
Library load re-emits the full list per 200-item page and re-sorts each time (LibraryReadFacade.kt:96, LibraryGridState.kt:99)Grid first visit; matters more as the library growsSLowGrid appears in one stepTV
Hoist the Regex in titleFallbackBackground / titleAmbientBackground (TitleArtwork.kt:152,167)Draw-time cost per cardXSLow—TV
Discover reads firstVisibleItemIndex in composition (DiscoverScreen.kt:107) → derivedStateOfFewer recompositions while scrolling Discover (34 of 44 janky frames were UI-thread)XSLowSmoother Discover scrollTV
NavBar label colour read in composition (NavBar.kt:86) → draw-phaseTab switch framesXSLow—TV
WolfskinImages.loader() is @Synchronized and called in compositionLock on every poster recompositionXSLow—TV
Cut full-screen layers: hero blur (RenderEffect) + two full-screen gradient scrims + ambientGPU 14–21 ms/frame is the floor today; pre-blurred tiny art and one combined scrim drawn onceMMedium. Must match the approved framesSmoother scroll on Home and DiscoverTV
Keys and contentTypeAlready done everywhere———

7. Baseline profile

Today: a hand seed of 6 lines (app/src/optimized/baselineProfiles/baseline-prof.txt), no generator and no Macrobenchmark. The box is at speed only because it was forced with cmd package compile. Every new install starts at verify (interpreted/JIT) until background dexopt runs.

State (same box, earlier tv-perf run)Cold startHome scroll janky / p50 / p90Movies grid
Opt just installed (verify)0.5–1.0 s51.4 % / 65 / 117 ms—
Opt after forced AOT (speed)0.39–0.44 s29.2 % / 38 / 61 ms17.7 % / 19 / 36 ms

Expected from a recorded profile (Baseline Profile Gradle plugin plus one UiAutomator journey through picker, Home, Discover, the grids, a title and a person page): a fresh install behaves like the AOT row from the first launch. That is roughly −40 % janky frames on Home scroll and up to half a second off cold start, for every build the second mate installs. It does not change steady state on a box already at speed. Effort M: the journey can run on an emulator in the build container; the box itself isn't needed. Risk low.

8. Recommended plan

  1. DNS and launch (TV, S, ~1 day). Persisted DNS answers plus parallel connection warm-up at process start. Show the picker from saved people and check the session in the background. Prove it: 6 cold launches spaced >60 s apart, picker at ≤0.5 s every time (today 1.65 s in half); first cast photos on a new title ≤1 s.
  2. Image sizes (TV, XS, hours). Sized WebP grid posters, TMDB w342, lean episode fields. Prove it: first Movies visit settles ≤400 ms (today 750); tank payload table.
  3. Load states and motion (TV, M, 2–3 days, against your pick on this page). Breathing shaped skeletons, art fade-in for network loads only, reserved late-line slots, staged reveal, hero cross-fade, reduced motion. Prove it: recordings like the ones above, no layout change after first content, gfxinfo janky no worse than today.
  4. Prefetch and memory cache (TV, M, 2 days). Dwell prefetch of title pages, a memory layer over the Conductarr cache, pre-decoded Home art behind the picker. Prove it: title first content ≤100 ms and Home settled ≤250 ms.
  5. Conductarr release (M–L, 2–4 days): held-title cache with webhook invalidation, readiness without duplicate reads or per-episode GETs, 304 before render, token-check cache. Caddy: compress the TV door. Prove it: the route table above for 24 h, with detail, season and readiness p50 ≤200 ms and p95 ≤600 ms, and the feed on the wire ≤40 KB.
  6. Baseline profile (TV build, M, 1–2 days). Recorded journey profile. Prove it: fresh install without a forced compile, Home scroll janky ≤30 %, cold start ≤0.5 s.
  7. Frame cost (TV, M, 2–3 days). The Compose fixes in section 6 and fewer full-screen layers. Prove it: Home and grid scroll janky ≤20 %, GPU p90 ≤16 ms in framestats.

Total: about 2 weeks of TV work and 3–4 days for a Conductarr release. Steps 1–2 alone remove the two waits you can feel today (the launch spinner and grids filling slowly), in about a day and a half.

A box-side alternative to step 1's DNS part: switch the box's Private DNS from dns.adguard.com to Automatic, so it uses the LAN resolver (136–160 ms uncached, 0 ms cached). That changes the whole box, not just Wolfskin, so it's your call; the TV-side fix works either way.

9. Your picks (all of them on one page)

Six decisions. Each locks the moment you send it and the build lanes start on it. Nothing here touches the TV while you are playing.

1. Load-state and motion direction

Decides plan step 3 (skeletons and motion). The prototypes above show each part.

2. The two quick speed fixes (about a day and a half, TV only)

(a) DNS cache with stale-serve plus a picker painted from saved people, so the launch spinner goes away; (b) sized WebP grid posters, smaller TMDB art, lean episode fields, so grids fill fast. Both ship in the next TV build.

3. Change the box Private DNS (your call, system setting)

Today the box uses dns.adguard.com: an uncached lookup costs 450 to 1350 ms. Automatic uses the LAN resolver (136 to 160 ms uncached, 0 ms cached). It changes DNS for the whole box, not just Wolfskin. I do it with ADB only when the TV is free and you say so; the TV-side fix in 2(a) works either way.

4. Later plan steps to queue (TV, after the build above)

Prefetch and memory cache (title pages, 2 days); recorded Baseline Profile (1 to 2 days); frame cost and Compose fixes (2 to 3 days). Each has its own proof target in section 8.

5. Conductarr title-read speed-up (needs a Conductarr release)

Held-title cache with Arr webhook invalidation, readiness without duplicate reads or per-episode calls, 304 before render, token-check cache, compression on the TV door. 3 to 4 days, plus a release and a server-docs update. Title pages are slow because of this (readiness p50 2.9 s).

6. When should these land on the TV?

You said you will not be using the TV during this session.

Method, raw numbers and files: data/tv-spike-motion-speed/report.md. Nothing was changed in the app or on the server. The box was left on the Movies tab.