YouTube Live CPU Tamer

降低超级聊天的高CPU利用率。外观完全没有变化。

目前为 2020-05-10 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube Live CPU Tamer
  3. // @name:ja YouTube Live CPU Tamer
  4. // @name:zh-CN YouTube Live CPU Tamer
  5. // @description It reduces the high CPU usage on Super Chats with nothing to lose.
  6. // @description:ja スーパーチャットによる高いCPU使用率を削減します。見た目は何も変わりません。
  7. // @description:zh-CN 降低超级聊天的高CPU利用率。外观完全没有变化。
  8. // @namespace knoa.jp
  9. // @include https://www.youtube.com/live_chat*
  10. // @include https://www.youtube.com/live_chat_replay*
  11. // @version 2.0.4
  12. // @grant none
  13. // ==/UserScript==
  14.  
  15. (function(){
  16. const SCRIPTID = 'YouTubeLiveCpuTamer';
  17. const SCRIPTNAME = 'YouTube Live CPU Tamer';
  18. const DEBUG = false;/*
  19. [update] 2.0.4
  20. Added "remove tickers" button for further CPU usage reduction. + minor fix.
  21.  
  22. [bug]
  23.  
  24. [todo]
  25.  
  26. [possible]
  27.  
  28. [research]
  29. 放送開始前の待機画面でもHelper(GPU)が食ってる件
  30. リアルタイム視聴時のほうがHelper(GPU)が消費する件は新着確認js処理のせいか
  31.  
  32. [memo]
  33. */
  34. if(console.time) console.time(SCRIPTID);
  35. const MS = 1, SECOND = 1000*MS, MINUTE = 60*SECOND, HOUR = 60*MINUTE, DAY = 24*HOUR, WEEK = 7*DAY, MONTH = 30*DAY, YEAR = 365*DAY;
  36. const THROTTLE = 1000*MS;
  37. const site = {
  38. targets: {
  39. itemsNode: () => $('yt-live-chat-ticker-renderer #items'),
  40. },
  41. get: {
  42. tickerItemInsideContainers: (items) => items.querySelectorAll('yt-live-chat-ticker-paid-message-item-renderer #container'),/* existing items */
  43. tickerItemInsideContainer: (node) => node.querySelector('yt-live-chat-ticker-paid-message-item-renderer #container'),/* for observer */
  44. },
  45. };
  46. let elements = {};
  47. const core = {
  48. initialize: function(){
  49. elements.html = document.documentElement;
  50. elements.html.classList.add(SCRIPTID);
  51. text.setup(texts, top.document.documentElement.lang);
  52. core.ready();
  53. core.addStyle('style');
  54. },
  55. ready: function(){
  56. core.getTargets(site.targets).then(() => {
  57. log("I'm ready.");
  58. core.observeTickerItems();
  59. core.prepareRemoveTickersButton();
  60. });
  61. },
  62. observeTickerItems: function(){
  63. let containers = site.get.tickerItemInsideContainers(elements.itemsNode);
  64. Array.from(containers).forEach(container => {
  65. core.observeTickerItemInsideContainer(container);
  66. });
  67. observe(elements.itemsNode, function(records){
  68. records.forEach(r => r.addedNodes.forEach(node => {
  69. let container = site.get.tickerItemInsideContainer(node);
  70. if(container) core.observeTickerItemInsideContainer(container);
  71. }));
  72. });
  73. },
  74. observeTickerItemInsideContainer: function(container){
  75. container.parentNode.style.background = container.style.background;
  76. let lastUpdated = Date.now();
  77. observe(container, function(records){
  78. let now = Date.now();
  79. if(now - lastUpdated < THROTTLE) return;
  80. lastUpdated = now;
  81. container.parentNode.style.background = container.style.background;
  82. }, {attributes: true, attributeFilter: ['style']});
  83. },
  84. prepareRemoveTickersButton: function(){
  85. let button = createElement(html.removeTickersButton());
  86. button.addEventListener('click', function(e){
  87. elements.itemsNode.parentNode.removeChild(elements.itemsNode);
  88. });
  89. elements.itemsNode.parentNode.appendChild(button);
  90. },
  91. getTarget: function(selector, retry = 10){
  92. const key = selector.name;
  93. const get = function(resolve, reject, retry){
  94. let selected = selector();
  95. if(selected && selected.length > 0) selected.forEach((s) => s.dataset.selector = key);/* elements */
  96. else if(selected instanceof HTMLElement) selected.dataset.selector = key;/* element */
  97. else if(--retry) return log(`Not found: ${key}, retrying... (${retry})`), setTimeout(get, 1000, resolve, reject, retry);
  98. else return reject(selector);
  99. elements[key] = selected;
  100. resolve(selected);
  101. };
  102. return new Promise(function(resolve, reject){
  103. get(resolve, reject, retry);
  104. }).catch(selector => {
  105. log(`Not found: ${key}, I give up.`);
  106. });
  107. },
  108. getTargets: function(selectors, retry = 10){
  109. return Promise.all(Object.values(selectors).map(selector => core.getTarget(selector, retry)));
  110. },
  111. addStyle: function(name = 'style'){
  112. if(html[name] === undefined) return;
  113. let style = createElement(html[name]());
  114. document.head.appendChild(style);
  115. if(elements[name] && elements[name].isConnected) document.head.removeChild(elements[name]);
  116. elements[name] = style;
  117. },
  118. };
  119. const texts = {
  120. 'remove tickers by ${SCRIPTNAME}': {
  121. en: () => `remove tickers by ${SCRIPTNAME}`,
  122. ja: () => `履歴欄を削除 by ${SCRIPTNAME}`,
  123. zh: () => `删除历史记录栏 by ${SCRIPTNAME}`,
  124. },
  125. };
  126. const html = {
  127. removeTickersButton: () => `<button id="${SCRIPTID}-removeTickers" title="${text('remove tickers by ${SCRIPTNAME}')}">╳</button>`,
  128. style: () => `
  129. <style type="text/css">
  130. yt-live-chat-ticker-renderer #items > *{
  131. border-radius: 999px;
  132. }
  133. yt-live-chat-ticker-renderer #items > * > #container{
  134. background: none !important;
  135. }
  136. yt-live-chat-ticker-renderer #${SCRIPTID}-removeTickers{
  137. cursor: pointer;
  138. position: absolute;
  139. top: 50%;
  140. left: 5px;
  141. transform: translateY(-50%);
  142. border-radius: 100vmax;
  143. background: white;
  144. filter: drop-shadow(0px 0px 1px rgba(0,0,0,.25));
  145. height: 20px;
  146. width: 20px;
  147. padding: 0 !important;
  148. opacity: 0;
  149. transition: opacity 250ms;
  150. pointer-events: none;
  151. }
  152. yt-live-chat-ticker-renderer:hover #left-arrow-container[hidden] ~ #${SCRIPTID}-removeTickers{
  153. opacity: 1;
  154. pointer-events: auto;
  155. }
  156. yt-live-chat-ticker-renderer #items > *{
  157. transition: transform 250ms;
  158. }
  159. yt-live-chat-ticker-renderer:hover #items > *{
  160. transform: translateX(5px);
  161. }
  162. </style>
  163. `,
  164. };
  165. const text = function(key, ...args){
  166. if(text.texts[key] === undefined){
  167. log('Not found text key:', key);
  168. return key;
  169. }else return text.texts[key](args);
  170. };
  171. text.setup = function(texts, language){
  172. let languages = [...window.navigator.languages];
  173. if(language) languages.unshift(...String(language).split('-').map((p,i,a) => a.slice(0,1+i).join('-')).reverse());
  174. if(!languages.includes('en')) languages.push('en');
  175. languages = languages.map(l => l.toLowerCase());
  176. Object.keys(texts).forEach(key => {
  177. Object.keys(texts[key]).forEach(l => texts[key][l.toLowerCase()] = texts[key][l]);
  178. texts[key] = texts[key][languages.find(l => texts[key][l] !== undefined)] || (() => key);
  179. });
  180. text.texts = texts;
  181. };
  182. const $ = function(s, f){
  183. let target = document.querySelector(s);
  184. if(target === null) return null;
  185. return f ? f(target) : target;
  186. };
  187. const $$ = function(s, f){
  188. let targets = document.querySelectorAll(s);
  189. return f ? Array.from(targets).map(t => f(t)) : targets;
  190. };
  191. const createElement = function(html = '<span></span>'){
  192. let outer = document.createElement('div');
  193. outer.innerHTML = html;
  194. return outer.firstElementChild;
  195. };
  196. const observe = function(element, callback, options = {childList: true, attributes: false, characterData: false, subtree: false}){
  197. let observer = new MutationObserver(callback.bind(element));
  198. observer.observe(element, options);
  199. return observer;
  200. };
  201. const log = function(){
  202. if(!DEBUG) return;
  203. let l = log.last = log.now || new Date(), n = log.now = new Date();
  204. let error = new Error(), line = log.format.getLine(error), callers = log.format.getCallers(error);
  205. //console.log(error.stack);
  206. console.log(
  207. SCRIPTID + ':',
  208. /* 00:00:00.000 */ n.toLocaleTimeString() + '.' + n.getTime().toString().slice(-3),
  209. /* +0.000s */ '+' + ((n-l)/1000).toFixed(3) + 's',
  210. /* :00 */ ':' + line,
  211. /* caller.caller */ (callers[2] ? callers[2] + '() => ' : '') +
  212. /* caller */ (callers[1] || '') + '()',
  213. ...arguments
  214. );
  215. };
  216. log.formats = [{
  217. name: 'Firefox Scratchpad',
  218. detector: /MARKER@Scratchpad/,
  219. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  220. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  221. }, {
  222. name: 'Firefox Console',
  223. detector: /MARKER@debugger/,
  224. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  225. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  226. }, {
  227. name: 'Firefox Greasemonkey 3',
  228. detector: /\/gm_scripts\//,
  229. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  230. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  231. }, {
  232. name: 'Firefox Greasemonkey 4+',
  233. detector: /MARKER@user-script:/,
  234. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 500,
  235. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  236. }, {
  237. name: 'Firefox Tampermonkey',
  238. detector: /MARKER@moz-extension:/,
  239. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 6,
  240. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  241. }, {
  242. name: 'Chrome Console',
  243. detector: /at MARKER \(<anonymous>/,
  244. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
  245. getCallers: (e) => e.stack.match(/[^ ]+(?= \(<anonymous>)/gm),
  246. }, {
  247. name: 'Chrome Tampermonkey',
  248. detector: /at MARKER \(chrome-extension:.*?\/userscript.html\?id=/,
  249. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1] - 5,
  250. getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
  251. }, {
  252. name: 'Chrome Extension',
  253. detector: /at MARKER \(chrome-extension:/,
  254. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
  255. getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
  256. }, {
  257. name: 'Edge Console',
  258. detector: /at MARKER \(eval/,
  259. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1],
  260. getCallers: (e) => e.stack.match(/[^ ]+(?= \(eval)/gm),
  261. }, {
  262. name: 'Edge Tampermonkey',
  263. detector: /at MARKER \(Function/,
  264. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1] - 4,
  265. getCallers: (e) => e.stack.match(/[^ ]+(?= \(Function)/gm),
  266. }, {
  267. name: 'Safari',
  268. detector: /^MARKER$/m,
  269. getLine: (e) => 0,/*e.lineが用意されているが最終呼び出し位置のみ*/
  270. getCallers: (e) => e.stack.split('\n'),
  271. }, {
  272. name: 'Default',
  273. detector: /./,
  274. getLine: (e) => 0,
  275. getCallers: (e) => [],
  276. }];
  277. log.format = log.formats.find(function MARKER(f){
  278. if(!f.detector.test(new Error().stack)) return false;
  279. //console.log('////', f.name, 'wants', 0/*line*/, '\n' + new Error().stack);
  280. return true;
  281. });
  282. core.initialize();
  283. if(console.timeEnd) console.timeEnd(SCRIPTID);
  284. })();