perf(series): list a series' episodes with one concurrent season walk
"More info" on Frasier took ~10 s. The series page asked Rust for the episodes and for the current episode as two commands; each walked every season, and each walk fetched the eleven seasons one after another. So the wait was the sum of twenty-two listings, each a cache read queued behind whatever the database was writing — measured at ~4 s per walk on a Fairphone 5 while the launch-time catalog sync ran. Seasons are now fetched together (gather_season_episodes), so a walk waits for its slowest season, not the sum. And repository_get_series_view returns the episodes and the current episode from one walk, with Next Up and resume fetched alongside it; the series page makes that one call. Under today's single database connection the cache reads themselves still queue on its mutex; the concurrency pays off fully once reads get their own connections. Halving the walks helps regardless. Test first: ten 100 ms seasons took 1.01 s sequentially; now well under the 400 ms bound, with a failing season still leaving the rest. DR-295, UT-264.
This commit is contained in:
@@ -1590,6 +1590,16 @@ async repositoryGetSeriesEpisodes(handle: string, seriesId: string) : Promise<Me
|
||||
async repositoryGetSeriesCurrentEpisode(handle: string, seriesId: string) : Promise<MediaItem | null> {
|
||||
return await TAURI_INVOKE("repository_get_series_current_episode", { handle, seriesId });
|
||||
},
|
||||
/**
|
||||
* A series' episodes and the viewer's current episode, from one season
|
||||
* fan-out. The series page used to ask for these as two commands, each of
|
||||
* which walked every season.
|
||||
*
|
||||
* TRACES: UR-062 | DR-101, DR-295
|
||||
*/
|
||||
async repositoryGetSeriesView(handle: string, seriesId: string) : Promise<SeriesView> {
|
||||
return await TAURI_INVOKE("repository_get_series_view", { handle, seriesId });
|
||||
},
|
||||
/**
|
||||
* Erase the viewer's watch history for an item.
|
||||
*
|
||||
@@ -3445,6 +3455,16 @@ export type SecurityStatus = { usingKeyring: boolean; storageType: string }
|
||||
* Audio track preference for a series
|
||||
*/
|
||||
export type SeriesAudioPreference = { seriesId: string; audioTrackDisplayTitle: string | null; audioTrackLanguage: string | null; audioTrackIndex: number | null }
|
||||
/**
|
||||
* A series' episodes and the one the viewer is up to, from **one** season
|
||||
* fan-out.
|
||||
*
|
||||
* The series page needs both, and asked for them as two commands; each walked
|
||||
* every season, so every visit listed the show twice. One call, one walk.
|
||||
*
|
||||
* TRACES: UR-062 | DR-101, DR-295
|
||||
*/
|
||||
export type SeriesView = { episodes: MediaItem[]; current: MediaItem | null }
|
||||
/**
|
||||
* The verdict on a server's version.
|
||||
*
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
// NO direct HTTP calls - everything routes through Rust backend
|
||||
|
||||
import { commands } from "./bindings";
|
||||
import type { DownloadDiskUsage, JRayActor, SearchScope, StreamSelection } from "./bindings";
|
||||
import type {
|
||||
DownloadDiskUsage,
|
||||
JRayActor,
|
||||
SearchScope,
|
||||
SeriesView,
|
||||
StreamSelection,
|
||||
} from "./bindings";
|
||||
import type { QualityPreset } from "./quality-presets";
|
||||
import type {
|
||||
Library,
|
||||
@@ -164,6 +170,17 @@ export class RepositoryClient {
|
||||
return commands.repositoryGetSeriesCurrentEpisode(this.ensureHandle(), seriesId);
|
||||
}
|
||||
|
||||
/**
|
||||
* A series' episodes and the episode the viewer is up to, from one season
|
||||
* fan-out in Rust. Prefer this over calling `getSeriesEpisodes` and
|
||||
* `getSeriesCurrentEpisode` together — each of those walks every season.
|
||||
*
|
||||
* TRACES: UR-062 | DR-101, DR-295
|
||||
*/
|
||||
async getSeriesView(seriesId: string): Promise<SeriesView> {
|
||||
return commands.repositoryGetSeriesView(this.ensureHandle(), seriesId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erase watch history for an item. On a series or season the server applies
|
||||
* it to everything inside, so the container returns to "never watched".
|
||||
|
||||
@@ -211,14 +211,9 @@
|
||||
const repo = auth.getRepository();
|
||||
const seasons = $libraryItems.filter((i) => i.kind === "season");
|
||||
|
||||
const [episodes, current] = await Promise.all([
|
||||
repo.getSeriesEpisodes(itemId),
|
||||
// Best-effort: a series still renders if the anchor cannot be resolved.
|
||||
repo.getSeriesCurrentEpisode(itemId).catch((e) => {
|
||||
log.warn("Could not resolve the current episode:", e);
|
||||
return null;
|
||||
}),
|
||||
]);
|
||||
// One call, one season fan-out: asking for episodes and the current
|
||||
// episode separately walked every season twice (DR-295).
|
||||
const { episodes, current } = await repo.getSeriesView(itemId);
|
||||
|
||||
seasonData = groupEpisodesBySeason(seasons, episodes);
|
||||
currentEpisode = current;
|
||||
|
||||
Reference in New Issue
Block a user