// ==UserScript== // @name YouTube CPU Tamer by AnimationFrame // @name:en YouTube CPU Tamer by AnimationFrame // @name:jp YouTube CPU Tamer by AnimationFrame // @name:zh-tw YouTube CPU Tamer by AnimationFrame // @name:zh-cn YouTube CPU Tamer by AnimationFrame // @namespace http://tampermonkey.net/ // @version 2022.04.30.1 // @license MIT License // @description Reduce Browser's Energy Impact for playing YouTube Video // @description:en Reduce Browser's Energy Impact for playing YouTube Video // @description:jp YouTubeビデオのエネルギーインパクトを減らす // @description:zh-tw 減少YouTube影片所致的能源消耗 // @description:zh-cn 减少YouTube影片所致的能源消耗 // @author CY Fung // @include https://www.youtube.com/* // @include https://www.youtube.com/embed/* // @include https://www.youtube-nocookie.com/embed/* // @include https://www.youtube.com/live_chat* // @include https://www.youtube.com/live_chat_replay* // @include https://music.youtube.com/* // @icon https://www.google.com/s2/favicons?domain=youtube.com // @run-at document-start // @grant none // @downloadURL none // ==/UserScript== (function $$() { 'use strict'; const [window, document] = new Function('return [window, document];')(); // real window & document object const hkey_script = 'nzsxclvflluv'; if (window[hkey_script]) return; // avoid duplicated scripting window[hkey_script] = true; //if (!document.documentElement) return window.requestAnimationFrame($$); // not required to check documentElement ready or not // copies of native functions const $$requestAnimationFrame = window.requestAnimationFrame.bind(window); // core looping const $$setTimeout = window.setTimeout.bind(window); // for race const $$setInterval = window.setInterval.bind(window); // for background execution const $$clearTimeout = window.clearTimeout.bind(window); // for native clearTimeout const $$clearInterval = window.clearInterval.bind(window); // for native clearInterval const $busy = Symbol('$busy'); // Number.MAX_SAFE_INTEGER = 9007199254740991 const INT_INITIAL_VALUE = 8192; // 1 ~ {INT_INITIAL_VALUE} are reserved for native setTimeout/setInterval const SAFE_INT_LIMIT = 2251799813685248; // in case cid would be used for multiplying const SAFE_INT_REDUCED = 67108864; // avoid persistent interval handlers with cids between {INT_INITIAL_VALUE + 1} and {SAFE_INT_REDUCED - 1} let mi = INT_INITIAL_VALUE; // skip first {INT_INITIAL_VALUE} cids to avoid browser not yet initialized const sb = {}; const sFunc = (prop) => { return (func, ms, ...args) => { mi++; // start at {INT_INITIAL_VALUE + 1} if (mi > SAFE_INT_LIMIT) mi = SAFE_INT_REDUCED; // just in case let handler = args.length > 0 ? func.bind(null, ...args) : func; // original func if no extra argument handler[$busy] || (handler[$busy] = 0); sb[mi] = { handler , [prop]: ms, // timeout / interval; value can be undefined nextAt: Date.now() + (ms > 0 ? ms : 0) // overload for setTimeout(func); }; return mi; }; }; const rm = function (jd) { if (!jd) return; // native setInterval & setTimeout start from 1 let o = sb[jd]; if (typeof o != 'object') { // to clear the same cid is unlikely to happen || requiring nativeFn is unlikely to happen if (jd <= INT_INITIAL_VALUE) this.nativeFn(jd); // only for clearTimeout & clearInterval return; } for (let k in o) o[k] = null; o = null; sb[jd] = null; delete sb[jd]; }; window.setTimeout = sFunc('timeout'); window.setInterval = sFunc('interval'); window.clearTimeout = rm.bind({ nativeFn: $$clearTimeout }); window.clearInterval = rm.bind({ nativeFn: $$clearInterval }); // window.clearInterval = window.clearTimeout = rm; const delay16ms = (resolve => $$setTimeout(resolve, 16)); const pf = ( handler => new Promise(resolve => { // try catch is not required - no further execution on the handler // For function handler with high energy impact, discard 1st, 2nd, ... (n-1)th calling: (a,b,c,a,b,d,e,f) => (c,a,b,d,e,f) // For function handler with low energy impact, discard or not discard depends on system performance if (handler[$busy] == 1) handler(); handler[$busy]--; handler = null; // remove the reference of `handler` resolve(); resolve = null; // remove the reference of `resolve` }) ); let toResetFuncHandlers = false; let bgExecutionAt = 0; // set at 0 to trigger tf in background startup when requestAnimationFrame is not responsive let dexActivePage = true; // true for default; false when checking triggered by setInterval let interupter = null; const raf = $$requestAnimationFrame.bind(window); const infiniteLooper = (resolve) => { interupter = resolve; if (dexActivePage) raf(resolve); // if dexActivePage = false, the checking will be triggered by setInterval } const mbx1 = async () => { // microTask #1 let now = Date.now(); //bgExecutionAt = now + 160; // if requestAnimationFrame is not responsive (e.g. background running) let promisesF = []; for (let jb in sb) { const o = sb[jb]; let { handler, // timeout, interval , nextAt } = o; if (now < nextAt) continue; handler[$busy]++; promisesF.push(handler); if (interval > 0) { // prevent undefined, zero, negative values const _interval = +interval; // convertion from string to number if necessary; decimal is acceptable if (o.nextAt + _interval > now) o.nextAt += _interval; else if (o.nextAt + 2 * _interval > now) o.nextAt += 2 * _interval; else if (o.nextAt + 3 * _interval > now) o.nextAt += 3 * _interval; else if (o.nextAt + 4 * _interval > now) o.nextAt += 4 * _interval; else if (o.nextAt + 5 * _interval > now) o.nextAt += 5 * _interval; else o.nextAt = now + _interval; } else { // jb in sb must > INT_INITIAL_VALUE rm(jb); // remove timeout } } return promisesF; }; let mbx2 = async (promisesF) => { // microTask #2 //bgExecutionAt = Date.now() + 160; // if requestAnimationFrame is not responsive (e.g. background running) if (promisesF.length == 0) { // no handler functions // requestAnimationFrame when the page is active // execution interval is no less than AnimationFrame } else if (dexActivePage) { let ret2 = new Promise(delay16ms); let ret3 = new Promise(resolveK => { // error would not affect calling the next tick Promise.all(promisesF.map(pf)).then(resolveK); //microTasks promisesF.length = 0; }) let race = Promise.race([ret2, ret3]); // ensure checking function must be called after 16ms to maintain visual changes in high fps. // >16ms examples: repaint/reflow, change of style/content await race; } else { new Promise(resolveK => { // error would not affect calling the next tick promisesF.forEach(pf); //microTasks promisesF.length = 0; }) } }; (async () => { while (true) { bgExecutionAt = Date.now() + 160; let interupterRes = await new Promise(infiniteLooper); interupter = null; if (dexActivePage === false && interupterRes !== true) toResetFuncHandlers = true; dexActivePage = (interupterRes !== true); if (toResetFuncHandlers) { toResetFuncHandlers = false; for (let jb in sb) sb[jb].handler[$busy] = 0; // including the functions with error } let promisesF = await mbx1(); await mbx2(promisesF); } })(); $$setInterval(() => { // no response of requestAnimationFrame; e.g. running in background if (interupter && Date.now() > bgExecutionAt) { interupter(document.hidden ? true : false); // if interupter is not called, wait for the next tick } }, 250); // i.e. 4 times per second for background execution - to keep YouTube application functional // if there is Timer Throttling for background running, the execution become the same as native setTimeout & setInterval. window.addEventListener("yt-navigate-finish", () => { toResetFuncHandlers = true; // ensure all function handlers can be executed after YouTube navigation. }, true); // capturing event - to let it runs before all everything else. // Your code here... })();