YouTube CPU Tamer by AnimationFrame

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

当前为 2022-12-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.12.14
  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. // @exclude /^https?://\S+\.(txt|png|jpg|jpeg|gif|xml|svg|manifest|log|ini)[^\/]*$/
  23. // @icon https://raw.githubusercontent.com/cyfung1031/userscript-supports/main/icons/youtube-cpu-tamper-by-animationframe.webp
  24. // @run-at document-start
  25. // @grant none
  26. // @unwrap
  27. // @allFrames
  28. // @inject-into page
  29. // ==/UserScript==
  30.  
  31. /* jshint esversion:8 */
  32.  
  33. (function () {
  34. 'use strict';
  35.  
  36. const $busy = Symbol('$busy');
  37.  
  38. // Number.MAX_SAFE_INTEGER = 9007199254740991
  39.  
  40. const INT_INITIAL_VALUE = 8192; // 1 ~ {INT_INITIAL_VALUE} are reserved for native setTimeout/setInterval
  41. const SAFE_INT_LIMIT = 2251799813685248; // in case cid would be used for multiplying
  42. const SAFE_INT_REDUCED = 67108864; // avoid persistent interval handlers with cids between {INT_INITIAL_VALUE + 1} and {SAFE_INT_REDUCED - 1}
  43.  
  44. let toResetFuncHandlers = false;
  45.  
  46. const [$$requestAnimationFrame, $$setTimeout, $$setInterval, $$clearTimeout, $$clearInterval, sb, rm] = (()=>{
  47.  
  48. let [window] = new Function('return [window];')(); // real window object
  49.  
  50. const hkey_script = 'nzsxclvflluv';
  51. if (window[hkey_script]) throw new Error('Duplicated Userscript Calling'); // avoid duplicated scripting
  52. window[hkey_script] = true;
  53.  
  54. // copies of native functions
  55.  
  56. /** @type {requestAnimationFrame} */
  57. const $$requestAnimationFrame = window.requestAnimationFrame.bind(window); // core looping
  58. /** @type {setTimeout} */
  59. const $$setTimeout = window.setTimeout.bind(window); // for race
  60. /** @type {setInterval} */
  61. const $$setInterval = window.setInterval.bind(window); // for background execution
  62. /** @type {clearTimeout} */
  63. const $$clearTimeout = window.clearTimeout.bind(window); // for native clearTimeout
  64. /** @type {clearInterval} */
  65. const $$clearInterval = window.clearInterval.bind(window); // for native clearInterval
  66.  
  67.  
  68. let mi = INT_INITIAL_VALUE; // skip first {INT_INITIAL_VALUE} cids to avoid browser not yet initialized
  69. /** @type { Map<number, object> } */
  70. const sb = new Map();
  71. let sFunc = (prop) => {
  72. return (func, ms, ...args) => {
  73. mi++; // start at {INT_INITIAL_VALUE + 1}
  74. if (mi > SAFE_INT_LIMIT) mi = SAFE_INT_REDUCED; // just in case
  75. let handler = args.length > 0 ? func.bind(null, ...args) : func; // original func if no extra argument
  76. handler[$busy] || (handler[$busy] = 0);
  77. sb.set(mi, {
  78. handler,
  79. [prop]: ms, // timeout / interval; value can be undefined
  80. nextAt: Date.now() + (ms > 0 ? ms : 0) // overload for setTimeout(func);
  81. });
  82. return mi;
  83. };
  84. };
  85. const rm = function (jd) {
  86. if (!jd) return; // native setInterval & setTimeout start from 1
  87. let o = sb.get(jd);
  88. if (typeof o !== 'object') { // to clear the same cid is unlikely to happen || requiring nativeFn is unlikely to happen
  89. if (jd <= INT_INITIAL_VALUE) this.nativeFn(jd); // only for clearTimeout & clearInterval
  90. }else{
  91. for (let k in o) o[k] = null;
  92. o = null;
  93. sb.delete(jd);
  94. }
  95. };
  96. window.setTimeout = sFunc('timeout');
  97. window.setInterval = sFunc('interval');
  98. window.clearTimeout = rm.bind({
  99. nativeFn: $$clearTimeout
  100. });
  101. window.clearInterval = rm.bind({
  102. nativeFn: $$clearInterval
  103. });
  104. try {
  105. window.setTimeout.toString = $$setTimeout.toString.bind($$setTimeout)
  106. window.setInterval.toString = $$setInterval.toString.bind($$setInterval)
  107. window.clearTimeout.toString = $$clearTimeout.toString.bind($$clearTimeout)
  108. window.clearInterval.toString = $$clearInterval.toString.bind($$clearInterval)
  109. } catch (e) { console.warn(e) }
  110.  
  111. window.addEventListener("yt-navigate-finish", () => {
  112. toResetFuncHandlers = true; // ensure all function handlers can be executed after YouTube navigation.
  113. }, true); // capturing event - to let it runs before all everything else.
  114.  
  115. window = null;
  116. sFunc = null;
  117.  
  118. return [$$requestAnimationFrame, $$setTimeout, $$setInterval, $$clearTimeout, $$clearInterval, sb, rm];
  119.  
  120. })();
  121.  
  122. let nonResponsiveResolve = null
  123. const delayNonResponsive = (resolve) => (nonResponsiveResolve = resolve);
  124.  
  125. const pf = (
  126. handler => new Promise(resolve => {
  127. // try catch is not required - no further execution on the handler
  128. // 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)
  129. // For function handler with low energy impact, discard or not discard depends on system performance
  130. if (handler[$busy] === 1) handler();
  131. handler[$busy]--;
  132. handler = null; // remove the reference of `handler`
  133. resolve();
  134. resolve = null; // remove the reference of `resolve`
  135. })
  136. );
  137.  
  138. let bgExecutionAt = 0; // set at 0 to trigger tf in background startup when requestAnimationFrame is not responsive
  139.  
  140. let dexActivePage = true; // true for default; false when checking triggered by setInterval
  141. /** @type {Function|null} */
  142. let interupter = null;
  143. const infiniteLooper = (resolve) => $$requestAnimationFrame(interupter = resolve); // rAF will not execute if document is hidden
  144.  
  145. const mbx = async () => {
  146.  
  147. // microTask #1
  148. let now = Date.now();
  149. // bgExecutionAt = now + 160; // if requestAnimationFrame is not responsive (e.g. background running)
  150. let promisesF = [];
  151. const lsb = sb;
  152. for (const jb of lsb.keys()) {
  153. const o = lsb.get(jb);
  154. const {
  155. handler,
  156. // timeout,
  157. interval,
  158. nextAt
  159. } = o;
  160. if (now < nextAt) continue;
  161. handler[$busy]++;
  162. promisesF.push(handler);
  163. if (interval > 0) { // prevent undefined, zero, negative values
  164. const _interval = +interval; // convertion from string to number if necessary; decimal is acceptable
  165. if (nextAt + _interval > now) o.nextAt += _interval;
  166. else if (nextAt + 2 * _interval > now) o.nextAt += 2 * _interval;
  167. else if (nextAt + 3 * _interval > now) o.nextAt += 3 * _interval;
  168. else if (nextAt + 4 * _interval > now) o.nextAt += 4 * _interval;
  169. else if (nextAt + 5 * _interval > now) o.nextAt += 5 * _interval;
  170. else o.nextAt = now + _interval;
  171. } else {
  172. // jb in sb must > INT_INITIAL_VALUE
  173. rm(jb); // remove timeout
  174. }
  175. }
  176.  
  177. await Promise.resolve(0); // split microTasks inside async()
  178.  
  179. // microTask #2
  180. // bgExecutionAt = Date.now() + 160; // if requestAnimationFrame is not responsive (e.g. background running)
  181. if (promisesF.length === 0) { // no handler functions
  182. // requestAnimationFrame when the page is active
  183. // execution interval is no less than AnimationFrame
  184. promisesF = null;
  185. } else if (dexActivePage) {
  186. // ret3: looping for all functions. First error leads resolve non-reachable;
  187. // the particular [$busy] will not reset to 0 normally
  188. let ret3 = new Promise(resolveK => {
  189. // error would not affect calling the next tick
  190. Promise.all(promisesF.map(pf)).then(resolveK); //microTasks
  191. promisesF.length = 0;
  192. promisesF = null;
  193. })
  194. let ret2 = new Promise(delayNonResponsive);
  195. // for every 125ms, ret2 probably resolve eariler than ret3
  196. // however it still be controlled by rAF (or 250ms) in the next looping
  197. let race = Promise.race([ret2, ret3]);
  198. // continue either 125ms time limit reached or all working functions have been done
  199. await race;
  200. nonResponsiveResolve = null;
  201. } else {
  202. new Promise(resolveK => {
  203. // error would not affect calling the next tick
  204. promisesF.forEach(pf); //microTasks
  205. promisesF.length = 0;
  206. promisesF = null;
  207. })
  208. }
  209.  
  210. };
  211.  
  212. (async () => {
  213. while (true) {
  214. bgExecutionAt = Date.now() + 160;
  215. await new Promise(infiniteLooper);
  216. if (interupter === null) {
  217. // triggered by setInterval
  218. dexActivePage = false;
  219. } else {
  220. // triggered by rAF
  221. interupter = null;
  222. if (dexActivePage === false) toResetFuncHandlers = true;
  223. dexActivePage = true;
  224. }
  225. if (toResetFuncHandlers) {
  226. // true if page change from hidden to visible OR yt-finish
  227. toResetFuncHandlers = false;
  228. for (let eb of sb.values()) eb.handler[$busy] = 0; // including the functions with error
  229. }
  230. await mbx();
  231. }
  232. })();
  233.  
  234. $$setInterval(() => {
  235. if (nonResponsiveResolve !== null) {
  236. nonResponsiveResolve();
  237. return;
  238. }
  239. // no response of requestAnimationFrame; e.g. running in background
  240. let interupter_t = interupter, now;
  241. if (interupter_t !== null && (now = Date.now()) > bgExecutionAt) {
  242. // interupter not triggered by rAF
  243. bgExecutionAt = now + 230;
  244. interupter = null;
  245. interupter_t();
  246. }
  247. }, 125);
  248. // --- 2022.12.14 ---
  249. // 125ms for race promise 'nonResponsiveResolve' only; interupter still works with interval set by bgExecutionAt
  250. // Timer Throttling might be more serious since 125ms is used instead of 250ms
  251. // ---------------------
  252. // 4 times per second for background execution - to keep YouTube application functional
  253. // if there is Timer Throttling for background running, the execution become the same as native setTimeout & setInterval.
  254.  
  255.  
  256. })();