From 1c764158461da414cea8bf34c3b514f1f98d7adf Mon Sep 17 00:00:00 2001 From: JellyBrick Date: Sun, 27 Apr 2025 04:23:19 +0900 Subject: [PATCH] feat(performance-improvement): added "performance improvement" plugin --- src/i18n/resources/en.json | 4 + src/plugins/performance-improvement/index.ts | 19 + .../cpu-tamer-by-animationframe.d.ts | 3 + .../cpu-tamer/cpu-tamer-by-animationframe.js | 286 ++++++ .../cpu-tamer/cpu-tamer-by-dom-mutation.d.ts | 3 + .../cpu-tamer/cpu-tamer-by-dom-mutation.js | 281 ++++++ .../scripts/cpu-tamer/index.ts | 22 + .../scripts/rm3/index.ts | 1 + .../scripts/rm3/rm3.d.ts | 121 +++ .../scripts/rm3/rm3.js | 828 ++++++++++++++++++ 10 files changed, 1568 insertions(+) create mode 100644 src/plugins/performance-improvement/index.ts create mode 100644 src/plugins/performance-improvement/scripts/cpu-tamer/cpu-tamer-by-animationframe.d.ts create mode 100644 src/plugins/performance-improvement/scripts/cpu-tamer/cpu-tamer-by-animationframe.js create mode 100644 src/plugins/performance-improvement/scripts/cpu-tamer/cpu-tamer-by-dom-mutation.d.ts create mode 100644 src/plugins/performance-improvement/scripts/cpu-tamer/cpu-tamer-by-dom-mutation.js create mode 100644 src/plugins/performance-improvement/scripts/cpu-tamer/index.ts create mode 100644 src/plugins/performance-improvement/scripts/rm3/index.ts create mode 100644 src/plugins/performance-improvement/scripts/rm3/rm3.d.ts create mode 100644 src/plugins/performance-improvement/scripts/rm3/rm3.js diff --git a/src/i18n/resources/en.json b/src/i18n/resources/en.json index 824a681e..57831468 100644 --- a/src/i18n/resources/en.json +++ b/src/i18n/resources/en.json @@ -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": { diff --git a/src/plugins/performance-improvement/index.ts b/src/plugins/performance-improvement/index.ts new file mode 100644 index 00000000..4b795ead --- /dev/null +++ b/src/plugins/performance-improvement/index.ts @@ -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(); + }, +}); diff --git a/src/plugins/performance-improvement/scripts/cpu-tamer/cpu-tamer-by-animationframe.d.ts b/src/plugins/performance-improvement/scripts/cpu-tamer/cpu-tamer-by-animationframe.d.ts new file mode 100644 index 00000000..8b2e0735 --- /dev/null +++ b/src/plugins/performance-improvement/scripts/cpu-tamer/cpu-tamer-by-animationframe.d.ts @@ -0,0 +1,3 @@ +export declare const injectCpuTamerByAnimationFrame: ( + __CONTEXT__: unknown, +) => void; diff --git a/src/plugins/performance-improvement/scripts/cpu-tamer/cpu-tamer-by-animationframe.js b/src/plugins/performance-improvement/scripts/cpu-tamer/cpu-tamer-by-animationframe.js new file mode 100644 index 00000000..bffcc977 --- /dev/null +++ b/src/plugins/performance-improvement/scripts/cpu-tamer/cpu-tamer-by-animationframe.js @@ -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); + }); + +}); diff --git a/src/plugins/performance-improvement/scripts/cpu-tamer/cpu-tamer-by-dom-mutation.d.ts b/src/plugins/performance-improvement/scripts/cpu-tamer/cpu-tamer-by-dom-mutation.d.ts new file mode 100644 index 00000000..2d5da8cc --- /dev/null +++ b/src/plugins/performance-improvement/scripts/cpu-tamer/cpu-tamer-by-dom-mutation.d.ts @@ -0,0 +1,3 @@ +export declare const injectCpuTamerByDomMutation: ( + __CONTEXT__: unknown, +) => void; diff --git a/src/plugins/performance-improvement/scripts/cpu-tamer/cpu-tamer-by-dom-mutation.js b/src/plugins/performance-improvement/scripts/cpu-tamer/cpu-tamer-by-dom-mutation.js new file mode 100644 index 00000000..cc5a9fbd --- /dev/null +++ b/src/plugins/performance-improvement/scripts/cpu-tamer/cpu-tamer-by-dom-mutation.js @@ -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); + */ + }); + +}); diff --git a/src/plugins/performance-improvement/scripts/cpu-tamer/index.ts b/src/plugins/performance-improvement/scripts/cpu-tamer/index.ts new file mode 100644 index 00000000..e2be37c5 --- /dev/null +++ b/src/plugins/performance-improvement/scripts/cpu-tamer/index.ts @@ -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); + } +}; diff --git a/src/plugins/performance-improvement/scripts/rm3/index.ts b/src/plugins/performance-improvement/scripts/rm3/index.ts new file mode 100644 index 00000000..fb527c15 --- /dev/null +++ b/src/plugins/performance-improvement/scripts/rm3/index.ts @@ -0,0 +1 @@ +export * from './rm3'; diff --git a/src/plugins/performance-improvement/scripts/rm3/rm3.d.ts b/src/plugins/performance-improvement/scripts/rm3/rm3.d.ts new file mode 100644 index 00000000..9a1ae61c --- /dev/null +++ b/src/plugins/performance-improvement/scripts/rm3/rm3.d.ts @@ -0,0 +1,121 @@ +declare class Rm3LinkedArrayNode { + value: T; + next: Rm3LinkedArrayNode | null; + prev: Rm3LinkedArrayNode | null; + constructor(value: T); +} + +declare class Rm3LinkedArray { + head: Rm3LinkedArrayNode | null; + tail: Rm3LinkedArrayNode | null; + length: number; + + constructor(); + + push(value: T): number; + pop(): T | undefined; + unshift(value: T): number; + shift(): T | undefined; + size(): number; + getNode(index: number): Rm3LinkedArrayNode | null; + get(index: number): T | undefined; + findNode(value: T): { node: Rm3LinkedArrayNode | null; index: number }; + toArray(): T[]; + insertBeforeNode(node: Rm3LinkedArrayNode | null, newValue: T): boolean; + insertAfterNode(node: Rm3LinkedArrayNode | null, newValue: T): boolean; + insertBefore(existingValue: T, newValue: T): boolean; + insertAfter(existingValue: T, newValue: T): boolean; + deleteNode(node: Rm3LinkedArrayNode): 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 extends Set { + limit: number; + constructor(n: number); + add(key: T): this; + removeAdd(key: T): void; +} + +// Define the structure of the entryRecord tuple used internally +// [ WeakRef, attached time, detached time, time of change, inside availablePool, reuse count ] +type Rm3EntryRecord = [ + WeakRef, + 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: (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; + + /** + * A Map where keys are component identifiers (e.g., "creatorTag.componentTag") + * and values are LinkedArrays of potentially reusable EntryRecords for detached elements. + */ + availablePools: Map>; + + /** + * 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; + + /** + * [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; + + /** + * A counter for the total number of times elements have been reused. + */ + reuseCount: number; +} + +export const rm3: Rm3; + +export function injectRm3(): void; diff --git a/src/plugins/performance-improvement/scripts/rm3/rm3.js b/src/plugins/performance-improvement/scripts/rm3/rm3.js new file mode 100644 index 00000000..1d82e3a6 --- /dev/null +++ b/src/plugins/performance-improvement/scripts/rm3/rm3.js @@ -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; +};