mirror of
https://github.com/SoPat712/Speeder.git
synced 2026-10-08 03:03:14 -04:00
fix(performance): bound media discovery work
This commit is contained in:
8 files changed
+109
-79
No files matched your search
+62
-57
@@ -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);
|
||||
|
||||
@@ -427,15 +427,15 @@
|
||||
<div class="row">
|
||||
<label for="subtitleNudgeInterval"
|
||||
>Nudge interval (milliseconds)<br /><em
|
||||
>How often to nudge: 10–1000. Smaller values are more
|
||||
frequent. Default: 50.</em
|
||||
>How often to nudge: 250–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–1000ms):<br /><em
|
||||
>How often to nudge: 10–1000. Smaller values are
|
||||
more frequent. Default: 50.</em
|
||||
>Nudge interval (250–1000ms):<br /><em
|
||||
>How often to nudge: 250–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>
|
||||
|
||||
@@ -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
|
||||
)
|
||||
);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"]
|
||||
})
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user