YouTube CPU Tamer by AnimationFrame

Reduce Browser's Energy Impact for playing YouTube Video

当前为 2022-08-14 提交的版本,查看 最新版本

  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 2022.08.14.4
  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. // @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. const [window] = new Function('return [window];')(); // real window & document object
  32. const hkey_script = 'nzsxclvflluv';
  33. if (window[hkey_script]) return; // avoid duplicated scripting
  34. window[hkey_script] = true;
  35. // copies of native functions
  36. const $$requestAnimationFrame = window.requestAnimationFrame.bind(window); // core looping
  37. const $$setTimeout = window.setTimeout.bind(window); // for race
  38. const $$setInterval = window.setInterval.bind(window); // for background execution
  39. const $$clearTimeout = window.clearTimeout.bind(window); // for native clearTimeout
  40. const $$clearInterval = window.clearInterval.bind(window); // for native clearInterval
  41. const $busy = Symbol('$busy');
  42. // Number.MAX_SAFE_INTEGER = 9007199254740991
  43. const INT_INITIAL_VALUE = 8192; // 1 ~ {INT_INITIAL_VALUE} are reserved for native setTimeout/setInterval
  44. const SAFE_INT_LIMIT = 2251799813685248; // in case cid would be used for multiplying
  45. const SAFE_INT_REDUCED = 67108864; // avoid persistent interval handlers with cids between {INT_INITIAL_VALUE + 1} and {SAFE_INT_REDUCED - 1}
  46. let mi = INT_INITIAL_VALUE; // skip first {INT_INITIAL_VALUE} cids to avoid browser not yet initialized
  47. const sb = {};
  48. const sFunc = (prop) => {
  49. return (func, ms, ...args) => {
  50. mi++; // start at {INT_INITIAL_VALUE + 1}
  51. if (mi > SAFE_INT_LIMIT) mi = SAFE_INT_REDUCED; // just in case
  52. let handler = args.length > 0 ? func.bind(null, ...args) : func; // original func if no extra argument
  53. handler[$busy] || (handler[$busy] = 0);
  54. sb[mi] = {
  55. handler,
  56. [prop]: ms, // timeout / interval; value can be undefined
  57. nextAt: Date.now() + (ms > 0 ? ms : 0) // overload for setTimeout(func);
  58. };
  59. return mi;
  60. };
  61. };
  62. const rm = function (jd) {
  63. if (!jd) return; // native setInterval & setTimeout start from 1
  64. let o = sb[jd];
  65. if (typeof o !== 'object') { // to clear the same cid is unlikely to happen || requiring nativeFn is unlikely to happen
  66. if (jd <= INT_INITIAL_VALUE) this.nativeFn(jd); // only for clearTimeout & clearInterval
  67. return;
  68. }
  69. for (let k in o) o[k] = null;
  70. o = null;
  71. sb[jd] = null;
  72. delete sb[jd];
  73. };
  74. window.setTimeout = sFunc('timeout');
  75. window.setInterval = sFunc('interval');
  76. window.clearTimeout = rm.bind({
  77. nativeFn: $$clearTimeout
  78. });
  79. window.clearInterval = rm.bind({
  80. nativeFn: $$clearInterval
  81. });
  82. // window.clearInterval = window.clearTimeout = rm;
  83. const delay16ms = (resolve => $$setTimeout(resolve, 16));
  84. const pf = (
  85. handler => new Promise(resolve => {
  86. // try catch is not required - no further execution on the handler
  87. // 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)
  88. // For function handler with low energy impact, discard or not discard depends on system performance
  89. if (handler[$busy] === 1) handler();
  90. handler[$busy]--;
  91. handler = null; // remove the reference of `handler`
  92. resolve();
  93. resolve = null; // remove the reference of `resolve`
  94. })
  95. );
  96. let toResetFuncHandlers = false;
  97. let bgExecutionAt = 0; // set at 0 to trigger tf in background startup when requestAnimationFrame is not responsive
  98. let dexActivePage = true; // true for default; false when checking triggered by setInterval
  99. let interupter = null;
  100. const raf = $$requestAnimationFrame.bind(window);
  101. const infiniteLooper = (resolve) => raf(interupter = resolve); // raf will not execute if document is hidden
  102. const mbx1 = async () => {
  103. // microTask #1
  104. let now = Date.now();
  105. //bgExecutionAt = now + 160; // if requestAnimationFrame is not responsive (e.g. background running)
  106. let promisesF = [];
  107. for (let jb in sb) {
  108. const o = sb[jb];
  109. let {
  110. handler,
  111. // timeout,
  112. interval,
  113. nextAt
  114. } = o;
  115. if (now < nextAt) continue;
  116. handler[$busy]++;
  117. promisesF.push(handler);
  118. if (interval > 0) { // prevent undefined, zero, negative values
  119. const _interval = +interval; // convertion from string to number if necessary; decimal is acceptable
  120. if (o.nextAt + _interval > now) o.nextAt += _interval;
  121. else if (o.nextAt + 2 * _interval > now) o.nextAt += 2 * _interval;
  122. else if (o.nextAt + 3 * _interval > now) o.nextAt += 3 * _interval;
  123. else if (o.nextAt + 4 * _interval > now) o.nextAt += 4 * _interval;
  124. else if (o.nextAt + 5 * _interval > now) o.nextAt += 5 * _interval;
  125. else o.nextAt = now + _interval;
  126. } else {
  127. // jb in sb must > INT_INITIAL_VALUE
  128. rm(jb); // remove timeout
  129. }
  130. }
  131. return promisesF;
  132. };
  133. const mbx2 = async (promisesF) => {
  134. // microTask #2
  135. //bgExecutionAt = Date.now() + 160; // if requestAnimationFrame is not responsive (e.g. background running)
  136. if (promisesF.length === 0) { // no handler functions
  137. // requestAnimationFrame when the page is active
  138. // execution interval is no less than AnimationFrame
  139. } else if (dexActivePage) {
  140. let ret2 = new Promise(delay16ms);
  141. let ret3 = new Promise(resolveK => {
  142. // error would not affect calling the next tick
  143. Promise.all(promisesF.map(pf)).then(resolveK); //microTasks
  144. promisesF.length = 0;
  145. })
  146. let race = Promise.race([ret2, ret3]);
  147. // ensure checking function must be called after 16ms to maintain visual changes in high fps.
  148. // >16ms examples: repaint/reflow, change of style/content
  149. await race;
  150. } else {
  151. new Promise(resolveK => {
  152. // error would not affect calling the next tick
  153. promisesF.forEach(pf); //microTasks
  154. promisesF.length = 0;
  155. })
  156. }
  157. };
  158. (async () => {
  159. while (true) {
  160. bgExecutionAt = Date.now() + 160;
  161. await new Promise(infiniteLooper);
  162. if (!interupter) {
  163. dexActivePage = false;
  164. } else {
  165. interupter = null;
  166. if (dexActivePage === false) toResetFuncHandlers = true;
  167. dexActivePage = true;
  168. }
  169. if (toResetFuncHandlers) {
  170. // true if page change from hidden to visible OR yt-finish
  171. toResetFuncHandlers = false;
  172. for (let jb in sb) sb[jb].handler[$busy] = 0; // including the functions with error
  173. }
  174. let promisesF = await mbx1();
  175. await mbx2(promisesF);
  176. interupter = null; // just ensure no interupter after mbx1 and mbx2
  177. }
  178. })();
  179. $$setInterval(() => {
  180. // no response of requestAnimationFrame; e.g. running in background
  181. let now = Date.now()
  182. if (interupter && now > bgExecutionAt) {
  183. bgExecutionAt = now + 230;
  184. let interupter_t = interupter;
  185. interupter = null; // avoid double calling
  186. interupter_t(); // if interupter is not called, wait for the next tick
  187. }
  188. }, 250);
  189. // i.e. 4 times per second for background execution - to keep YouTube application functional
  190. // if there is Timer Throttling for background running, the execution become the same as native setTimeout & setInterval.
  191. window.addEventListener("yt-navigate-finish", () => {
  192. toResetFuncHandlers = true; // ensure all function handlers can be executed after YouTube navigation.
  193. }, true); // capturing event - to let it runs before all everything else.
  194. // Your code here...
  195. })();