YouTube CPU Tamer by AnimationFrame

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

目前为 2022-08-18 提交的版本。查看 最新版本

// ==UserScript==
// @name         YouTube CPU Tamer by AnimationFrame
// @name:en      YouTube CPU Tamer by AnimationFrame
// @name:ja      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.08.18
// @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:ja  YouTubeビデオのエネルギーインパクトを減らす
// @description:zh-tw  減少YouTube影片所致的能源消耗
// @description:zh-cn  减少YouTube影片所致的能源消耗
// @author       CY Fung
// @match     https://www.youtube.com/*
// @match     https://www.youtube.com/embed/*
// @match     https://www.youtube-nocookie.com/embed/*
// @match     https://www.youtube.com/live_chat*
// @match     https://www.youtube.com/live_chat_replay*
// @match     https://music.youtube.com/*
// @icon         https://www.google.com/s2/favicons?domain=youtube.com
// @run-at      document-start
// @grant       none
// ==/UserScript==

/* jshint esversion:8 */

(function () {
    'use strict';

    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 toResetFuncHandlers = false;

    const [$$requestAnimationFrame, $$setTimeout, $$setInterval, $$clearTimeout, $$clearInterval, sb, rm] = (()=>{
        
      let [window] = new Function('return [window];')(); // real window object
      
      const hkey_script = 'nzsxclvflluv';
      if (window[hkey_script]) throw new Error('Duplicated Userscript Calling'); // avoid duplicated scripting
      window[hkey_script] = true;
      
      // copies of native functions

      /** @type {requestAnimationFrame} */ 
      const $$requestAnimationFrame = window.requestAnimationFrame.bind(window); // core looping
      /** @type {setTimeout} */ 
      const $$setTimeout = window.setTimeout.bind(window); // for race
      /** @type {setInterval} */ 
      const $$setInterval = window.setInterval.bind(window); // for background execution
      /** @type {clearTimeout} */ 
      const $$clearTimeout = window.clearTimeout.bind(window); // for native clearTimeout
      /** @type {clearInterval} */ 
      const $$clearInterval = window.clearInterval.bind(window); // for native clearInterval
      
      
      let mi = INT_INITIAL_VALUE; // skip first {INT_INITIAL_VALUE} cids to avoid browser not yet initialized
      const sb = {};
      let 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;

      
      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.

      window = null;
      sFunc = null;

      return [$$requestAnimationFrame, $$setTimeout, $$setInterval, $$clearTimeout, $$clearInterval, sb, 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 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 
    /** @type {Function|null} */ 
    let interupter = null;
    const infiniteLooper = (resolve) => $$requestAnimationFrame(interupter = resolve); // rAF will not execute if document is hidden
    
    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;
    };
    
    const 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;
            await new Promise(infiniteLooper);
            if (!interupter) {
                // triggered by setInterval
                dexActivePage = false;
            } else {
                // triggered by rAF
                interupter = null;
                if (dexActivePage === false) toResetFuncHandlers = true;
                dexActivePage = true;
            }
            if (toResetFuncHandlers) {
                // true if page change from hidden to visible OR yt-finish
                toResetFuncHandlers = false;
                for (let jb in sb) sb[jb].handler[$busy] = 0; // including the functions with error
            }
            let promisesF = await mbx1();
            await mbx2(promisesF);
            interupter = null; // just ensure no interupter after mbx1 and mbx2
        }
    })();
    
    $$setInterval(() => {
        // no response of requestAnimationFrame; e.g. running in background
        let interupter_t = interupter, now;
        if (interupter_t && (now = Date.now()) > bgExecutionAt) {
            // interupter not triggered by rAF
            bgExecutionAt = now + 230;
            interupter = null;
            interupter_t();
        }
    }, 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.
        
        
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址