mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-10 10:11:46 +00:00
feat(performance-improvement): added "performance improvement" plugin
This commit is contained in:
@ -588,6 +588,10 @@
|
||||
},
|
||||
"name": "Notifications"
|
||||
},
|
||||
"performance-improvement": {
|
||||
"description": "Improve performance by enabling dangerous scripts",
|
||||
"name": "Performance improvement [Beta]"
|
||||
},
|
||||
"picture-in-picture": {
|
||||
"description": "Allows to switch the app to picture-in-picture mode",
|
||||
"menu": {
|
||||
|
||||
19
src/plugins/performance-improvement/index.ts
Normal file
19
src/plugins/performance-improvement/index.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { createPlugin } from '@/utils';
|
||||
import { t } from '@/i18n';
|
||||
|
||||
import { injectRm3 } from './scripts/rm3';
|
||||
import { injectCpuTamer } from './scripts/cpu-tamer';
|
||||
|
||||
export default createPlugin({
|
||||
name: () => t('plugins.performance-improvement.name'),
|
||||
description: () => t('plugins.performance-improvement.description'),
|
||||
restartNeeded: true,
|
||||
addedVersion: '3.9.X',
|
||||
config: {
|
||||
enabled: true,
|
||||
},
|
||||
renderer() {
|
||||
injectRm3();
|
||||
injectCpuTamer();
|
||||
},
|
||||
});
|
||||
3
src/plugins/performance-improvement/scripts/cpu-tamer/cpu-tamer-by-animationframe.d.ts
vendored
Normal file
3
src/plugins/performance-improvement/scripts/cpu-tamer/cpu-tamer-by-animationframe.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
export declare const injectCpuTamerByAnimationFrame: (
|
||||
__CONTEXT__: unknown,
|
||||
) => void;
|
||||
@ -0,0 +1,286 @@
|
||||
/*
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright 2021-2025 CY Fung
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
export const injectCpuTamerByAnimationFrame = ((__CONTEXT__) => {
|
||||
'use strict';
|
||||
|
||||
const win = this instanceof Window ? this : window;
|
||||
|
||||
// Create a unique key for the script and check if it is already running
|
||||
const hkey_script = 'nzsxclvflluv';
|
||||
if (win[hkey_script]) throw new Error('Duplicated Userscript Calling'); // avoid duplicated scripting
|
||||
win[hkey_script] = true;
|
||||
|
||||
/** @type {globalThis.PromiseConstructor} */
|
||||
const Promise = (async () => { })().constructor; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve.
|
||||
const PromiseExternal = ((resolve_, reject_) => {
|
||||
const h = (resolve, reject) => { resolve_ = resolve; reject_ = reject };
|
||||
return class PromiseExternal extends Promise {
|
||||
constructor(cb = h) {
|
||||
super(cb);
|
||||
if (cb === h) {
|
||||
/** @type {(value: any) => void} */
|
||||
this.resolve = resolve_;
|
||||
/** @type {(reason?: any) => void} */
|
||||
this.reject = reject_;
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
const isGPUAccelerationAvailable = (() => {
|
||||
// https://gist.github.com/cvan/042b2448fcecefafbb6a91469484cdf8
|
||||
try {
|
||||
const canvas = document.createElement('canvas');
|
||||
return !!(canvas.getContext('webgl') || canvas.getContext('experimental-webgl'));
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
if (!isGPUAccelerationAvailable) {
|
||||
throw new Error('Your browser does not support GPU Acceleration. YouTube CPU Tamer by AnimationFrame is skipped.');
|
||||
}
|
||||
|
||||
const timeupdateDT = (() => {
|
||||
|
||||
window.__j6YiAc__ = 1;
|
||||
|
||||
document.addEventListener('timeupdate', () => {
|
||||
window.__j6YiAc__ = Date.now();
|
||||
}, true);
|
||||
|
||||
let kz = -1;
|
||||
try {
|
||||
kz = top.__j6YiAc__;
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
|
||||
return kz >= 1 ? () => top.__j6YiAc__ : () => window.__j6YiAc__;
|
||||
|
||||
})();
|
||||
|
||||
const cleanContext = async (win) => {
|
||||
const waitFn = requestAnimationFrame; // shall have been binded to window
|
||||
try {
|
||||
let mx = 16; // MAX TRIAL
|
||||
const frameId = 'vanillajs-iframe-v1'
|
||||
let frame = document.getElementById(frameId);
|
||||
let removeIframeFn = null;
|
||||
if (!frame) {
|
||||
frame = document.createElement('iframe');
|
||||
frame.id = frameId;
|
||||
const blobURL = typeof webkitCancelAnimationFrame === 'function' && typeof kagi === 'undefined' ? (frame.src = URL.createObjectURL(new Blob([], { type: 'text/html' }))) : null; // avoid Brave Crash
|
||||
frame.sandbox = 'allow-same-origin'; // script cannot be run inside iframe but API can be obtained from iframe
|
||||
let n = document.createElement('noscript'); // wrap into NOSCRPIT to avoid reflow (layouting)
|
||||
n.appendChild(frame);
|
||||
while (!document.documentElement && mx-- > 0) await new Promise(waitFn); // requestAnimationFrame here could get modified by YouTube engine
|
||||
const root = document.documentElement;
|
||||
root.appendChild(n); // throw error if root is null due to exceeding MAX TRIAL
|
||||
if (blobURL) Promise.resolve().then(() => URL.revokeObjectURL(blobURL));
|
||||
|
||||
removeIframeFn = (setTimeout) => {
|
||||
const removeIframeOnDocumentReady = (e) => {
|
||||
e && win.removeEventListener("DOMContentLoaded", removeIframeOnDocumentReady, false);
|
||||
e = n;
|
||||
n = win = removeIframeFn = 0;
|
||||
setTimeout ? setTimeout(() => e.remove(), 200) : e.remove();
|
||||
}
|
||||
if (!setTimeout || document.readyState !== 'loading') {
|
||||
removeIframeOnDocumentReady();
|
||||
} else {
|
||||
win.addEventListener("DOMContentLoaded", removeIframeOnDocumentReady, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
while (!frame.contentWindow && mx-- > 0) await new Promise(waitFn);
|
||||
const fc = frame.contentWindow;
|
||||
if (!fc) throw "window is not found."; // throw error if root is null due to exceeding MAX TRIAL
|
||||
try {
|
||||
const { requestAnimationFrame, setInterval, setTimeout, clearInterval, clearTimeout } = fc;
|
||||
const res = { requestAnimationFrame, setInterval, setTimeout, clearInterval, clearTimeout };
|
||||
for (let k in res) res[k] = res[k].bind(win); // necessary
|
||||
if (removeIframeFn) Promise.resolve(res.setTimeout).then(removeIframeFn);
|
||||
return res;
|
||||
} catch (e) {
|
||||
if (removeIframeFn) removeIframeFn();
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
cleanContext(win).then(__CONTEXT__ => {
|
||||
|
||||
if (!__CONTEXT__) return null;
|
||||
|
||||
const { requestAnimationFrame, setTimeout, setInterval, clearTimeout, clearInterval } = __CONTEXT__;
|
||||
|
||||
/** @type {Function|null} */
|
||||
let afInterupter = null;
|
||||
|
||||
const getRAFHelper = () => {
|
||||
const asc = document.createElement('a-f');
|
||||
if (!('onanimationiteration' in asc)) {
|
||||
return (resolve) => requestAnimationFrame(afInterupter = resolve);
|
||||
}
|
||||
asc.id = 'a-f';
|
||||
let qr = null;
|
||||
asc.onanimationiteration = function () {
|
||||
if (qr !== null) qr = (qr(), null);
|
||||
}
|
||||
if (!document.getElementById('afscript')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'afscript';
|
||||
style.textContent = `
|
||||
@keyFrames aF1 {
|
||||
0% {
|
||||
order: 0;
|
||||
}
|
||||
100% {
|
||||
order: 1;
|
||||
}
|
||||
}
|
||||
#a-f[id] {
|
||||
visibility: collapse !important;
|
||||
position: fixed !important;
|
||||
display: block !important;
|
||||
top: -100px !important;
|
||||
left: -100px !important;
|
||||
margin:0 !important;
|
||||
padding:0 !important;
|
||||
outline:0 !important;
|
||||
border:0 !important;
|
||||
z-index:-1 !important;
|
||||
width: 0px !important;
|
||||
height: 0px !important;
|
||||
contain: strict !important;
|
||||
pointer-events: none !important;
|
||||
animation: 1ms steps(2, jump-none) 0ms infinite alternate forwards running aF1 !important;
|
||||
}
|
||||
`;
|
||||
(document.head || document.documentElement).appendChild(style);
|
||||
}
|
||||
document.documentElement.insertBefore(asc, document.documentElement.firstChild);
|
||||
return (resolve) => (qr = afInterupter = resolve);
|
||||
};
|
||||
|
||||
/** @type {(resolve: () => void)} */
|
||||
const rafPN = getRAFHelper(); // rAF will not execute if document is hidden
|
||||
|
||||
(() => {
|
||||
let afPromiseP, afPromiseQ; // non-null
|
||||
afPromiseP = afPromiseQ = { resolved: true }; // initial state for !uP && !uQ
|
||||
let afix = 0;
|
||||
const afResolve = async (rX) => {
|
||||
await new Promise(rafPN);
|
||||
rX.resolved = true;
|
||||
const t = afix = (afix & 1073741823) + 1;
|
||||
return rX.resolve(t), t;
|
||||
};
|
||||
const eFunc = async () => {
|
||||
const uP = !afPromiseP.resolved ? afPromiseP : null;
|
||||
const uQ = !afPromiseQ.resolved ? afPromiseQ : null;
|
||||
let t = 0;
|
||||
if (uP && uQ) {
|
||||
const t1 = await uP;
|
||||
const t2 = await uQ;
|
||||
t = ((t1 - t2) & 536870912) === 0 ? t1 : t2; // = 0 for t1 - t2 = [0, 536870911], [–1073741824, -536870913]
|
||||
} else {
|
||||
const vP = !uP ? (afPromiseP = new PromiseExternal()) : null;
|
||||
const vQ = !uQ ? (afPromiseQ = new PromiseExternal()) : null;
|
||||
if (uQ) await uQ; else if (uP) await uP;
|
||||
if (vP) t = await afResolve(vP);
|
||||
if (vQ) t = await afResolve(vQ);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
const inExec = new Set();
|
||||
const wFunc = async (handler, wStore) => {
|
||||
try {
|
||||
const ct = Date.now();
|
||||
if (ct - timeupdateDT() < 800 && ct - wStore.dt < 800) {
|
||||
const cid = wStore.cid;
|
||||
inExec.add(cid);
|
||||
const t = await eFunc();
|
||||
const didNotRemove = inExec.delete(cid); // true for valid key
|
||||
if (!didNotRemove || t === wStore.lastExecution) return;
|
||||
wStore.lastExecution = t;
|
||||
}
|
||||
wStore.dt = ct;
|
||||
handler();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
const sFunc = (propFunc) => {
|
||||
return (func, ms = 0, ...args) => {
|
||||
if (typeof func === 'function') { // ignore all non-function parameter (e.g. string)
|
||||
const wStore = { dt: Date.now() };
|
||||
return (wStore.cid = propFunc(wFunc, ms, (args.length > 0 ? func.bind(null, ...args) : func), wStore));
|
||||
} else {
|
||||
return propFunc(func, ms, ...args);
|
||||
}
|
||||
};
|
||||
};
|
||||
win.setTimeout = sFunc(setTimeout);
|
||||
win.setInterval = sFunc(setInterval);
|
||||
|
||||
const dFunc = (propFunc) => {
|
||||
return (cid) => {
|
||||
if (cid) inExec.delete(cid) || propFunc(cid);
|
||||
};
|
||||
};
|
||||
|
||||
win.clearTimeout = dFunc(clearTimeout);
|
||||
win.clearInterval = dFunc(clearInterval);
|
||||
|
||||
try {
|
||||
win.setTimeout.toString = setTimeout.toString.bind(setTimeout);
|
||||
win.setInterval.toString = setInterval.toString.bind(setInterval);
|
||||
win.clearTimeout.toString = clearTimeout.toString.bind(clearTimeout);
|
||||
win.clearInterval.toString = clearInterval.toString.bind(clearInterval);
|
||||
} catch (e) { console.warn(e) }
|
||||
|
||||
})();
|
||||
|
||||
let mInterupter = null;
|
||||
setInterval(() => {
|
||||
if (mInterupter === afInterupter) {
|
||||
if (mInterupter !== null) afInterupter = mInterupter = (mInterupter(), null);
|
||||
} else {
|
||||
mInterupter = afInterupter;
|
||||
}
|
||||
}, 125);
|
||||
});
|
||||
|
||||
});
|
||||
3
src/plugins/performance-improvement/scripts/cpu-tamer/cpu-tamer-by-dom-mutation.d.ts
vendored
Normal file
3
src/plugins/performance-improvement/scripts/cpu-tamer/cpu-tamer-by-dom-mutation.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
export declare const injectCpuTamerByDomMutation: (
|
||||
__CONTEXT__: unknown,
|
||||
) => void;
|
||||
@ -0,0 +1,281 @@
|
||||
/*
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright 2024-2025 CY Fung
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
export const injectCpuTamerByDomMutation = ((__CONTEXT__) => {
|
||||
'use strict';
|
||||
|
||||
const win = this instanceof Window ? this : window;
|
||||
|
||||
// Create a unique key for the script and check if it is already running
|
||||
const hkey_script = 'nzsxclvflluv';
|
||||
if (win[hkey_script]) throw new Error('Duplicated Userscript Calling'); // avoid duplicated scripting
|
||||
win[hkey_script] = true;
|
||||
|
||||
/** @type {globalThis.PromiseConstructor} */
|
||||
const Promise = (async () => { })().constructor; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve.
|
||||
const PromiseExternal = ((resolve_, reject_) => {
|
||||
const h = (resolve, reject) => { resolve_ = resolve; reject_ = reject };
|
||||
return class PromiseExternal extends Promise {
|
||||
constructor(cb = h) {
|
||||
super(cb);
|
||||
if (cb === h) {
|
||||
/** @type {(value: any) => void} */
|
||||
this.resolve = resolve_;
|
||||
/** @type {(reason?: any) => void} */
|
||||
this.reject = reject_;
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
// for future use
|
||||
/*
|
||||
const timeupdateDT = (() => {
|
||||
|
||||
window.__j6YiAc__ = 1;
|
||||
|
||||
document.addEventListener('timeupdate', () => {
|
||||
window.__j6YiAc__ = Date.now();
|
||||
}, true);
|
||||
|
||||
let kz = -1;
|
||||
try {
|
||||
kz = top.__j6YiAc__;
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
|
||||
return kz >= 1 ? () => top.__j6YiAc__ : () => window.__j6YiAc__;
|
||||
|
||||
})();
|
||||
*/
|
||||
|
||||
const cleanContext = async (win) => {
|
||||
const waitFn = requestAnimationFrame; // shall have been binded to window
|
||||
try {
|
||||
let mx = 16; // MAX TRIAL
|
||||
const frameId = 'vanillajs-iframe-v1'
|
||||
let frame = document.getElementById(frameId);
|
||||
let removeIframeFn = null;
|
||||
if (!frame) {
|
||||
frame = document.createElement('iframe');
|
||||
frame.id = frameId;
|
||||
const blobURL = typeof webkitCancelAnimationFrame === 'function' && typeof kagi === 'undefined' ? (frame.src = URL.createObjectURL(new Blob([], { type: 'text/html' }))) : null; // avoid Brave Crash
|
||||
frame.sandbox = 'allow-same-origin'; // script cannot be run inside iframe but API can be obtained from iframe
|
||||
let n = document.createElement('noscript'); // wrap into NOSCRPIT to avoid reflow (layouting)
|
||||
n.appendChild(frame);
|
||||
while (!document.documentElement && mx-- > 0) await new Promise(waitFn); // requestAnimationFrame here could get modified by YouTube engine
|
||||
const root = document.documentElement;
|
||||
root.appendChild(n); // throw error if root is null due to exceeding MAX TRIAL
|
||||
if (blobURL) Promise.resolve().then(() => URL.revokeObjectURL(blobURL));
|
||||
|
||||
removeIframeFn = (setTimeout) => {
|
||||
const removeIframeOnDocumentReady = (e) => {
|
||||
e && win.removeEventListener("DOMContentLoaded", removeIframeOnDocumentReady, false);
|
||||
e = n;
|
||||
n = win = removeIframeFn = 0;
|
||||
setTimeout ? setTimeout(() => e.remove(), 200) : e.remove();
|
||||
}
|
||||
if (!setTimeout || document.readyState !== 'loading') {
|
||||
removeIframeOnDocumentReady();
|
||||
} else {
|
||||
win.addEventListener("DOMContentLoaded", removeIframeOnDocumentReady, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
while (!frame.contentWindow && mx-- > 0) await new Promise(waitFn);
|
||||
const fc = frame.contentWindow;
|
||||
if (!fc) throw "window is not found."; // throw error if root is null due to exceeding MAX TRIAL
|
||||
try {
|
||||
const { requestAnimationFrame, setInterval, setTimeout, clearInterval, clearTimeout } = fc;
|
||||
const res = { requestAnimationFrame, setInterval, setTimeout, clearInterval, clearTimeout };
|
||||
for (let k in res) res[k] = res[k].bind(win); // necessary
|
||||
if (removeIframeFn) Promise.resolve(res.setTimeout).then(removeIframeFn);
|
||||
return res;
|
||||
} catch (e) {
|
||||
if (removeIframeFn) removeIframeFn();
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const { _setAttribute, _insertBefore, _hasAttribute } = (() => {
|
||||
let _setAttribute = Element.prototype.setAttribute;
|
||||
try {
|
||||
_setAttribute = ShadyDOM.nativeMethods.setAttribute || _setAttribute;
|
||||
} catch (e) { }
|
||||
let _hasAttribute = Element.prototype.hasAttribute;
|
||||
try {
|
||||
_hasAttribute = ShadyDOM.nativeMethods.hasAttribute || _hasAttribute;
|
||||
} catch (e) { }
|
||||
let _insertBefore = Node.prototype.insertBefore;
|
||||
try {
|
||||
_insertBefore = ShadyDOM.nativeMethods.insertBefore || _insertBefore;
|
||||
} catch (e) { }
|
||||
return { _setAttribute, _insertBefore, _hasAttribute};
|
||||
})();
|
||||
|
||||
cleanContext(win).then(__CONTEXT__ => {
|
||||
|
||||
if (!__CONTEXT__) return null;
|
||||
|
||||
const { setTimeout, setInterval, clearTimeout, clearInterval } = __CONTEXT__;
|
||||
|
||||
/*
|
||||
/-** @type {Function|null} *-/
|
||||
// let afInterupter = null;
|
||||
*/
|
||||
|
||||
const getDMHelper = () => {
|
||||
let _dm = document.getElementById('d-m');
|
||||
if (!_dm) {
|
||||
_dm = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
|
||||
_dm.id = 'd-m';
|
||||
_insertBefore.call(document.documentElement, _dm, document.documentElement.firstChild);
|
||||
}
|
||||
const dm = _dm;
|
||||
dm._setAttribute = _setAttribute;
|
||||
dm._hasAttribute = _hasAttribute;
|
||||
let j = 0;
|
||||
let attributeName_;
|
||||
while (dm._hasAttribute(attributeName_ = `dm-${Math.floor(Math.random() * 314159265359 + 314159265359).toString(36)}`)) {
|
||||
// none
|
||||
}
|
||||
const attributeName = attributeName_;
|
||||
let sr = null;
|
||||
const mo = new MutationObserver(() => {
|
||||
const sr_ = sr;
|
||||
if (sr_ !== null) {
|
||||
sr = null;
|
||||
if (j > 8) j = 0;
|
||||
sr_.resolve();
|
||||
}
|
||||
});
|
||||
mo.observe(document, { childList: true, subtree: true, attributes: true });
|
||||
return () => {
|
||||
return sr || (sr = (dm._setAttribute(attributeName, ++j), (new PromiseExternal()))); // mutationcallback in next macrotask
|
||||
};
|
||||
};
|
||||
|
||||
/** @type {(resolve: () => void)} */
|
||||
const dmSN = getDMHelper(); // dm will execute even if document is hidden
|
||||
|
||||
(() => {
|
||||
let dmPromiseP, dmPromiseQ; // non-null
|
||||
dmPromiseP = dmPromiseQ = { resolved: true }; // initial state for !uP && !uQ
|
||||
let dmix = 0;
|
||||
const dmResolve = async (rX) => {
|
||||
await dmSN();
|
||||
rX.resolved = true;
|
||||
const t = dmix = (dmix & 1073741823) + 1;
|
||||
return rX.resolve(t), t;
|
||||
};
|
||||
const eFunc = async () => {
|
||||
const uP = !dmPromiseP.resolved ? dmPromiseP : null;
|
||||
const uQ = !dmPromiseQ.resolved ? dmPromiseQ : null;
|
||||
let t = 0;
|
||||
if (uP && uQ) {
|
||||
const t1 = await uP;
|
||||
const t2 = await uQ;
|
||||
t = ((t1 - t2) & 536870912) === 0 ? t1 : t2; // = 0 for t1 - t2 = [0, 536870911], [–1073741824, -536870913]
|
||||
} else {
|
||||
const vP = !uP ? (dmPromiseP = new PromiseExternal()) : null;
|
||||
const vQ = !uQ ? (dmPromiseQ = new PromiseExternal()) : null;
|
||||
if (uQ) await uQ; else if (uP) await uP;
|
||||
if (vP) t = await dmResolve(vP);
|
||||
if (vQ) t = await dmResolve(vQ);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
const inExec = new Set();
|
||||
const wFunc = async (handler, wStore) => {
|
||||
try {
|
||||
const ct = Date.now();
|
||||
if (ct - wStore.dt < 800) {
|
||||
const cid = wStore.cid;
|
||||
inExec.add(cid);
|
||||
const t = await eFunc();
|
||||
const didNotRemove = inExec.delete(cid); // true for valid key
|
||||
if (!didNotRemove || t === wStore.lastExecution) return;
|
||||
wStore.lastExecution = t;
|
||||
}
|
||||
wStore.dt = ct;
|
||||
handler();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
const sFunc = (propFunc) => {
|
||||
return (func, ms = 0, ...args) => {
|
||||
if (typeof func === 'function') { // ignore all non-function parameter (e.g. string)
|
||||
const wStore = { dt: Date.now() };
|
||||
return (wStore.cid = propFunc(wFunc, ms, (args.length > 0 ? func.bind(null, ...args) : func), wStore));
|
||||
} else {
|
||||
return propFunc(func, ms, ...args);
|
||||
}
|
||||
};
|
||||
};
|
||||
win.setTimeout = sFunc(setTimeout);
|
||||
win.setInterval = sFunc(setInterval);
|
||||
|
||||
const dFunc = (propFunc) => {
|
||||
return (cid) => {
|
||||
if (cid) inExec.delete(cid) || propFunc(cid);
|
||||
};
|
||||
};
|
||||
|
||||
win.clearTimeout = dFunc(clearTimeout);
|
||||
win.clearInterval = dFunc(clearInterval);
|
||||
|
||||
try {
|
||||
win.setTimeout.toString = setTimeout.toString.bind(setTimeout);
|
||||
win.setInterval.toString = setInterval.toString.bind(setInterval);
|
||||
win.clearTimeout.toString = clearTimeout.toString.bind(clearTimeout);
|
||||
win.clearInterval.toString = clearInterval.toString.bind(clearInterval);
|
||||
} catch (e) { console.warn(e) }
|
||||
|
||||
})();
|
||||
|
||||
/*
|
||||
let mInterupter = null;
|
||||
setInterval(() => {
|
||||
if (mInterupter === afInterupter) {
|
||||
if (mInterupter !== null) afInterupter = mInterupter = (mInterupter(), null);
|
||||
} else {
|
||||
mInterupter = afInterupter;
|
||||
}
|
||||
}, 125);
|
||||
*/
|
||||
});
|
||||
|
||||
});
|
||||
@ -0,0 +1,22 @@
|
||||
import { injectCpuTamerByAnimationFrame } from './cpu-tamer-by-animationframe';
|
||||
import { injectCpuTamerByDomMutation } from './cpu-tamer-by-dom-mutation';
|
||||
|
||||
const isGPUAccelerationAvailable = () => {
|
||||
// https://gist.github.com/cvan/042b2448fcecefafbb6a91469484cdf8
|
||||
try {
|
||||
const canvas = document.createElement('canvas');
|
||||
return !!(
|
||||
canvas.getContext('webgl') || canvas.getContext('experimental-webgl')
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const injectCpuTamer = () => {
|
||||
if (isGPUAccelerationAvailable()) {
|
||||
injectCpuTamerByAnimationFrame(null);
|
||||
} else {
|
||||
injectCpuTamerByDomMutation(null);
|
||||
}
|
||||
};
|
||||
1
src/plugins/performance-improvement/scripts/rm3/index.ts
Normal file
1
src/plugins/performance-improvement/scripts/rm3/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './rm3';
|
||||
121
src/plugins/performance-improvement/scripts/rm3/rm3.d.ts
vendored
Normal file
121
src/plugins/performance-improvement/scripts/rm3/rm3.d.ts
vendored
Normal file
@ -0,0 +1,121 @@
|
||||
declare class Rm3LinkedArrayNode<T> {
|
||||
value: T;
|
||||
next: Rm3LinkedArrayNode<T> | null;
|
||||
prev: Rm3LinkedArrayNode<T> | null;
|
||||
constructor(value: T);
|
||||
}
|
||||
|
||||
declare class Rm3LinkedArray<T> {
|
||||
head: Rm3LinkedArrayNode<T> | null;
|
||||
tail: Rm3LinkedArrayNode<T> | null;
|
||||
length: number;
|
||||
|
||||
constructor();
|
||||
|
||||
push(value: T): number;
|
||||
pop(): T | undefined;
|
||||
unshift(value: T): number;
|
||||
shift(): T | undefined;
|
||||
size(): number;
|
||||
getNode(index: number): Rm3LinkedArrayNode<T> | null;
|
||||
get(index: number): T | undefined;
|
||||
findNode(value: T): { node: Rm3LinkedArrayNode<T> | null; index: number };
|
||||
toArray(): T[];
|
||||
insertBeforeNode(node: Rm3LinkedArrayNode<T> | null, newValue: T): boolean;
|
||||
insertAfterNode(node: Rm3LinkedArrayNode<T> | null, newValue: T): boolean;
|
||||
insertBefore(existingValue: T, newValue: T): boolean;
|
||||
insertAfter(existingValue: T, newValue: T): boolean;
|
||||
deleteNode(node: Rm3LinkedArrayNode<T>): boolean; // Note: Original JS allowed deleting null, but TS implies non-null here
|
||||
|
||||
static Node: typeof Rm3LinkedArrayNode;
|
||||
}
|
||||
|
||||
// Define the structure of the internal LimitedSizeSet class
|
||||
declare class Rm3LimitedSizeSet<T> extends Set<T> {
|
||||
limit: number;
|
||||
constructor(n: number);
|
||||
add(key: T): this;
|
||||
removeAdd(key: T): void;
|
||||
}
|
||||
|
||||
// Define the structure of the entryRecord tuple used internally
|
||||
// [ WeakRef<HTMLElement>, attached time, detached time, time of change, inside availablePool, reuse count ]
|
||||
type Rm3EntryRecord = [
|
||||
WeakRef<HTMLElement>,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
boolean,
|
||||
number,
|
||||
];
|
||||
|
||||
// Define the interface for the exported rm3 object
|
||||
export interface Rm3 {
|
||||
/**
|
||||
* Removes duplicate values from an array.
|
||||
* @param array The input array.
|
||||
* @returns A new array with unique values.
|
||||
*/
|
||||
uniq: <T>(array: T[]) => T[];
|
||||
|
||||
/**
|
||||
* [Debug only] The current page URL. Only available if DEBUG_OPT was true.
|
||||
*/
|
||||
location?: string;
|
||||
|
||||
/**
|
||||
* [Debug only] Inspects the document for elements with a polymerController and returns their unique node names.
|
||||
* @returns An array of unique node names.
|
||||
*/
|
||||
inspect: () => string[];
|
||||
|
||||
/**
|
||||
* A Set containing records of element operations (attach/detach).
|
||||
* Each record tracks an element's lifecycle state.
|
||||
*/
|
||||
operations: Set<Rm3EntryRecord>;
|
||||
|
||||
/**
|
||||
* A Map where keys are component identifiers (e.g., "creatorTag.componentTag")
|
||||
* and values are LinkedArrays of potentially reusable EntryRecords for detached elements.
|
||||
*/
|
||||
availablePools: Map<string, Rm3LinkedArray<Rm3EntryRecord>>;
|
||||
|
||||
/**
|
||||
* Checks the parent status of elements tracked in the operations set.
|
||||
* Primarily for elements that have been detached (detached time > 0).
|
||||
* @returns An array of tuples: [elementExists: boolean, nodeName: string | undefined, isParentNull: boolean]
|
||||
*/
|
||||
checkWhetherUnderParent: () => [boolean, string | undefined, boolean][];
|
||||
|
||||
/**
|
||||
* Gets a list of unique element tag names (from `element.is`) that have been tracked.
|
||||
* @returns An array of unique tag names.
|
||||
*/
|
||||
hookTags: () => string[];
|
||||
|
||||
/**
|
||||
* [Debug only] A Set containing tags that have had their methods hooked. Only available if DEBUG_OPT was true.
|
||||
*/
|
||||
hookTos?: Set<string>;
|
||||
|
||||
/**
|
||||
* [Debug only] A function that returns an array representation of the reuse record log. Only available if DEBUG_OPT was true.
|
||||
* @returns An array of tuples: [timestamp, tagName, entryRecord]
|
||||
*/
|
||||
reuseRecord?: () => [number, string, Rm3EntryRecord][];
|
||||
|
||||
/**
|
||||
* [Debug only] A Map tracking the reuse count per component tag name. Only available if DEBUG_OPT was true.
|
||||
*/
|
||||
reuseCount_?: Map<string, number>;
|
||||
|
||||
/**
|
||||
* A counter for the total number of times elements have been reused.
|
||||
*/
|
||||
reuseCount: number;
|
||||
}
|
||||
|
||||
export const rm3: Rm3;
|
||||
|
||||
export function injectRm3(): void;
|
||||
828
src/plugins/performance-improvement/scripts/rm3/rm3.js
Normal file
828
src/plugins/performance-improvement/scripts/rm3/rm3.js
Normal file
@ -0,0 +1,828 @@
|
||||
/*
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright 2024-2025 CY Fung
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
export const rm3 = {};
|
||||
|
||||
export const injectRm3 = () => {
|
||||
const DEBUG_OPT = false;
|
||||
const CONFIRM_TIME = 4000;
|
||||
const CHECK_INTERVAL = 400;
|
||||
const DEBUG_dataChangeReflection = true;
|
||||
|
||||
/** @type {globalThis.PromiseConstructor} */
|
||||
const Promise = (async () => {})().constructor; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve.
|
||||
|
||||
// https://qiita.com/piroor/items/02885998c9f76f45bfa0
|
||||
// https://gist.github.com/piroor/829ecb32a52c2a42e5393bbeebe5e63f
|
||||
function uniq(array) {
|
||||
return [...new Set(array)];
|
||||
}
|
||||
|
||||
rm3.uniq = uniq; // [[debug]]
|
||||
DEBUG_OPT && (rm3.location = location.href);
|
||||
|
||||
rm3.inspect = () => {
|
||||
return uniq(
|
||||
[...document.getElementsByTagName('*')]
|
||||
.filter((e) => e?.polymerController?.createComponent_)
|
||||
.map((e) => e.nodeName),
|
||||
); // [[debug]]
|
||||
};
|
||||
|
||||
const insp = (o) => o ? o.polymerController || o.inst || o || 0 : o || 0;
|
||||
const indr = (o) => insp(o).$ || o.$ || 0;
|
||||
|
||||
const getProto = (element) => {
|
||||
if (element) {
|
||||
const cnt = insp(element);
|
||||
return cnt.constructor.prototype || null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const LinkedArray = (() => {
|
||||
class Node {
|
||||
constructor(value) {
|
||||
this.value = value;
|
||||
this.next = null;
|
||||
this.prev = null;
|
||||
}
|
||||
}
|
||||
|
||||
class LinkedArray {
|
||||
constructor() {
|
||||
this.head = null;
|
||||
this.tail = null;
|
||||
this.length = 0;
|
||||
}
|
||||
|
||||
push(value) {
|
||||
const newNode = new Node(value);
|
||||
if (this.length === 0) {
|
||||
this.head = newNode;
|
||||
this.tail = newNode;
|
||||
} else {
|
||||
this.tail.next = newNode;
|
||||
newNode.prev = this.tail;
|
||||
this.tail = newNode;
|
||||
}
|
||||
this.length++;
|
||||
return this.length;
|
||||
}
|
||||
|
||||
pop() {
|
||||
if (this.length === 0) return undefined;
|
||||
const removedNode = this.tail;
|
||||
if (this.length === 1) {
|
||||
this.head = null;
|
||||
this.tail = null;
|
||||
} else {
|
||||
this.tail = removedNode.prev;
|
||||
this.tail.next = null;
|
||||
removedNode.prev = null;
|
||||
}
|
||||
this.length--;
|
||||
return removedNode.value;
|
||||
}
|
||||
|
||||
unshift(value) {
|
||||
const newNode = new Node(value);
|
||||
if (this.length === 0) {
|
||||
this.head = newNode;
|
||||
this.tail = newNode;
|
||||
} else {
|
||||
newNode.next = this.head;
|
||||
this.head.prev = newNode;
|
||||
this.head = newNode;
|
||||
}
|
||||
this.length++;
|
||||
return this.length;
|
||||
}
|
||||
|
||||
shift() {
|
||||
if (this.length === 0) return undefined;
|
||||
const removedNode = this.head;
|
||||
if (this.length === 1) {
|
||||
this.head = null;
|
||||
this.tail = null;
|
||||
} else {
|
||||
this.head = removedNode.next;
|
||||
this.head.prev = null;
|
||||
removedNode.next = null;
|
||||
}
|
||||
this.length--;
|
||||
return removedNode.value;
|
||||
}
|
||||
|
||||
size() {
|
||||
return this.length;
|
||||
}
|
||||
|
||||
// Get a node by index (0-based)
|
||||
getNode(index) {
|
||||
if (index < 0 || index >= this.length) return null;
|
||||
|
||||
let current;
|
||||
let counter;
|
||||
|
||||
// Optimization: start from closest end
|
||||
if (index < this.length / 2) {
|
||||
current = this.head;
|
||||
counter = 0;
|
||||
while (counter !== index) {
|
||||
current = current.next;
|
||||
counter++;
|
||||
}
|
||||
} else {
|
||||
current = this.tail;
|
||||
counter = this.length - 1;
|
||||
while (counter !== index) {
|
||||
current = current.prev;
|
||||
counter--;
|
||||
}
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
// Get value by index
|
||||
get(index) {
|
||||
const node = this.getNode(index);
|
||||
return node ? node.value : undefined;
|
||||
}
|
||||
|
||||
// Find the first node with the given value and return both node and index
|
||||
findNode(value) {
|
||||
let current = this.head;
|
||||
let idx = 0;
|
||||
while (current) {
|
||||
if (current.value === value) {
|
||||
return { node: current, index: idx };
|
||||
}
|
||||
current = current.next;
|
||||
idx++;
|
||||
}
|
||||
return { node: null, index: -1 };
|
||||
}
|
||||
|
||||
toArray() {
|
||||
const arr = [];
|
||||
let current = this.head;
|
||||
while (current) {
|
||||
arr.push(current.value);
|
||||
current = current.next;
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
// Insert a new value before a given node (provided you already have the node reference)
|
||||
insertBeforeNode(node, newValue) {
|
||||
if (!node) {
|
||||
this.unshift(newValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (node === this.head) {
|
||||
// If the target is the head, just unshift
|
||||
this.unshift(newValue);
|
||||
} else {
|
||||
const newNode = new Node(newValue);
|
||||
const prevNode = node.prev;
|
||||
|
||||
prevNode.next = newNode;
|
||||
newNode.prev = prevNode;
|
||||
newNode.next = node;
|
||||
node.prev = newNode;
|
||||
|
||||
this.length++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Insert a new value after a given node (provided you already have the node reference)
|
||||
insertAfterNode(node, newValue) {
|
||||
if (!node) {
|
||||
this.push(newValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (node === this.tail) {
|
||||
// If the target is the tail, just push
|
||||
this.push(newValue);
|
||||
} else {
|
||||
const newNode = new Node(newValue);
|
||||
const nextNode = node.next;
|
||||
|
||||
node.next = newNode;
|
||||
newNode.prev = node;
|
||||
newNode.next = nextNode;
|
||||
nextNode.prev = newNode;
|
||||
|
||||
this.length++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Insert a new value before the first occurrence of an existing value (search by value)
|
||||
insertBefore(existingValue, newValue) {
|
||||
const { node } = this.findNode(existingValue);
|
||||
if (!node) return false; // Not found
|
||||
return this.insertBeforeNode(node, newValue);
|
||||
}
|
||||
|
||||
// Insert a new value after the first occurrence of an existing value (search by value)
|
||||
insertAfter(existingValue, newValue) {
|
||||
const { node } = this.findNode(existingValue);
|
||||
if (!node) return false; // Not found
|
||||
return this.insertAfterNode(node, newValue);
|
||||
}
|
||||
|
||||
// Delete a given node from the list
|
||||
deleteNode(node) {
|
||||
if (!node) return false;
|
||||
|
||||
if (this.length === 1 && node === this.head && node === this.tail) {
|
||||
// Only one element in the list
|
||||
this.head = null;
|
||||
this.tail = null;
|
||||
} else if (node === this.head) {
|
||||
// Node is the head
|
||||
this.head = node.next;
|
||||
this.head.prev = null;
|
||||
node.next = null;
|
||||
} else if (node === this.tail) {
|
||||
// Node is the tail
|
||||
this.tail = node.prev;
|
||||
this.tail.next = null;
|
||||
node.prev = null;
|
||||
} else {
|
||||
// Node is in the middle
|
||||
const prevNode = node.prev;
|
||||
const nextNode = node.next;
|
||||
prevNode.next = nextNode;
|
||||
nextNode.prev = prevNode;
|
||||
node.prev = null;
|
||||
node.next = null;
|
||||
}
|
||||
|
||||
this.length--;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
LinkedArray.Node = Node;
|
||||
return LinkedArray;
|
||||
})();
|
||||
|
||||
class LimitedSizeSet extends Set {
|
||||
constructor(n) {
|
||||
super();
|
||||
this.limit = n;
|
||||
}
|
||||
|
||||
add(key) {
|
||||
if (!super.has(key)) {
|
||||
super.add(key);
|
||||
let n = super.size - this.limit;
|
||||
if (n > 0) {
|
||||
const iterator = super.values();
|
||||
do {
|
||||
const firstKey = iterator.next().value; // Get the first (oldest) key
|
||||
super.delete(firstKey); // Delete the oldest key
|
||||
} while (--n > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeAdd(key) {
|
||||
super.delete(key);
|
||||
this.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!document.createElement9512 &&
|
||||
typeof document.createElement === 'function' &&
|
||||
document.createElement.length === 1
|
||||
) {
|
||||
// sizing of Map / Set. Shall limit ?
|
||||
|
||||
const hookTos = new Set(); // [[debug]]
|
||||
DEBUG_OPT && (rm3.hookTos = hookTos);
|
||||
|
||||
// const reusePool = new Map(); // xx858
|
||||
const entryRecords = new WeakMap(); // a weak link between element and record
|
||||
|
||||
// rm3.list = [];
|
||||
|
||||
const operations = rm3.operations = new Set(); // to find out the "oldest elements"
|
||||
|
||||
const availablePools = rm3.availablePools = new Map(); // those "old elements" can be used
|
||||
let lastTimeCheck = 0;
|
||||
|
||||
const reuseRecord_ = new LimitedSizeSet(256); // [[debug]]
|
||||
const reuseCount_ = new Map();
|
||||
|
||||
let noTimeCheck = false;
|
||||
|
||||
// const defaultValues = new Map();
|
||||
// const noValues = new Map();
|
||||
|
||||
const timeCheck = () => {
|
||||
// regularly check elements are old enough to put into the available pools
|
||||
// note: the characterists of YouTube components are non-volatile. So don't need to waste time to check weakRef.deref() is null or not for removing in operations.
|
||||
|
||||
const ct = Date.now();
|
||||
if (ct - lastTimeCheck < CHECK_INTERVAL || noTimeCheck) return;
|
||||
lastTimeCheck = ct;
|
||||
noTimeCheck = true;
|
||||
|
||||
// 16,777,216
|
||||
if (hookTos.size > 777216) hookTos.clear(); // just debug usage, dont concern
|
||||
if (operations.size > 7777216) {
|
||||
// extremely old elements in operations mean they have no attach/detach action. so no reuse as well. they are just trash in memory.
|
||||
// as no checking of the weakRef.deref() being null or not, those trash could be already cleaned. However we don't concern this.
|
||||
// (not to count whether they are actual memory trash or not)
|
||||
const half = operations.size >>> 1;
|
||||
let i = 0;
|
||||
for (const value of operations) {
|
||||
if (i++ > half) break;
|
||||
operations.delete(value);
|
||||
}
|
||||
}
|
||||
|
||||
// {
|
||||
// // smallest to largest
|
||||
// // past to recent
|
||||
|
||||
// const iterator = operations[Symbol.iterator]();
|
||||
// console.log(1831, '------------------------')
|
||||
// while (true) {
|
||||
// const iteratorResult = iterator.next(); // 順番に値を取りだす
|
||||
// if (iteratorResult.done) break; // 取り出し終えたなら、break
|
||||
// console.log(1835, iteratorResult.value[3])
|
||||
// }
|
||||
|
||||
// console.log(1839, '------------------------')
|
||||
// }
|
||||
|
||||
// Set iterator
|
||||
// s.add(2) s.add(6) s.add(1) s.add(3)
|
||||
// next: 2 -> 6 -> 1 -> 3
|
||||
// op1 (oldest) -> op2 -> op3 -> op4 (latest)
|
||||
const iterator = operations[Symbol.iterator]();
|
||||
|
||||
const targetTime = ct - CONFIRM_TIME;
|
||||
|
||||
const pivotNodes = new WeakMap();
|
||||
|
||||
while (true) {
|
||||
const iteratorResult = iterator.next(); // 順番に値を取りだす
|
||||
if (iteratorResult.done) break; // 取り出し終えたなら、break
|
||||
const entryRecord = iteratorResult.value;
|
||||
if (entryRecord[3] > targetTime) break;
|
||||
|
||||
if (!entryRecord[4] && entryRecord[1] < 0 && entryRecord[2] > 0) {
|
||||
const element = entryRecord[0].deref();
|
||||
const eKey = (element || 0).__rm3Tag003__;
|
||||
if (!eKey) {
|
||||
operations.delete(entryRecord);
|
||||
} else if (
|
||||
element.isConnected === false &&
|
||||
insp(element).isAttached === false
|
||||
) {
|
||||
entryRecord[4] = true;
|
||||
|
||||
let availablePool = availablePools.get(eKey);
|
||||
if (!availablePool)
|
||||
availablePools.set(eKey, availablePool = new LinkedArray());
|
||||
if (!(availablePool instanceof LinkedArray)) throw new Error();
|
||||
DEBUG_OPT &&
|
||||
console.log(3885, 'add key', eKey, availablePools.size);
|
||||
// rm3.showSize = ()=>availablePools.size
|
||||
// setTimeout(()=>{
|
||||
// // window?.euu1 = availablePools
|
||||
// // window?.euu2 = availablePools.size
|
||||
// console.log(availablePools.size)
|
||||
// }, 8000)
|
||||
let pivotNode = pivotNodes.get(availablePool);
|
||||
if (!pivotNode)
|
||||
pivotNodes.set(availablePool, pivotNode = availablePool.head); // cached the previous newest node (head) as pivotNode
|
||||
|
||||
availablePool.insertBeforeNode(pivotNode, entryRecord); // head = newest, tail = oldest
|
||||
}
|
||||
}
|
||||
}
|
||||
noTimeCheck = false;
|
||||
};
|
||||
|
||||
const attachedDefine = function () {
|
||||
Promise.resolve().then(timeCheck);
|
||||
try {
|
||||
const hostElement = this?.hostElement;
|
||||
if (hostElement instanceof HTMLElement) {
|
||||
const entryRecord = entryRecords.get(hostElement);
|
||||
if (
|
||||
entryRecord &&
|
||||
entryRecord[0].deref() === hostElement &&
|
||||
hostElement.isConnected === true &&
|
||||
this?.isAttached === true
|
||||
) {
|
||||
noTimeCheck = true;
|
||||
const ct = Date.now();
|
||||
entryRecord[1] = ct;
|
||||
entryRecord[2] = -1;
|
||||
entryRecord[3] = ct;
|
||||
operations.delete(entryRecord);
|
||||
operations.add(entryRecord);
|
||||
noTimeCheck = false;
|
||||
// note: because of performance prespective, deletion for availablePools[eKey]'s linked element would not be done here.
|
||||
// entryRecord[4] is not required to be updated here.
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
return this.attached9512();
|
||||
};
|
||||
const detachedDefine = function () {
|
||||
Promise.resolve().then(timeCheck);
|
||||
try {
|
||||
const hostElement = this?.hostElement;
|
||||
if (hostElement instanceof HTMLElement) {
|
||||
const entryRecord = entryRecords.get(hostElement);
|
||||
if (
|
||||
entryRecord &&
|
||||
entryRecord[0].deref() === hostElement &&
|
||||
hostElement.isConnected === false &&
|
||||
this?.isAttached === false
|
||||
) {
|
||||
noTimeCheck = true;
|
||||
const ct = Date.now();
|
||||
entryRecord[2] = ct;
|
||||
entryRecord[1] = -1;
|
||||
entryRecord[3] = ct;
|
||||
operations.delete(entryRecord);
|
||||
operations.add(entryRecord);
|
||||
noTimeCheck = false;
|
||||
// note: because of performance prespective, deletion for availablePools[eKey]'s linked element would not be done here.
|
||||
// entryRecord[4] is not required to be updated here.
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
return this.detached9512();
|
||||
};
|
||||
|
||||
// function cpy(x) {
|
||||
// if (!x) return x;
|
||||
// try {
|
||||
// if (typeof x === 'object' && typeof x.length ==='number' && typeof x.slice === 'function') {
|
||||
// x = x.slice(0)
|
||||
// } else if (typeof x === 'object' && !x.length) {
|
||||
// x = JSON.parse(JSON.stringify(x));
|
||||
// } else {
|
||||
// return Object.assign({}, x);
|
||||
// }
|
||||
// } catch (e) { }
|
||||
// return x;
|
||||
// }
|
||||
|
||||
async function digestMessage(message) {
|
||||
const msgUint8 = new TextEncoder().encode(message); // (utf-8 の) Uint8Array にエンコードする
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8); // メッセージをハッシュする
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer)); // バッファーをバイト列に変換する
|
||||
const hashHex = hashArray
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join(''); // バイト列を 16 進文字列に変換する
|
||||
return hashHex.toUpperCase();
|
||||
}
|
||||
|
||||
let onPageContainer = null;
|
||||
|
||||
const createComponentDefine_ = function (a, b, c) {
|
||||
Promise.resolve().then(timeCheck);
|
||||
|
||||
const creatorTag = this?.is || this?.nodeName?.toLowerCase() || '';
|
||||
|
||||
const componentTag = typeof a === 'string' ? a : (a || 0).component || '';
|
||||
|
||||
const eKey =
|
||||
creatorTag && componentTag ? `${creatorTag}.${componentTag}` : '*'; // '*' for play-safe
|
||||
const availablePool = availablePools.get(eKey);
|
||||
|
||||
try {
|
||||
if (availablePool instanceof LinkedArray) {
|
||||
noTimeCheck = true;
|
||||
|
||||
let node = availablePool.tail; // oldest
|
||||
|
||||
while (node instanceof LinkedArray.Node) {
|
||||
const entryRecord = node.value;
|
||||
const prevNode = node.prev;
|
||||
|
||||
let ok = false;
|
||||
let elm = null;
|
||||
if (entryRecord[1] < 0 && entryRecord[2] > 0 && entryRecord[4]) {
|
||||
elm = entryRecord[0].deref();
|
||||
// elm && console.log(3882, (elm.__shady_native_textContent || elm.textContent))
|
||||
if (
|
||||
elm &&
|
||||
elm instanceof HTMLElement &&
|
||||
elm.isConnected === false &&
|
||||
insp(elm).isAttached === false &&
|
||||
elm.parentNode === null
|
||||
) {
|
||||
ok = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
// useEntryRecord = entryRecord;
|
||||
entryRecord[4] = false;
|
||||
// console.log('nodeDeleted', 1, entryRecord[0].deref().nodeName)
|
||||
availablePool.deleteNode(node);
|
||||
// break;
|
||||
|
||||
if (!onPageContainer) {
|
||||
let p = document.createElement('noscript');
|
||||
document.body.prepend(p);
|
||||
onPageContainer = p;
|
||||
}
|
||||
|
||||
onPageContainer.appendChild(elm); // to fix some issues for the rendered elements
|
||||
|
||||
const cnt = insp(elm);
|
||||
|
||||
cnt.__dataInvalid = false;
|
||||
// cnt._initializeProtoProperties(cnt.data)
|
||||
|
||||
// window.meaa = cnt.$.container;
|
||||
if (typeof (cnt.__data || 0) === 'object') {
|
||||
cnt.__data = Object.assign({}, cnt.__data);
|
||||
}
|
||||
cnt.__dataPending = {};
|
||||
cnt.__dataOld = {};
|
||||
|
||||
try {
|
||||
cnt.markDirty();
|
||||
} catch (e) {}
|
||||
try {
|
||||
cnt.markDirtyVisibilityObserver();
|
||||
} catch (e) {}
|
||||
|
||||
try {
|
||||
cnt.wasPrescan = cnt.wasVisible = !1;
|
||||
} catch (e) {}
|
||||
|
||||
// try{
|
||||
// cnt._setPendingProperty('data', Object.assign({}, cntData), !0);
|
||||
// }catch(e){}
|
||||
// try {
|
||||
// cnt._flushProperties();
|
||||
// } catch (e) { }
|
||||
|
||||
if (DEBUG_OPT && DEBUG_dataChangeReflection) {
|
||||
let jC1 = null;
|
||||
let jC2 = null;
|
||||
const jKey = `${Math.floor(Math.random() * 314159265359 + 314159265359).toString(36)}`;
|
||||
try {
|
||||
jC1 =
|
||||
cnt.hostElement.__shady_native_textContent ||
|
||||
cnt.hostElement.textContent;
|
||||
// console.log(83802, jKey, (cnt.hostElement.__shady_native_textContent || cnt.hostElement.textContent))
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
try {
|
||||
jC2 =
|
||||
cnt.hostElement.__shady_native_textContent ||
|
||||
cnt.hostElement.textContent;
|
||||
// console.log(83804, jKey, (cnt.hostElement.__shady_native_textContent || cnt.hostElement.textContent))
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
|
||||
(async () => {
|
||||
jC1 = await digestMessage(jC1);
|
||||
jC2 = await digestMessage(jC2);
|
||||
|
||||
console.log(
|
||||
83804,
|
||||
jKey,
|
||||
jC1.substring(0, 7),
|
||||
jC2.substring(0, 7),
|
||||
);
|
||||
})();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
if (entryRecord[5] < 1e9) entryRecord[5] += 1;
|
||||
DEBUG_OPT &&
|
||||
Promise.resolve().then(() =>
|
||||
console.log(`${eKey} reuse`, entryRecord),
|
||||
); // give some time for attach process
|
||||
DEBUG_OPT && reuseRecord_.add([Date.now(), cnt.is, entryRecord]);
|
||||
DEBUG_OPT &&
|
||||
reuseCount_.set(cnt.is, (reuseCount_.get(cnt.is) || 0) + 1);
|
||||
if (rm3.reuseCount < 1e9) rm3.reuseCount++;
|
||||
|
||||
return elm;
|
||||
}
|
||||
|
||||
// console.log('condi88', entryRecord[1] < 0 , entryRecord[2] > 0 , !!entryRecord[4], !!entryRecord[0].deref())
|
||||
|
||||
entryRecord[4] = false;
|
||||
|
||||
// console.log(entryRecord);
|
||||
// console.log('nodeDeleted',2, entryRecord[0]?.deref()?.nodeName)
|
||||
availablePool.deleteNode(node);
|
||||
node = prevNode;
|
||||
}
|
||||
// for(const ) availablePool
|
||||
// noTimeCheck = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
noTimeCheck = false;
|
||||
|
||||
// console.log('createComponentDefine_', a, b, c)
|
||||
|
||||
// if (!reusePool.has(componentTag)) reusePool.set(componentTag, new LinkedArray()); // xx858
|
||||
|
||||
// const pool = reusePool.get(componentTag); // xx858
|
||||
// if (!(pool instanceof LinkedArray)) throw new Error(); // xx858
|
||||
|
||||
const newElement = this.createComponent9512_(a, b, c);
|
||||
// if(componentTag.indexOf( 'ticker')>=0)console.log(1883, a,newElement)
|
||||
|
||||
try {
|
||||
const cntE = insp(newElement);
|
||||
if (!cntE.attached9512 && cntE.attached) {
|
||||
const cProtoE = getProto(newElement);
|
||||
|
||||
if (cProtoE.attached === cntE.attached) {
|
||||
if (
|
||||
!cProtoE.attached9512 &&
|
||||
typeof cProtoE.attached === 'function' &&
|
||||
cProtoE.attached.length === 0
|
||||
) {
|
||||
cProtoE.attached9512 = cProtoE.attached;
|
||||
|
||||
cProtoE.attached = attachedDefine;
|
||||
// hookTos.add(a);
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
typeof cntE.attached === 'function' &&
|
||||
cntE.attached.length === 3
|
||||
) {
|
||||
cntE.attached9512 = cntE.attached;
|
||||
|
||||
cntE.attached = attachedDefine;
|
||||
// hookTos.add(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!cntE.detached9512 && cntE.detached) {
|
||||
const cProtoE = getProto(newElement);
|
||||
|
||||
if (cProtoE.detached === cntE.detached) {
|
||||
if (
|
||||
!cProtoE.detached9512 &&
|
||||
typeof cProtoE.detached === 'function' &&
|
||||
cProtoE.detached.length === 0
|
||||
) {
|
||||
cProtoE.detached9512 = cProtoE.detached;
|
||||
|
||||
cProtoE.detached = detachedDefine;
|
||||
// hookTos.add(a);
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
typeof cntE.detached === 'function' &&
|
||||
cntE.detached.length === 3
|
||||
) {
|
||||
cntE.detached9512 = cntE.detached;
|
||||
|
||||
cntE.detached = detachedDefine;
|
||||
// hookTos.add(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const acceptance = true;
|
||||
// const acceptance = !cntE.__dataReady && cntE.__dataInvalid !== false; // we might need to change the acceptance condition along with YouTube Coding updates.
|
||||
if (acceptance) {
|
||||
// [[ weak ElementNode, attached time, detached time, time of change, inside availablePool, reuse count ]]
|
||||
const entryRecord = [new WeakRef(newElement), -1, -1, -1, false, 0];
|
||||
|
||||
newElement.__rm3Tag003__ = eKey;
|
||||
entryRecords.set(newElement, entryRecord);
|
||||
} else {
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
return newElement;
|
||||
};
|
||||
|
||||
document.createElement9512 = document.createElement;
|
||||
document.createElement = function (a) {
|
||||
const r = document.createElement9512(a);
|
||||
try {
|
||||
const cnt = insp(r);
|
||||
if (cnt.createComponent_ && !cnt.createComponent9512_) {
|
||||
const cProto = getProto(r);
|
||||
if (cProto.createComponent_ === cnt.createComponent_) {
|
||||
if (
|
||||
!cProto.createComponent9512_ &&
|
||||
typeof cProto.createComponent_ === 'function' &&
|
||||
cProto.createComponent_.length === 3
|
||||
) {
|
||||
cProto.createComponent9512_ = cProto.createComponent_;
|
||||
|
||||
cProto.createComponent_ = createComponentDefine_;
|
||||
DEBUG_OPT && hookTos.add(a);
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
typeof cnt.createComponent_ === 'function' &&
|
||||
cnt.createComponent_.length === 3
|
||||
) {
|
||||
cnt.createComponent9512_ = cnt.createComponent_;
|
||||
|
||||
cnt.createComponent_ = createComponentDefine_;
|
||||
DEBUG_OPT && hookTos.add(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
|
||||
return r;
|
||||
};
|
||||
|
||||
rm3.checkWhetherUnderParent = () => {
|
||||
const r = [];
|
||||
for (const operation of operations) {
|
||||
const elm = operation[0].deref();
|
||||
if (operation[2] > 0) {
|
||||
r.push([
|
||||
!!elm,
|
||||
elm?.nodeName.toLowerCase(),
|
||||
elm?.parentNode === null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
return r;
|
||||
};
|
||||
|
||||
rm3.hookTags = () => {
|
||||
const r = new Set();
|
||||
for (const operation of operations) {
|
||||
const elm = operation[0].deref();
|
||||
if (elm && elm.is) {
|
||||
r.add(elm.is);
|
||||
}
|
||||
}
|
||||
return [...r];
|
||||
};
|
||||
|
||||
DEBUG_OPT &&
|
||||
(rm3.reuseRecord = () => {
|
||||
return [...reuseRecord_]; // [[debug]]
|
||||
});
|
||||
|
||||
DEBUG_OPT && (rm3.reuseCount_ = reuseCount_);
|
||||
}
|
||||
|
||||
rm3.reuseCount = 0;
|
||||
};
|
||||
Reference in New Issue
Block a user