Files
plezy/lib/profiles
edde746 c4059d0ead fix(startup): stop Cronet and Plex Home from blocking time-to-interactive
Two cold-start findings from the same pass. They share a call site in
`MainScreen`'s post-frame block, so they land together.

## Cronet was 33% of time-to-interactive

`createPlatformClient()` built the shared `CronetEngine` inline, so whichever
consumer happened to create the first HTTP client paid for it — and that landed
between `database_ready` and `credentials_loaded`, i.e. squarely on the path to
the first usable screen.

Measured on the Amlogic SC2 box, phase marks relative to `dart_main`, by
temporarily forcing the existing `_cronetBroken` fallback so no engine is ever
built:

| phase | engine built inline | engine never built |
|---|---|---|
| database_ready | +455 | +456 |
| credentials_loaded | +1171 | +703 |
| binding_settled | +1331 | +827 |
| main_screen | +1394 | +932 |

So ~462 ms, fully serial. Logcat shows where it goes: `DynamiteModule
loadModule2NoCrashUtils` then `HttpFlagsLoader` reading
`com.google.android.gms/app_httpflags/flags.binarypb`. The cause is provider
*enumeration*, not selection — `CronetEngine.Builder(Context)` calls
`isEnabled()` on every registered provider, and `PlayServicesCronetProvider`
answers that by installing the Play services Dynamite module. `play-services-cronet`
arrives transitively through `media3-datasource-cronet`, and `package:cronet_http`
offers no way to choose a provider, so the only lever available in Dart is *when*
the cost is paid.

Android's `createPlatformClient()` now returns a client that resolves its
delegate per request: the tuned IOClient that already backstops a broken Cronet
until the shared engine exists, Cronet afterwards. Per-request matters — a
`MediaServerHttpClient` builds its client in a constructor initializer and lives
for the process, so deciding once at construction would have pinned primary
media-server traffic to HTTP/1.1 forever, which is a silent steady-state
regression rather than a fix. `warmUpPlatformHttpClient()` then builds the engine
from `MainScreen`'s post-frame block.

Result: `main_screen` +1394 -> +915 ms, and logcat carries both client lines
(`IOClient (Android fallback)` then `CronetClient`), proving the swap. The build
now runs from +1023 to +1419, entirely after the first screen, and produces no
Choreographer or Davey report — the UI is static waiting on hub content there, so
there are no frames to drop.

## Plex Home refresh raced the offline decision

`PlexHomeService.start()` conflated disk hydration with going live: it decoded
the cached `plex_home_users_*` entries *and* subscribed to connection changes,
installed a refresh timer and fired `_refreshAll()`. It was invoked straight from
a provider `create:`, so on a box with no network — or the flaky 2.4 GHz Wi-Fi
these devices typically have — it started requests that would time out during the
exact window the startup gate needs. Its immediate neighbour
`ActiveProfileBinder` is explicitly not auto-started for this reason and says so
in a comment; the same argument applied here and had simply not been followed.

`start()` is now the live/network entry point and `hydrate()` is the disk-only
half, coalesced and lifecycle-guarded like `start()` already was. The provider
`create:` hydrates; `_reloadSnapshot` and `reloadFromStorage` hydrate; the borrow
picker hydrates, because it reads `current` immediately and is reachable while
offline. Only `MainScreen` goes live, gated on `!_isOffline`, with
`_handleOfflineStatusChanged` picking it up if the session later regains network —
otherwise an airplane-mode launch would never refresh Plex Home again.

Hydration still `_emit()`s, so `stream`'s replay contract holds even when the
network side never starts, which is what keeps a late listener behind a
`combineLatest` off a permanent spinner.
2026-08-23 18:13:39 +02:00
..