Greasy Fork

YouTube CPU Tamer by AnimationFrame

减少YouTube影片所致的能源消耗

目前为 2021-09-30 提交的版本。查看 最新版本

// ==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     2021.09.30
// @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
// ==/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 jf, tf, toResetFuncHandlers = false;
    let bgExecutionAt = 0; // set at 0 to trigger tf in background startup when requestAnimationFrame is not responsive
    tf = () => {
        if (toResetFuncHandlers) {
            toResetFuncHandlers = false;
            for (let jb in sb) sb[jb].handler[$busy] = 0; // including the functions with error
        }
        new Promise(resolveApp1 => {
            // 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
                }
            }
            resolveApp1(promisesF);
        }).then(promisesF => {
            // microTask #2
            bgExecutionAt = Date.now() + 160; // if requestAnimationFrame is not responsive (e.g. background running)
            let hidden = document.hidden; // background running would not call requestAnimationFrame
            if (promisesF.length == 0) { // no handler functions
                // requestAnimationFrame when the page is active
                // execution interval is no less than AnimationFrame
                return hidden || jf();
            }
            if (!hidden) {
                let ret2 = new Promise(delay16ms);
                let promises = promisesF.map(pf); //microTasks
                let ret1 = Promise.all(promises);
                let race = Promise.race([ret1, ret2]);
                // ensure jf must be called after 16ms to maintain visual changes in high fps.
                // >16ms examples: repaint/reflow, change of style/content
                race.then(jf);
                promises.length = 0;
            } else {
                promisesF.forEach(pf);
            }
            promisesF.length = 0;
        });
    };
    (jf = $$requestAnimationFrame.bind(window, tf))();

    $$setInterval(() => {
        // no response of requestAnimationFrame; e.g. running in background
        // toResetFuncHandlers = true;
        if (Date.now() > bgExecutionAt) {
            toResetFuncHandlers = true;
            tf();
        }
    }, 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...
})();