fix(performance): bound media discovery work

This commit is contained in:
joshpatra committed 2026-07-16 01:44:34 -04:00
1 parent 85d4a0588a
commit c0bd997287
8 files changed
+109 -79

No files matched your search

+62 -57
View File
@@ -128,7 +128,7 @@ var tc = {
"subtitleNudgeEnabledByDefault",
true
),
subtitleNudgeInterval: getSharedDefault("subtitleNudgeInterval", 50),
subtitleNudgeInterval: getSharedDefault("subtitleNudgeInterval", 250),
subtitleNudgeAmount: getSharedDefault("subtitleNudgeAmount", 0.001),
customButtonIcons: {}
},
@@ -161,6 +161,7 @@ var YT_NATIVE_MIN = 0.25;
var YT_NATIVE_MAX = 2.0;
var YT_NATIVE_STEP = 0.05;
var vscObservedRoots = new WeakSet();
var vscObservedRootList = [];
var vscConnectedScannedRoots = new WeakSet();
var vscInitializedDocuments = new WeakSet();
var vscSourceObjectIds = new WeakMap();
@@ -1608,21 +1609,6 @@ function scanNodeForMedia(node, parent, added) {
observeRoot(node.shadowRoot);
}
// Deep-scan descendant elements for shadow roots we haven't observed yet.
// This catches custom elements (like archive.org's <play-av>) whose shadow
// roots were created before our attachShadow patch was installed.
if (added && typeof node.querySelectorAll === "function") {
try {
var allElements = node.querySelectorAll("*");
for (var j = 0; j < allElements.length; j++) {
if (allElements[j].shadowRoot && !vscObservedRoots.has(allElements[j].shadowRoot)) {
observeRoot(allElements[j].shadowRoot);
}
}
} catch (e) {
// querySelectorAll may throw on detached or unusual nodes
}
}
}
function getScanNodeForRoot(root) {
@@ -1683,6 +1669,24 @@ function rescanOpenShadowRoots(root, visited, rescanObserved) {
});
}
function rescanObservedMediaRoots(doc) {
var retainedRoots = [];
vscObservedRootList.forEach(function(root) {
if (!root || root.nodeType === Node.DOCUMENT_NODE) return;
var connected = Boolean(root.host ? root.host.isConnected : root.isConnected);
if (!connected) return;
retainedRoots.push(root);
if (
!doc ||
root.ownerDocument === doc ||
(root.host && root.host.ownerDocument === doc)
) {
scanRootForMedia(root);
}
});
vscObservedRootList = retainedRoots;
}
function observeRoot(root) {
if (!root) return;
@@ -1698,6 +1702,7 @@ function observeRoot(root) {
if (!vscObservedRoots.has(root)) {
vscObservedRoots.add(root);
vscObservedRootList.push(root);
setupListener(root);
attachMutationObserver(root);
attachMediaDetectionListeners(root);
@@ -1765,24 +1770,34 @@ function installPageShadowBridge() {
);
}
function startPersistentShadowFallback() {
if (window.vscPersistentShadowFallbackTimer) return;
window.vscPersistentShadowFallbackTimer = setInterval(function() {
requestIdle(
function() {
if (!document.body) return;
rescanOpenShadowRoots(document, undefined, false);
},
{ timeout: 2000 }
);
}, 30000);
function startBoundedShadowFallback() {
if (window.vscBoundedShadowFallbackStarted) return;
window.vscBoundedShadowFallbackStarted = true;
window.vscBoundedShadowFallbackTimers = [3000, 10000, 30000].map(
function(delay) {
return setTimeout(function() {
requestIdle(
function() {
if (
!document.body ||
(delay !== 3000 && window.vscPageShadowBridgeLoaded)
) {
return;
}
rescanOpenShadowRoots(document, undefined, false);
},
{ timeout: 2000 }
);
}, delay);
}
);
}
function handleBridgeFailure() {
window.vscPageShadowBridgeRequested = false;
window.vscPageShadowBridgeRetries =
(Number(window.vscPageShadowBridgeRetries) || 0) + 1;
startPersistentShadowFallback();
startBoundedShadowFallback();
if (window.vscPageShadowBridgeRetries > 3) return;
clearTimeout(window.vscPageShadowBridgeRetryTimer);
window.vscPageShadowBridgeRetryTimer = setTimeout(function() {
@@ -1815,10 +1830,6 @@ function installPageShadowBridge() {
window.vscPageShadowBridgeLoaded = true;
window.vscPageShadowBridgeRetries = 0;
clearTimeout(window.vscPageShadowBridgeRetryTimer);
if (window.vscPersistentShadowFallbackTimer) {
clearInterval(window.vscPersistentShadowFallbackTimer);
window.vscPersistentShadowFallbackTimer = null;
}
});
bridge.addEventListener("error", function() {
bridge.remove();
@@ -1826,6 +1837,9 @@ function installPageShadowBridge() {
});
window.vscPageShadowBridgeRequested = true;
parent.appendChild(bridge);
// The bridge reports future roots, but cannot report roots that existed
// before the extension loaded. Keep one bounded initial discovery pass.
startBoundedShadowFallback();
} catch (error) {
handleBridgeFailure();
log(`Unable to install page shadow bridge: ${error.message}`, 3);
@@ -1956,7 +1970,7 @@ function hydrateRuntimeSettings(rawStorage, options) {
: getSharedDefault("subtitleNudgeEnabledByDefault", true);
tc.settings.subtitleNudgeInterval = Math.min(
1000,
Math.max(10, Number(storage.subtitleNudgeInterval) || 50)
Math.max(250, Number(storage.subtitleNudgeInterval) || 250)
);
tc.settings.subtitleNudgeAmount =
Number(storage.subtitleNudgeAmount) ||
@@ -2810,7 +2824,7 @@ function setupControllerHostTracking(videoController, wrapper, mount) {
var frameId = null;
var geometryRetryTimer = null;
var geometryRetryAttempts = 0;
var maxGeometryRetryAttempts = 40;
var maxGeometryRetryAttempts = 12;
var update = function() {
frameId = null;
positionControllerHost(wrapper, videoController.video, mount);
@@ -4241,9 +4255,11 @@ function attachMutationObserver(root) {
function() {
var mutationsToProcess = pendingMutations.splice(0);
mutationProcessingScheduled = false;
var controllerMountTargets = new Set();
mutationsToProcess.forEach(function(mutation) {
if (mutation.type === "childList") {
controllerMountTargets.add(mutation.target);
mutation.addedNodes.forEach(function(node) {
// Skip text nodes, comments, etc. — only elements can contain media
if (node.nodeType !== Node.ELEMENT_NODE) return;
@@ -4254,14 +4270,6 @@ function attachMutationObserver(root) {
scanNodeForMedia(node, node.parentNode || mutation.target, false);
});
// Player gesture/click panes are frequently inserted after the
// video. Keep equal-z-index controller hosts last, and repair a
// host that the site removed independently of its media element.
tc.mediaElements.slice().forEach(function(video) {
if (!video || !video.vsc || !video.vsc.div) return;
if (video.vsc.controllerHostMount !== mutation.target) return;
remountControllerHost(video.vsc, mutation.target);
});
return;
}
@@ -4299,6 +4307,17 @@ function attachMutationObserver(root) {
}
});
// Reconcile once per batch, not once per mutation. Large news/social
// pages can emit hundreds of childList records in one render pass.
if (controllerMountTargets.size > 0) {
tc.mediaElements.slice().forEach(function(video) {
if (!video || !video.vsc || !video.vsc.div) return;
var mount = video.vsc.controllerHostMount;
if (!controllerMountTargets.has(mount)) return;
remountControllerHost(video.vsc, mount);
});
}
// Document selectors do not cross shadow boundaries. A detached
// custom-element host can therefore hide connected-looking media from
// the removed subtree scan; prune by composed connectivity as a final
@@ -4414,26 +4433,12 @@ function initializeNow(doc, forceReinit = false) {
attachKeydownListeners(doc);
attachMediaTargetTracking(doc);
// Bounded fallback scans catch open page-world roots if a site's CSP blocked
// the bridge script. The event bridge handles the normal ongoing path.
if (!doc.vscDelayedShadowScanDone) {
doc.vscDelayedShadowScanDone = true;
[3000, 10000, 30000].forEach(function(delay) {
setTimeout(function() {
if (!doc.body) return;
try {
rescanOpenShadowRoots(doc);
} catch (e) {}
}, delay);
});
}
if (forceReinit) {
log("Force re-initialization requested", 4);
// A root is normally scanned only once. A user-requested rescan must also
// revisit media that was present but source-less during the initial scan.
scanRootForMedia(doc);
rescanOpenShadowRoots(doc);
rescanObservedMediaRoots(doc);
refreshAllControllerGeometry();
if (tc.settings.rememberSpeed || tc.settings.forceLastSavedSpeed) {
tc.mediaElements.slice().forEach(applyRememberedSpeedPolicy);
+7 -7
View File
@@ -427,15 +427,15 @@
<div class="row">
<label for="subtitleNudgeInterval"
>Nudge interval (milliseconds)<br /><em
>How often to nudge: 10&ndash;1000. Smaller values are more
frequent. Default: 50.</em
>How often to nudge: 250&ndash;1000. Smaller values are more
frequent. Default: 250.</em
>
</label>
<input
id="subtitleNudgeInterval"
type="text"
value=""
placeholder="50"
placeholder="250"
/>
</div>
</section>
@@ -909,12 +909,12 @@
</div>
<div class="site-rule-option site-rule-option-field">
<label
>Nudge interval (10&ndash;1000ms):<br /><em
>How often to nudge: 10&ndash;1000. Smaller values are
more frequent. Default: 50.</em
>Nudge interval (250&ndash;1000ms):<br /><em
>How often to nudge: 250&ndash;1000. Smaller values are
more frequent. Default: 250.</em
></label
>
<input type="text" class="site-subtitleNudgeInterval" placeholder="50" />
<input type="text" class="site-subtitleNudgeInterval" placeholder="250" />
</div>
</div>
</div>
+3 -3
View File
@@ -878,8 +878,8 @@ function save_options() {
parseInt(document.getElementById("subtitleNudgeInterval").value, 10) ||
tcDefaults.subtitleNudgeInterval;
if (settings.subtitleNudgeInterval < 10) {
settings.subtitleNudgeInterval = 10;
if (settings.subtitleNudgeInterval < 250) {
settings.subtitleNudgeInterval = 250;
}
if (settings.subtitleNudgeInterval > 1000) {
settings.subtitleNudgeInterval = 1000;
@@ -1003,7 +1003,7 @@ function save_options() {
rule.subtitleNudgeInterval = Math.min(
1000,
Math.max(
10,
250,
Number.isFinite(nudgeIv) ? nudgeIv : settings.subtitleNudgeInterval
)
);
+4 -6
View File
@@ -149,9 +149,7 @@
{
title: "YouTube videos",
pattern: "/^https:\\/\\/(www\\.)?youtube\\.com\\/(?!shorts\\/).*/",
enabled: true,
enableSubtitleNudge: true,
subtitleNudgeInterval: 50
enabled: true
},
{
title: "YouTube Shorts",
@@ -176,7 +174,7 @@
popupControllerButtons: DEFAULT_BUTTONS.slice(),
enableSubtitleNudge: false,
subtitleNudgeEnabledByDefault: true,
subtitleNudgeInterval: 50,
subtitleNudgeInterval: 250,
subtitleNudgeAmount: 0.001
};
@@ -359,7 +357,7 @@
return clampFiniteNumber(value, 0, 200, undefined);
}
if (key === "subtitleNudgeInterval") {
return clampFiniteNumber(value, 10, 1000, undefined);
return clampFiniteNumber(value, 250, 1000, undefined);
}
if (key === "preferredSpeed") {
return clampFiniteNumber(value, 0.0625, 16, undefined);
@@ -914,7 +912,7 @@
});
expanded.subtitleNudgeInterval = clampFiniteNumber(
expanded.subtitleNudgeInterval,
10,
250,
1000,
DEFAULT_SETTINGS.subtitleNudgeInterval
);
+25 -2
View File
@@ -154,7 +154,7 @@ describe("inject.js media/controller lifecycle regressions", () => {
expect(shadowEvents).toBe(1);
});
it("retries a blocked page bridge and keeps a persistent shadow fallback", async () => {
it("retries a blocked page bridge with bounded shadow fallback scans", async () => {
vi.useFakeTimers();
bootInject();
const firstBridge = document.querySelector(
@@ -165,7 +165,9 @@ describe("inject.js media/controller lifecycle regressions", () => {
firstBridge.dispatchEvent(new Event("error"));
expect(window.vscPageShadowBridgeRequested).toBe(false);
expect(window.vscPersistentShadowFallbackTimer).not.toBeNull();
expect(window.vscBoundedShadowFallbackStarted).toBe(true);
expect(window.vscBoundedShadowFallbackTimers).toHaveLength(3);
expect(window.vscPersistentShadowFallbackTimer).toBeUndefined();
await vi.advanceTimersByTimeAsync(250);
expect(
document.querySelector('script[src$="content/shadow-bridge.js"]')
@@ -513,6 +515,27 @@ describe("inject.js media/controller lifecycle regressions", () => {
expect(mountA.querySelector(".vsc-controller")).toBeNull();
});
it("reconciles a burst of player mutations only once per idle batch", async () => {
bootInject();
await settleLifecycle();
const { mount, wrapper } = createControlledVideo();
const originalAppendChild = mount.appendChild.bind(mount);
let controllerReorders = 0;
mount.appendChild = function(node) {
if (node === wrapper) controllerReorders += 1;
return originalAppendChild(node);
};
for (let index = 0; index < 25; index += 1) {
mount.appendChild(document.createElement("span"));
}
await settleLifecycle(8);
expect(controllerReorders).toBe(1);
expect(mount.lastElementChild).toBe(wrapper);
});
it("suppresses a zero-size host and reveals it when the video becomes visible", async () => {
bootInject();
await settleLifecycle();
+3 -1
View File
@@ -250,7 +250,9 @@ describe("inject runtime", () => {
loadScript("extension/content/inject.js");
// Fast-forward 3000ms for delayed rescan to trigger
vi.advanceTimersByTime(3000);
await vi.advanceTimersByTimeAsync(3000);
// The bounded discovery pass yields to an idle callback before traversing.
await vi.advanceTimersByTimeAsync(2000);
for (let i = 0; i < 5; i += 1) {
await flushAsyncWork();
+2 -2
View File
@@ -236,7 +236,7 @@ describe("options page", () => {
expect(savedSettings.controllerMarginTop).toBe(200);
expect(savedSettings.controllerMarginBottom).toBe(0);
expect(savedSettings.subtitleNudgeEnabledByDefault).toBe(false);
expect(savedSettings.subtitleNudgeInterval).toBe(10);
expect(savedSettings.subtitleNudgeInterval).toBe(250);
expect(savedSettings.showPopupControlBar).toBe(false);
expect(savedSettings.popupMatchHoverControls).toBe(false);
expect(savedSettings.popupControllerButtons).toEqual(["rewind", "faster"]);
@@ -251,7 +251,7 @@ describe("options page", () => {
controllerOpacity: 0,
enableSubtitleNudge: true,
subtitleNudgeEnabledByDefault: false,
subtitleNudgeInterval: 75,
subtitleNudgeInterval: 250,
showPopupControlBar: false,
popupControllerButtons: ["advance"]
})
+3 -1
View File
@@ -34,6 +34,8 @@ describe("canonical settings storage", () => {
})
);
expect(settings.shortcutTargetMode).toBe("closest");
expect(settings.subtitleNudgeInterval).toBe(250);
expect(settings.siteRules[0].enableSubtitleNudge).toBeUndefined();
});
it("round-trips global and site-specific shortcut targeting", () => {
@@ -228,7 +230,7 @@ describe("canonical settings storage", () => {
expect(rule.rememberSpeed).toBe(false);
expect(rule.forceLastSavedSpeed).toBe(true);
expect(rule.controllerOpacity).toBe(1);
expect(rule.subtitleNudgeInterval).toBe(10);
expect(rule.subtitleNudgeInterval).toBe(250);
expect(rule.preferredSpeed).toBe(16);
});