The Discover tab fanned out its whole request set twice on every cold start and replayed slow rows on a shrinking timeout ladder, so a healthy remote server produced anywhere from 4s to 15s of loading. Measured against a remote Jellyfin server with four libraries, 24 interleaved cold-start samples per side: requests 19 -> 9 payload 219 KB -> 94 KB settled 5231ms -> 2502ms median, 13222ms -> 5927ms p95 Four independent causes: - Retry policy. `Client.send` resolves on response headers, so the connect budget covers the server's think time and a slow-but-alive query raises `connectionTimeout`. Replaying it made the server re-run the query with a shorter budget than the one it just missed; the `[10s, 8s, 5s]` ladder turned an 11s answer into an empty row after 23s. Hub surfaces now get one whole-request deadline, retry only immediate connection errors, and the deadline bounds the whole call including the request still in flight. - Request shape. `/Items/Latest` groups a TV library by series, so its rows are Series folder dtos and `RecursiveItemCount`/`ChildCount` cost a DB count each, per row. Hub rows now ask for `Overview` only; watch state survives because Jellyfin derives `UserData.Played` from `UnplayedItemCount` when the count fields are absent. `/Shows/NextUp` sends `NextUpDateCutoff` to bound the server's series-key scan, and `Thumb` leaves `EnableImageTypes` since nothing reads it. `UserData` and `PremiereDate` leave the browse set: neither is an `ItemFields` member, so the server dropped them anyway. - Fan-out. Per-library hubs ran in batches of three separated by a barrier, so one slow library stalled every library behind it. A sliding window keeps the same peak concurrency without head-of-line blocking. Concurrent `fetchLibraries` calls now share one `/Views` instead of racing two identical round trips, Plex's global and music hub legs start together, and Jellyfin gets Plex's pool tuning. - Duplicate pass. `DiscoverScreen.initState` starts a load and the online-entry hook asked for a full refresh on top of it, which `CoalescedLoadCoordinator` correctly queued as a trailing pass. The hook now calls `primeRefresh`, which rides along with a load already in flight; profile switches still go through `fullRefresh`. Refs #1784
79 lines
4.0 KiB
Dart
79 lines
4.0 KiB
Dart
/// Centralized HTTP timeout constants for both backends. The same
|
|
/// [MediaServerHttpClient] wrapper is used by Plex and Jellyfin clients —
|
|
/// timeouts are kept here so the budgets per phase are visible at a
|
|
/// glance.
|
|
class MediaServerTimeouts {
|
|
static const connect = Duration(seconds: 10);
|
|
|
|
static const receive = Duration(seconds: 120);
|
|
|
|
/// Whole-request deadline for home `/hubs` startup calls. These endpoints can
|
|
/// be slow while Plex wakes idle disks or a CDN-fronted Jellyfin runs a cold
|
|
/// query, but should not block forever.
|
|
///
|
|
/// Deliberately a *single* budget rather than a retry ladder. `Client.send`
|
|
/// resolves when response headers arrive, so this budget covers the server's
|
|
/// think time, not just the socket connect — a slow-but-alive query trips it.
|
|
/// Replaying that request makes the server re-run the same expensive query
|
|
/// from scratch, so the old `[10s, 5s, 2.5s]` ladder turned an 11s answer
|
|
/// into a 17.5s empty row (#1784). See [retryTransientMediaServerCall].
|
|
static const homeHubDeadline = Duration(seconds: 15);
|
|
|
|
/// Whole-request deadline for per-library home hub rows
|
|
/// (`/hubs/sections/{id}`, Jellyfin `/Items/Latest`). These can be slower
|
|
/// than the top-level home hub call on remote servers. Same single-budget
|
|
/// rationale as [homeHubDeadline] — it replaced `[10s, 8s, 5s]`, whose 23s
|
|
/// worst case was the dominant cold-start stall in #1784.
|
|
static const libraryHubDeadline = Duration(seconds: 20);
|
|
|
|
/// Timeout for probing a cached/preferred endpoint (used in
|
|
/// [PlexServer.findBestWorkingConnection]).
|
|
static const preferredEndpointProbe = Duration(milliseconds: 1500);
|
|
|
|
/// How long the cached/preferred endpoint probe gets to answer before the
|
|
/// full candidate race starts alongside it. A healthy cached endpoint
|
|
/// answers well inside this window and wins deterministically; a stale one
|
|
/// (e.g. a cached LAN address probed from outside the LAN) only delays
|
|
/// discovery by this much instead of the full [preferredEndpointProbe].
|
|
static const preferredEndpointHeadStart = Duration(milliseconds: 300);
|
|
|
|
/// Timeout for the connection race where all candidates are tested in
|
|
/// parallel (used in [PlexServer.findBestWorkingConnection]).
|
|
static const connectionRace = Duration(seconds: 2);
|
|
|
|
/// Per-server connection watchdog ceiling. The discovery path is no longer
|
|
/// strictly serial (cached probe overlaps the race; the HTTPS upgrade runs
|
|
/// off the critical path), so this is a generous upper bound rather than a
|
|
/// sum of phases.
|
|
static const perServerConnect = Duration(milliseconds: 6500);
|
|
|
|
/// HTTP timeout for the live-TV tune POST. Matches Plex web's value — the
|
|
/// default 10s connect budget is too tight on Fire-TV cold starts.
|
|
static const tune = Duration(seconds: 30);
|
|
|
|
static const plexTvConnect = Duration(seconds: 15);
|
|
|
|
static const plexTvReceive = Duration(seconds: 10);
|
|
|
|
/// Authenticated health probe timeout. Health sweeps await every server, so
|
|
/// a stale Plex endpoint must not hold the whole sweep for [receive].
|
|
static const plexProbe = Duration(seconds: 8);
|
|
|
|
/// Probe + token-validate timeout — Jellyfin servers respond fast on
|
|
/// `/System/Info/Public` and `/Users/Me`.
|
|
static const jellyfinProbe = Duration(seconds: 8);
|
|
|
|
/// Per-item delete-permission probe. Shorter than [jellyfinProbe] because it
|
|
/// blocks a context menu from opening: a server that is nominally online but
|
|
/// hung must not hold the menu for a health-sweep budget. Unlike the other
|
|
/// values here it is also applied as a whole-request deadline by the caller
|
|
/// (the per-request budget covers the connect and receive phases
|
|
/// individually), and expiry fails closed — no delete entry — so the ceiling
|
|
/// only ever costs an entry, never safety.
|
|
static const jellyfinDeletePermission = Duration(seconds: 3);
|
|
|
|
/// Best-effort `/Sessions/Logout` timeout — short because the call is
|
|
/// fire-and-forget; the token is removed locally regardless.
|
|
static const jellyfinSignOut = Duration(seconds: 5);
|
|
}
|