mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 02:46:16 -08:00
perf(dashboard): per-rail skeletons + drop CAST that blocked content index
Two independent fixes that compound: the dashboard now renders progressively as each rail's data resolves, and the slowest of those rails (Xtream recently-added) no longer scans the entire content table. Per-rail skeletons (template restructure): The old @if (!ready()) gate hid the whole dashboard until ALL FOUR loading flags resolved (playlistsLoaded, globalRecentLoaded, globalFavoritesLoaded, xtreamRecentlyAddedLoaded). The slowest one pinned the entire skeleton up for the full tail latency — visibly seconds of "loading" even when 3 of 4 rails could have rendered immediately. Replace with per-rail conditionals: - Hero: renders the moment globalRecentItems[0] is available; skeleton shows only while data.globalRecentLoading() is true and no item exists. - Each rail: shows real content if its cards are non-empty, its own skeleton if its dedicated loading flag is true, nothing otherwise. - The Xtream recently-added rail's skeleton is gated on having Xtream playlists at all, so M3U-only users never see a skeleton for it. The loading signals were already exposed on DashboardDataService (globalRecentLoading, globalFavoritesLoading, xtreamRecentlyAddedLoading) but went unused because of the monolithic gate. Same skeleton markup is reused per rail; no styling changes. Drop CAST(added AS INTEGER) in getGlobalRecentlyAdded: The query ordered by sql<number>\`CAST(content.added AS INTEGER)\`. SQLite cannot use an index on a column wrapped in a function, so the existing idx_content_type_added index was bypassed and the planner did a full table scan + sort on content (10k–100k+ rows for a typical Xtream catalog) on every dashboard load. Sort by schema.content.added directly. Xtream stores Unix-epoch timestamps as 10-digit numeric strings (anything since 2001-09-09), so lexicographic and numeric sort are equivalent. The (type, added) index now drives the ORDER BY too — full table scan becomes an index range scan + LIMIT 20. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 1e3392724bc8
This commit is contained in:
1 parent
31bddbd70f
commit
d67150d38f
2 files changed
+86
-36
No files matched your search
@@ -124,7 +124,6 @@ export async function getGlobalRecentlyAdded(
|
||||
const normalizedLimit = Number.isFinite(limit)
|
||||
? Math.min(Math.max(Math.trunc(limit), 1), 200)
|
||||
: 200;
|
||||
const addedOrder = sql<number>`CAST(${schema.content.added} AS INTEGER)`;
|
||||
|
||||
const whereConditions = [
|
||||
inArray(schema.content.type, contentTypes),
|
||||
@@ -136,6 +135,12 @@ export async function getGlobalRecentlyAdded(
|
||||
whereConditions.push(eq(schema.playlists.type, playlistType));
|
||||
}
|
||||
|
||||
// Sort by `added` directly. Xtream stores Unix-epoch timestamps as
|
||||
// 10-digit numeric strings (since 2001-09-09), so lexicographic sort
|
||||
// is equivalent to numeric sort. Wrapping the column in CAST(... AS
|
||||
// INTEGER) — as we used to — blocks SQLite from using
|
||||
// idx_content_type_added and forces a full table scan + sort on the
|
||||
// entire content table (often 100k+ rows) on every dashboard load.
|
||||
return db
|
||||
.select({
|
||||
...selectContentFields(),
|
||||
@@ -153,7 +158,7 @@ export async function getGlobalRecentlyAdded(
|
||||
eq(schema.categories.playlistId, schema.playlists.id)
|
||||
)
|
||||
.where(and(...whereConditions))
|
||||
.orderBy(desc(addedOrder))
|
||||
.orderBy(desc(schema.content.added))
|
||||
.limit(normalizedLimit);
|
||||
}
|
||||
|
||||
|
||||
+79
-34
@@ -3,46 +3,17 @@
|
||||
[class.rails-page--empty]="ready() && !hasPlaylists()"
|
||||
data-test-id="dashboard-page"
|
||||
>
|
||||
@if (!ready()) {
|
||||
<!-- Precise ghosts that mirror the real hero + rails so the layout
|
||||
doesn't reflow when data arrives. -->
|
||||
<section
|
||||
class="rails-page__skeleton-hero"
|
||||
aria-hidden="true"
|
||||
aria-busy="true"
|
||||
>
|
||||
<div class="rails-page__skeleton-hero-poster"></div>
|
||||
<div class="rails-page__skeleton-hero-body">
|
||||
<div class="rails-page__skeleton-hero-title"></div>
|
||||
<div
|
||||
class="rails-page__skeleton-hero-title rails-page__skeleton-hero-title--short"
|
||||
></div>
|
||||
<div class="rails-page__skeleton-hero-subtitle"></div>
|
||||
<div class="rails-page__skeleton-hero-cta"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@for (_ of skeletonRails; track $index) {
|
||||
<section
|
||||
class="rails-page__skeleton-rail"
|
||||
aria-hidden="true"
|
||||
aria-busy="true"
|
||||
>
|
||||
<div class="rails-page__skeleton-header"></div>
|
||||
<div class="rails-page__skeleton-track">
|
||||
@for (slot of skeletonSlots; track slot) {
|
||||
<div class="rails-page__skeleton-card"></div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
} @else if (!hasPlaylists()) {
|
||||
@if (data.playlistsLoaded() && !hasPlaylists()) {
|
||||
<app-empty-state
|
||||
[type]="'welcome-dashboard'"
|
||||
[showElectronOnlyValueProps]="isElectron"
|
||||
(addPlaylistClicked)="onAddPlaylist($event)"
|
||||
/>
|
||||
} @else {
|
||||
<!-- Hero: real card if a recent item resolved, skeleton while still
|
||||
loading the first global recent item, nothing if no recent items
|
||||
exist. Each block below renders as soon as ITS data is ready —
|
||||
no longer pinned on the slowest rail's I/O. -->
|
||||
@if (hero(); as item) {
|
||||
<a
|
||||
class="hero"
|
||||
@@ -113,8 +84,25 @@
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
} @else if (data.globalRecentLoading()) {
|
||||
<section
|
||||
class="rails-page__skeleton-hero"
|
||||
aria-hidden="true"
|
||||
aria-busy="true"
|
||||
>
|
||||
<div class="rails-page__skeleton-hero-poster"></div>
|
||||
<div class="rails-page__skeleton-hero-body">
|
||||
<div class="rails-page__skeleton-hero-title"></div>
|
||||
<div
|
||||
class="rails-page__skeleton-hero-title rails-page__skeleton-hero-title--short"
|
||||
></div>
|
||||
<div class="rails-page__skeleton-hero-subtitle"></div>
|
||||
<div class="rails-page__skeleton-hero-cta"></div>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
<!-- Recently watched rail -->
|
||||
@if (recentlyWatchedCards().length > 0) {
|
||||
<lib-dashboard-rail
|
||||
[label]="'WORKSPACE.DASHBOARD.RECENTLY_WATCHED' | translate"
|
||||
@@ -123,8 +111,22 @@
|
||||
[seeAllLink]="['/workspace/global-recent']"
|
||||
[testId]="'dashboard-recently-watched-rail'"
|
||||
/>
|
||||
} @else if (data.globalRecentLoading()) {
|
||||
<section
|
||||
class="rails-page__skeleton-rail"
|
||||
aria-hidden="true"
|
||||
aria-busy="true"
|
||||
>
|
||||
<div class="rails-page__skeleton-header"></div>
|
||||
<div class="rails-page__skeleton-track">
|
||||
@for (slot of skeletonSlots; track slot) {
|
||||
<div class="rails-page__skeleton-card"></div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
<!-- Favorites rail -->
|
||||
@if (favoriteCards().length > 0) {
|
||||
<lib-dashboard-rail
|
||||
[label]="'WORKSPACE.DASHBOARD.GLOBAL_FAVORITES' | translate"
|
||||
@@ -133,8 +135,22 @@
|
||||
[seeAllLink]="['/workspace/global-favorites']"
|
||||
[testId]="'dashboard-global-favorites-rail'"
|
||||
/>
|
||||
} @else if (data.globalFavoritesLoading()) {
|
||||
<section
|
||||
class="rails-page__skeleton-rail"
|
||||
aria-hidden="true"
|
||||
aria-busy="true"
|
||||
>
|
||||
<div class="rails-page__skeleton-header"></div>
|
||||
<div class="rails-page__skeleton-track">
|
||||
@for (slot of skeletonSlots; track slot) {
|
||||
<div class="rails-page__skeleton-card"></div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
<!-- Sources rail -->
|
||||
@if (sourceCards().length > 0) {
|
||||
<lib-dashboard-rail
|
||||
[label]="'WORKSPACE.DASHBOARD.RECENT_SOURCES' | translate"
|
||||
@@ -145,8 +161,22 @@
|
||||
[testId]="'dashboard-recent-sources-rail'"
|
||||
(actionSelected)="onSourceActionSelected($event)"
|
||||
/>
|
||||
} @else if (!data.playlistsLoaded()) {
|
||||
<section
|
||||
class="rails-page__skeleton-rail"
|
||||
aria-hidden="true"
|
||||
aria-busy="true"
|
||||
>
|
||||
<div class="rails-page__skeleton-header"></div>
|
||||
<div class="rails-page__skeleton-track">
|
||||
@for (slot of skeletonSlots; track slot) {
|
||||
<div class="rails-page__skeleton-card"></div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
<!-- Xtream recently-added rail (only relevant if there are Xtream playlists) -->
|
||||
@if (xtreamRecentlyAddedCards().length > 0) {
|
||||
<lib-dashboard-rail
|
||||
[label]="
|
||||
@@ -156,6 +186,21 @@
|
||||
[totalCount]="data.xtreamRecentlyAddedItems().length"
|
||||
[testId]="'dashboard-xtream-recently-added-rail'"
|
||||
/>
|
||||
} @else if (
|
||||
xtreamPlaylistCount() > 0 && data.xtreamRecentlyAddedLoading()
|
||||
) {
|
||||
<section
|
||||
class="rails-page__skeleton-rail"
|
||||
aria-hidden="true"
|
||||
aria-busy="true"
|
||||
>
|
||||
<div class="rails-page__skeleton-header"></div>
|
||||
<div class="rails-page__skeleton-track">
|
||||
@for (slot of skeletonSlots; track slot) {
|
||||
<div class="rails-page__skeleton-card"></div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
</section>
|
||||
Reference in new issue
Block a user