From c1e40b49255cabd10d9a8349a1b3201b8c5b02db Mon Sep 17 00:00:00 2001 From: KrX3D Date: Sat, 29 Aug 2026 20:48:23 +0200 Subject: [PATCH] fix: cap playlist helpers at one per visit instead of one per batch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Standing down our own auto-load cascade settled the open question from the last PR, and the answer was the one that meant my attribution was wrong: autoload_skipped x3 our cascade did stand down keep-one.register x3 the same three helpers appeared anyway YouTube's viewport refill drives the refetches, not schedulePlaylistAutoLoad. With filtering leaving a single tile on a screen that fits four or five rows, the list is starved and YouTube pulls the whole playlist unaided — 780ms flat: 18:44:00.675 batch collect starts 18:44:00.693 helper 1 (initial response) 18:44:01.022 continuation -> helper 2 18:44:01.239 continuation -> helper 3 18:44:01.455 continuation 18:44:03.338 collector's FIRST extra batch, 2.6s late and irrelevant Which also makes the fix clear. A helper exists only so the list is never empty — an empty list is what makes YouTube TV reload the page. It does not have to be a NEW one each batch, and the measurement above shows the extra ones buy nothing: YouTube keeps loading whether or not we add them. Every one it does add is stranded permanently in the virtual list's data model, unreachable on Tizen 5.0 (654577e), so it stays a blank navigable slot for the rest of the visit. That is the four-slots-and-four-presses symptom. So once a playlist has a helper, later all-watched batches return [] and reuse it. Four helpers become one, and that one is retired and DOM-removed by the existing clearPlaylistHelperVideoIdSet path when the final (!hasContinuation) batch lands. Guarded two ways, because the failure mode of getting this wrong is an empty list and a page reload: - Reuse is tied to the location hash the helper was created under, so a helper stranded by navigating away mid-load cannot suppress helper creation on a different playlist. - A page-level playlist response resets the cap, since a freshly rendered list cannot contain an earlier visit's helper. This also clears the stale retired ids behind the page-restart deadlock 654577e described. Note the pacing delay is now doing nothing useful: this run had zero fetch failures, and it is what puts the collector 2.6s behind a race that is over in 780ms. Left alone here to keep this change to one thing. --- mods/features/adblock.js | 48 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/mods/features/adblock.js b/mods/features/adblock.js index 2c2c3a8d..f0df86bc 100644 --- a/mods/features/adblock.js +++ b/mods/features/adblock.js @@ -534,10 +534,43 @@ function filterContinuationItems(items, pageName, hasContinuation = false, label const fallbackItem = reverseItems.find(item => item?.tileRenderer?.header?.tileHeaderRenderer?.thumbnail?.thumbnails?.length) || reverseItems.find(item => item?.tileRenderer) || items[items.length - 1]; + // Cap the playlist at ONE helper for the whole visit. + // + // A helper exists so the list is never left empty, because an empty list + // is what makes YouTube TV reload the page. It does NOT need to be a fresh + // one per batch: an all-watched 68-video playlist was measured keeping one + // helper per response — four blank slots, each permanently stranded in the + // virtual list's data model, which is unreachable on Tizen 5.0 (654577e). + // + // Keeping the existing helper instead is safe because the list stays + // non-empty either way, and it is sufficient because YouTube drives the + // remaining batches itself. That was confirmed by disabling our own + // schedulePlaylistAutoLoad cascade entirely (autoload_skipped x3) and + // watching YouTube still pull the whole playlist in 780ms unaided + // — 18:44:01.022, .239, .455 — purely because filtering leaves the + // viewport starved. So dropping the extra helpers costs no loading. + // Scoped to this playlist. The helper sets live on window and are only + // wiped when a !hasContinuation response arrives, so navigating away + // mid-load leaves them populated — reusing one of those on the NEXT + // playlist would mean never keeping a helper there, leaving its list + // empty, which is the exact condition that makes YouTube TV reload the + // page. Tie the reuse to the hash the helper was created under. + const playlistKey = String(window.location?.hash || ''); + const helperKeyMatches = window.__ttHelperPlaylistKey === playlistKey; + const existingHelper = helperKeyMatches && + (getPlaylistHelperVideoIdSet().size > 0 || getRetiredPlaylistHelperVideoIdSet().size > 0); + if (existingHelper) { + appendFileOnlyLog(`${label}.keep-one.reused`, { + active: Array.from(getPlaylistHelperVideoIdSet()), + retired: getRetiredPlaylistHelperVideoIdSet().size, + }); + return []; + } if (fallbackItem && typeof fallbackItem === 'object') { fallbackItem.__ttKeepOneForContinuation = true; fallbackItem.__ttKeepOneForContinuationLabel = `${label}.visible`; fallbackItem.__ttKeepOneForContinuationParseSeq = Number(window.__ttParseSeq || 0); + window.__ttHelperPlaylistKey = playlistKey; registerPlaylistHelperVideoId(getItemVideoId(fallbackItem), `${label}.keep-one`); } // All items in this batch are watched — auto-fetch the next batch without waiting @@ -561,6 +594,21 @@ function filterContinuationItems(items, pageName, hasContinuation = false, label function filterPlaylistRendererContents(playlistRenderer, pageName, label = 'playlist.renderer') { if (!playlistRenderer || !Array.isArray(playlistRenderer.contents)) return; + if (pageName === 'playlist') { + // A page-level playlist response means a freshly rendered list, so no + // helper from an earlier visit can still be on screen. Reset the one-per- + // playlist cap here: the sets live on window and are otherwise only wiped + // by a !hasContinuation response, so leaving a playlist mid-load would + // strand them — and a stranded set would make the cap skip creating this + // playlist's own helper, leaving the list empty, which is what makes + // YouTube TV reload the page. Also covers the page "restart" 654577e + // documented, where stale retired ids caused the observer to strip + // freshly rendered tiles. + getPlaylistHelperVideoIdSet().clear(); + getRetiredPlaylistHelperVideoIdSet().clear(); + window.__ttHelperPlaylistKey = null; + updateAllHelperHideStyles(); + } const hasContinuation = !!playlistRenderer?.continuations; const before = playlistRenderer.contents.length; playlistRenderer.contents = filterContinuationItems(playlistRenderer.contents, pageName, hasContinuation, label);