YouTube CPU Tamer by AnimationFrame

Reduce Browser's Energy Impact for playing YouTube Video

当前为 2021-09-15 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube CPU Tamer by AnimationFrame
  3. // @name:en YouTube CPU Tamer by AnimationFrame
  4. // @name:jp YouTube CPU Tamer by AnimationFrame
  5. // @name:zh-tw YouTube CPU Tamer by AnimationFrame
  6. // @name:zh-cn YouTube CPU Tamer by AnimationFrame
  7. // @namespace http://tampermonkey.net/
  8. // @version 2021.09.15
  9. // @license MIT License
  10. // @description Reduce Browser's Energy Impact for playing YouTube Video
  11. // @description:en Reduce Browser's Energy Impact for playing YouTube Video
  12. // @description:jp YouTubeビデオのエネルギーインパクトを減らす
  13. // @description:zh-tw 減少YouTube影片所致的能源消耗
  14. // @description:zh-cn 减少YouTube影片所致的能源消耗
  15. // @author CY Fung
  16. // @include https://www.youtube.com/*
  17. // @include https://www.youtube.com/embed/*
  18. // @include https://www.youtube-nocookie.com/embed/*
  19. // @include https://www.youtube.com/live_chat*
  20. // @include https://www.youtube.com/live_chat_replay*
  21. // @icon https://www.google.com/s2/favicons?domain=youtube.com
  22. // @run-at document-start
  23. // @grant none
  24. // ==/UserScript==
  25. (function $$() {
  26. 'use strict';
  27.  
  28. const [window, document] = new Function('return [window, document];')(); // real window & document object
  29.  
  30. const hkey_script = 'nzsxclvflluv';
  31. if (window[hkey_script]) return; // avoid duplicated scripting
  32. window[hkey_script] = true;
  33.  
  34. //if (!document.documentElement) return window.requestAnimationFrame($$); // not required to check documentElement ready or not
  35.  
  36. // copies of native functions
  37. const $$requestAnimationFrame = window.requestAnimationFrame.bind(window); // core looping
  38. const $$setTimeout = window.setTimeout.bind(window); // for race
  39. const $$setInterval = window.setInterval.bind(window); // for background execution
  40. const $$clearTimeout = window.clearTimeout.bind(window); // for native clearTimeout
  41. const $$clearInterval = window.clearInterval.bind(window); // for native clearInterval
  42.  
  43. const $busy = Symbol('$busy');
  44.  
  45. // Number.MAX_SAFE_INTEGER = 9007199254740991
  46.  
  47. const INT_INITIAL_VALUE = 8192; // 1 ~ {INT_INITIAL_VALUE} are reserved for native setTimeout/setInterval
  48. const SAFE_INT_LIMIT = 2251799813685248; // in case cid would be used for multiplying
  49. const SAFE_INT_REDUCED = 67108864; // avoid persistent interval handlers with cids between {INT_INITIAL_VALUE + 1} and {SAFE_INT_REDUCED - 1}
  50.  
  51. let mi = INT_INITIAL_VALUE; // skip first {INT_INITIAL_VALUE} cids to avoid browser not yet initialized
  52. const sb = {};
  53. const sFunc = (prop) => {
  54. return (func, ms, ...args) => {
  55. mi++; // start at {INT_INITIAL_VALUE + 1}
  56. if( mi > SAFE_INT_LIMIT ) mi = SAFE_INT_REDUCED; // just in case
  57. let handler = args.length > 0 ? func.bind(null, ...args) : func; // original func if no extra argument
  58. handler[$busy] || ( handler[$busy] = 0 );
  59. sb[mi] = {
  60. handler,
  61. [prop]: ms, // timeout / interval; value can be undefined
  62. nextAt: Date.now() + (ms > 0 ? ms : 0) // overload for setTimeout(func);
  63. };
  64. return mi;
  65. };
  66. };
  67. const rm = function (jd) {
  68. if (!jd) return; // native setInterval & setTimeout start from 1
  69. let o = sb[jd];
  70. if (typeof o != 'object'){ // to clear the same cid is unlikely to happen || requiring nativeFn is unlikely to happen
  71. if (jd <= INT_INITIAL_VALUE) this.nativeFn(jd); // only for clearTimeout & clearInterval
  72. return;
  73. }
  74. for (let k in o) o[k] = null;
  75. o = null;
  76. sb[jd] = null;
  77. delete sb[jd];
  78. };
  79. window.setTimeout = sFunc('timeout');
  80. window.setInterval = sFunc('interval');
  81. window.clearTimeout = rm.bind({nativeFn: $$clearTimeout});
  82. window.clearInterval = rm.bind({nativeFn: $$clearInterval});
  83. // window.clearInterval = window.clearTimeout = rm;
  84.  
  85.  
  86. const pf = (
  87. handler => new Promise(resolve => {
  88. // try catch is not required - no further execution on the handler
  89. // 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)
  90. // For function handler with low energy impact, discard or not discard depends on system performance
  91. if (handler[$busy] == 1) handler();
  92. handler[$busy]--;
  93. handler = null; // remove the reference of `handler`
  94. resolve();
  95. resolve = null; // remove the reference of `resolve`
  96. })
  97. );
  98.  
  99. let jf, tf, toResetFuncHandlers = false;
  100. let bgExecutionAt = 0; // set at 0 to trigger tf in background startup when requestAnimationFrame is not responsive
  101. tf = () => {
  102. if (toResetFuncHandlers) {
  103. toResetFuncHandlers = false;
  104. for (let jb in sb) sb[jb].handler[$busy] = 0; // including the functions with error
  105. }
  106. new Promise(resolveApp1 => {
  107. // microTask #1
  108. let now = Date.now();
  109. bgExecutionAt = now + 160; // if requestAnimationFrame is not responsive (e.g. background running)
  110. let promisesF = [];
  111. for (let jb in sb) {
  112. const o = sb[jb];
  113. let {
  114. handler,
  115. // timeout,
  116. interval,
  117. nextAt
  118. } = o;
  119. if (now < nextAt) continue;
  120. handler[$busy]++;
  121. promisesF.push(handler);
  122. if (interval > 0) { // prevent undefined, zero, negative values
  123. const _interval = +interval; // convertion from string to number if necessary; decimal is acceptable
  124. if(o.nextAt + _interval > now) o.nextAt += _interval;
  125. else if(o.nextAt + 2*_interval > now) o.nextAt += 2*_interval;
  126. else if(o.nextAt + 3*_interval > now) o.nextAt += 3*_interval;
  127. else if(o.nextAt + 4*_interval > now) o.nextAt += 4*_interval;
  128. else if(o.nextAt + 5*_interval > now) o.nextAt += 5*_interval;
  129. else o.nextAt = now + _interval;
  130. } else {
  131. // jb in sb must > INT_INITIAL_VALUE
  132. rm(jb); // remove timeout
  133. }
  134. }
  135. resolveApp1(promisesF);
  136. }).then(promisesF => {
  137. // microTask #2
  138. bgExecutionAt = Date.now() + 160; // if requestAnimationFrame is not responsive (e.g. background running)
  139. let hidden = document.hidden; // background running would not call requestAnimationFrame
  140. if (promisesF.length == 0) { // no handler functions
  141. // requestAnimationFrame when the page is active
  142. // execution interval is no less than AnimationFrame
  143. return hidden || jf();
  144. }
  145. if (!hidden) {
  146. let ret2 = new Promise(resolve => $$setTimeout(resolve, 16));
  147. let promises = promisesF.map(pf); //microTasks
  148. let ret1 = Promise.all(promises);
  149. let race = Promise.race([ret1, ret2]);
  150. // ensure jf must be called after 16ms to maintain visual changes in high fps.
  151. // >16ms examples: repaint/reflow, change of style/content
  152. race.then(jf);
  153. promises.length = 0;
  154. } else {
  155. promisesF.forEach(pf);
  156. }
  157. promisesF.length = 0;
  158. })
  159. };
  160. (jf = $$requestAnimationFrame.bind(window, tf))();
  161.  
  162. $$setInterval(() => {
  163. // no response of requestAnimationFrame; e.g. running in background
  164. // toResetFuncHandlers = true;
  165. if (Date.now() > bgExecutionAt) {
  166. toResetFuncHandlers = true;
  167. tf();
  168. }
  169. }, 250);
  170. // i.e. 4 times per second for background execution - to keep YouTube application functional
  171. // if there is Timer Throttling for background running, the execution become the same as native setTimeout & setInterval.
  172.  
  173. window.addEventListener("yt-navigate-finish", () => {
  174. toResetFuncHandlers = true; // ensure all function handlers can be executed after YouTube navigation.
  175. }, true); // capturing event - to let it runs before all everything else.
  176.  
  177. // Your code here...
  178. })();