YouTube CPU Tamer by AnimationFrame

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

当前为 2022-11-25 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube CPU Tamer by AnimationFrame
  3. // @name:en YouTube CPU Tamer by AnimationFrame
  4. // @name:ja 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 2022.11.26
  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:ja YouTubeビデオのエネルギーインパクトを減らす
  13. // @description:zh-TW 減少YouTube影片所致的能源消耗
  14. // @description:zh-CN 减少YouTube影片所致的能源消耗
  15. // @author CY Fung
  16. // @match https://www.youtube.com/*
  17. // @match https://www.youtube.com/embed/*
  18. // @match https://www.youtube-nocookie.com/embed/*
  19. // @match https://www.youtube.com/live_chat*
  20. // @match https://www.youtube.com/live_chat_replay*
  21. // @match https://music.youtube.com/*
  22. // @icon https://www.google.com/s2/favicons?domain=youtube.com
  23. // @run-at document-start
  24. // @grant none
  25. // ==/UserScript==
  26.  
  27. /* jshint esversion:8 */
  28.  
  29. (function () {
  30. 'use strict';
  31.  
  32. const $busy = Symbol('$busy');
  33.  
  34. // Number.MAX_SAFE_INTEGER = 9007199254740991
  35.  
  36. const INT_INITIAL_VALUE = 8192; // 1 ~ {INT_INITIAL_VALUE} are reserved for native setTimeout/setInterval
  37. const SAFE_INT_LIMIT = 2251799813685248; // in case cid would be used for multiplying
  38. const SAFE_INT_REDUCED = 67108864; // avoid persistent interval handlers with cids between {INT_INITIAL_VALUE + 1} and {SAFE_INT_REDUCED - 1}
  39.  
  40. let toResetFuncHandlers = false;
  41.  
  42. const [$$requestAnimationFrame, $$setTimeout, $$setInterval, $$clearTimeout, $$clearInterval, sb, rm] = (()=>{
  43.  
  44. let [window] = new Function('return [window];')(); // real window object
  45.  
  46. const hkey_script = 'nzsxclvflluv';
  47. if (window[hkey_script]) throw new Error('Duplicated Userscript Calling'); // avoid duplicated scripting
  48. window[hkey_script] = true;
  49.  
  50. // copies of native functions
  51.  
  52. /** @type {requestAnimationFrame} */
  53. const $$requestAnimationFrame = window.requestAnimationFrame.bind(window); // core looping
  54. /** @type {setTimeout} */
  55. const $$setTimeout = window.setTimeout.bind(window); // for race
  56. /** @type {setInterval} */
  57. const $$setInterval = window.setInterval.bind(window); // for background execution
  58. /** @type {clearTimeout} */
  59. const $$clearTimeout = window.clearTimeout.bind(window); // for native clearTimeout
  60. /** @type {clearInterval} */
  61. const $$clearInterval = window.clearInterval.bind(window); // for native clearInterval
  62.  
  63.  
  64. let mi = INT_INITIAL_VALUE; // skip first {INT_INITIAL_VALUE} cids to avoid browser not yet initialized
  65. /** @type { Map<number, object> } */
  66. const sb = new Map();
  67. let sFunc = (prop) => {
  68. return (func, ms, ...args) => {
  69. mi++; // start at {INT_INITIAL_VALUE + 1}
  70. if (mi > SAFE_INT_LIMIT) mi = SAFE_INT_REDUCED; // just in case
  71. let handler = args.length > 0 ? func.bind(null, ...args) : func; // original func if no extra argument
  72. handler[$busy] || (handler[$busy] = 0);
  73. sb.set(mi, {
  74. handler,
  75. [prop]: ms, // timeout / interval; value can be undefined
  76. nextAt: Date.now() + (ms > 0 ? ms : 0) // overload for setTimeout(func);
  77. });
  78. return mi;
  79. };
  80. };
  81. const rm = function (jd) {
  82. if (!jd) return; // native setInterval & setTimeout start from 1
  83. let o = sb.get(jd);
  84. if (typeof o !== 'object') { // to clear the same cid is unlikely to happen || requiring nativeFn is unlikely to happen
  85. if (jd <= INT_INITIAL_VALUE) this.nativeFn(jd); // only for clearTimeout & clearInterval
  86. }else{
  87. for (let k in o) o[k] = null;
  88. o = null;
  89. sb.delete(jd);
  90. }
  91. };
  92. window.setTimeout = sFunc('timeout');
  93. window.setInterval = sFunc('interval');
  94. window.clearTimeout = rm.bind({
  95. nativeFn: $$clearTimeout
  96. });
  97. window.clearInterval = rm.bind({
  98. nativeFn: $$clearInterval
  99. });
  100. // window.clearInterval = window.clearTimeout = rm;
  101.  
  102.  
  103. window.addEventListener("yt-navigate-finish", () => {
  104. toResetFuncHandlers = true; // ensure all function handlers can be executed after YouTube navigation.
  105. }, true); // capturing event - to let it runs before all everything else.
  106.  
  107. window = null;
  108. sFunc = null;
  109.  
  110. return [$$requestAnimationFrame, $$setTimeout, $$setInterval, $$clearTimeout, $$clearInterval, sb, rm];
  111.  
  112. })();
  113.  
  114. const delay16ms = (resolve => $$setTimeout(resolve, 16));
  115.  
  116. const pf = (
  117. handler => new Promise(resolve => {
  118. // try catch is not required - no further execution on the handler
  119. // 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)
  120. // For function handler with low energy impact, discard or not discard depends on system performance
  121. if (handler[$busy] === 1) handler();
  122. handler[$busy]--;
  123. handler = null; // remove the reference of `handler`
  124. resolve();
  125. resolve = null; // remove the reference of `resolve`
  126. })
  127. );
  128.  
  129. let bgExecutionAt = 0; // set at 0 to trigger tf in background startup when requestAnimationFrame is not responsive
  130.  
  131. let dexActivePage = true; // true for default; false when checking triggered by setInterval
  132. /** @type {Function|null} */
  133. let interupter = null;
  134. const infiniteLooper = (resolve) => $$requestAnimationFrame(interupter = resolve); // rAF will not execute if document is hidden
  135.  
  136. const mbx = async () => {
  137.  
  138. // microTask #1
  139. let now = Date.now();
  140. // bgExecutionAt = now + 160; // if requestAnimationFrame is not responsive (e.g. background running)
  141. let promisesF = [];
  142. const lsb = sb;
  143. for (let jb of lsb.keys()) {
  144. const o = lsb.get(jb);
  145. let {
  146. handler,
  147. // timeout,
  148. interval,
  149. nextAt
  150. } = o;
  151. if (now < nextAt) continue;
  152. handler[$busy]++;
  153. promisesF.push(handler);
  154. if (interval > 0) { // prevent undefined, zero, negative values
  155. const _interval = +interval; // convertion from string to number if necessary; decimal is acceptable
  156. if (o.nextAt + _interval > now) o.nextAt += _interval;
  157. else if (o.nextAt + 2 * _interval > now) o.nextAt += 2 * _interval;
  158. else if (o.nextAt + 3 * _interval > now) o.nextAt += 3 * _interval;
  159. else if (o.nextAt + 4 * _interval > now) o.nextAt += 4 * _interval;
  160. else if (o.nextAt + 5 * _interval > now) o.nextAt += 5 * _interval;
  161. else o.nextAt = now + _interval;
  162. } else {
  163. // jb in sb must > INT_INITIAL_VALUE
  164. rm(jb); // remove timeout
  165. }
  166. }
  167.  
  168. await Promise.resolve(0); // split microTasks inside async()
  169.  
  170. // microTask #2
  171. // bgExecutionAt = Date.now() + 160; // if requestAnimationFrame is not responsive (e.g. background running)
  172. if (promisesF.length === 0) { // no handler functions
  173. // requestAnimationFrame when the page is active
  174. // execution interval is no less than AnimationFrame
  175. promisesF = null;
  176. } else if (dexActivePage) {
  177. let ret2 = new Promise(delay16ms);
  178. let ret3 = new Promise(resolveK => {
  179. // error would not affect calling the next tick
  180. Promise.all(promisesF.map(pf)).then(resolveK); //microTasks
  181. promisesF.length = 0;
  182. promisesF = null;
  183. })
  184. let race = Promise.race([ret2, ret3]);
  185. // ensure checking function must be called after 16ms to maintain visual changes in high fps.
  186. // >16ms examples: repaint/reflow, change of style/content
  187. await race;
  188. } else {
  189. new Promise(resolveK => {
  190. // error would not affect calling the next tick
  191. promisesF.forEach(pf); //microTasks
  192. promisesF.length = 0;
  193. promisesF = null;
  194. })
  195. }
  196.  
  197. };
  198.  
  199. (async () => {
  200. while (true) {
  201. bgExecutionAt = Date.now() + 160;
  202. await new Promise(infiniteLooper);
  203. if (interupter === null) {
  204. // triggered by setInterval
  205. dexActivePage = false;
  206. } else {
  207. // triggered by rAF
  208. interupter = null;
  209. if (dexActivePage === false) toResetFuncHandlers = true;
  210. dexActivePage = true;
  211. }
  212. if (toResetFuncHandlers) {
  213. // true if page change from hidden to visible OR yt-finish
  214. toResetFuncHandlers = false;
  215. for (let eb of sb.values()) eb.handler[$busy] = 0; // including the functions with error
  216. }
  217. await mbx();
  218. }
  219. })();
  220.  
  221. $$setInterval(() => {
  222. // no response of requestAnimationFrame; e.g. running in background
  223. let interupter_t = interupter, now;
  224. if (interupter_t && (now = Date.now()) > bgExecutionAt) {
  225. // interupter not triggered by rAF
  226. bgExecutionAt = now + 230;
  227. interupter = null;
  228. interupter_t();
  229. }
  230. }, 250);
  231. // i.e. 4 times per second for background execution - to keep YouTube application functional
  232. // if there is Timer Throttling for background running, the execution become the same as native setTimeout & setInterval.
  233.  
  234.  
  235. })();