Background tab setTimeout muffler

Defers setTimeout calls if no user input has been received for 15 seconds. Useful for stopping background tabs from continuing to use CPU cycles.

  1. // ==UserScript==
  2. // @name Background tab setTimeout muffler
  3. // @namespace BSP
  4. // @description Defers setTimeout calls if no user input has been received for 15 seconds. Useful for stopping background tabs from continuing to use CPU cycles.
  5. // @include http://*
  6. // @include https://*
  7. // @exclude http://*.google.tld/*
  8. // @exclude https://*.google.tld/*
  9. // @exclude http://irc.lc/*
  10. // @exclude https://irc.lc/*
  11. // @exclude http://www.newsblur.com/*
  12. // @exclude https://www.newsblur.com/*
  13. // @exclude http://steamcommunity.com/id*
  14. // @exclude https://www.duolingo.com/*
  15. // @version 1
  16. // @run-at document-start
  17. // ==/UserScript==
  18.  
  19. var setInterval_old = unsafeWindow.setInterval;
  20. var setTimeout_old = unsafeWindow.setTimeout;
  21. var clearInterval_old = unsafeWindow.clearInterval;
  22. var clearTimeout_old = unsafeWindow.clearTimeout;
  23. var startDate = new Date;
  24.  
  25. //QueuedTimer format: {ID: [startTime, repeat, func, delay, args]}
  26. var queuedTimers = null;
  27. //ActiveTimer format: {ID: [repeat, func, delay, args, browserTimerID]}
  28. var activeTimers = {};
  29. var nextID = 1000000;
  30. var lastUserInteraction = Date.now();
  31.  
  32. function debugLog() {
  33. false && console && console.log && console.log(Array.slice(arguments, 0));
  34. }
  35.  
  36. function muffleTimers() {
  37. return (Date.now() - lastUserInteraction) > 15000;
  38. }
  39.  
  40. function addTimer(ID, repeat, func, delay, args, isNew) {
  41. if(typeof func != "function" && typeof func != "string") {
  42. debugLog("addTimer", "invalid func", arguments);
  43. throw Error("setInterval/setTimeout: invalid func");
  44. }
  45. delay = +(delay || 0); //coerce to number
  46. if(isNaN(delay)) { //check for NaN (results from non-number strings/objects, etc.)
  47. debugLog("addTimer", "invalid delay", arguments);
  48. throw Error("setInterval/setTimeout: invalid delay");
  49. }
  50. //if(Object.keys(queuedTimers || {}).length + Object.keys(activeTimers).length > 500 && new Date - startDate > 15000) {
  51. // debugLog("addTimer", "More than 100 timers!? Fuck that!", arguments);
  52. // throw Error("STFU PLZ");
  53. //}
  54. if(muffleTimers()) {
  55. if(!queuedTimers) queuedTimers = {};
  56. isNew && debugLog("addTimer", "Deferred", [ID, repeat, delay, func]);
  57. queuedTimers[ID] = [Date.now(), repeat, func, delay, args];
  58. } else {
  59. var browserTimerID = setTimeout_old(execTimer, delay, ID, repeat, func, delay, args);
  60. isNew && debugLog("addTimer", "Scheduled", [ID, repeat, delay, func]);
  61. activeTimers[ID] = [repeat, func, delay, args, browserTimerID];
  62.  
  63. }
  64. return ID;
  65. }
  66.  
  67. function execTimer(ID, repeat, func, delay, args) {
  68. delete activeTimers[ID];
  69. debugLog("execTimer", "Executing", ID, repeat);
  70. //Re-add to the list so it can be cleared while in progress
  71. if(repeat) {
  72. addTimer(ID, repeat, func, delay, args, false);
  73. }
  74. try {
  75. if(typeof func == "string") {
  76. (function() { unsafeWindow.eval(func); }).apply(unsafeWindow);
  77. } else {
  78. func.apply(unsafeWindow, args);
  79. }
  80. } catch(ex) { /* Silence it like JS normally does */ debugLog("execTimer", "Error", arguments, ex); }
  81. }
  82.  
  83. function onUserInteraction(event) {
  84. lastUserInteraction = Date.now();
  85. //If any timers are queued, restart them
  86. if(queuedTimers) {
  87. debugLog("onUserInteraction", "Processing deferred timers", arguments, queuedTimers);
  88. try {
  89. for(var ID in queuedTimers) {
  90. //QueuedTimer format: {ID: [startTime, repeat, func, delay, args]}
  91. var timer = queuedTimers[ID];
  92. var delay = Math.max(0, timer[0] + timer[3] - Date.now());
  93.  
  94. var browserTimerID = setTimeout_old(execTimer, delay, ID, timer[1], timer[2], timer[3], timer[4]);
  95. //ActiveTimer format: {ID: [repeat, func, delay, args, browserTimerID]}
  96. activeTimers[ID] = [timer[1], timer[2], timer[3], timer[4], browserTimerID];
  97. delete queuedTimers[ID];
  98. }
  99. } catch(ex) {
  100. debugLog("onUserInteraction", "Error", ex);
  101. }
  102. queuedTimers = null;
  103. }
  104. removeEventListener("mousemove", onUserInteraction);
  105. removeEventListener("keydown", onUserInteraction);
  106. setTimeout_old(function() {
  107. addEventListener("mousemove", onUserInteraction);
  108. addEventListener("keydown", onUserInteraction);
  109. }, 5000);
  110. }
  111.  
  112. addEventListener("mousemove", onUserInteraction);
  113. addEventListener("keydown", onUserInteraction);
  114.  
  115.  
  116. unsafeWindow.setInterval = function setInterval() {
  117. var func = arguments[0];
  118. var delay = arguments[1];
  119. var args = Array.slice(arguments, 2);
  120. return addTimer(nextID++, true, func, delay, args, true);
  121. };
  122.  
  123.  
  124. unsafeWindow.setTimeout = function setTimeout() {
  125. var func = arguments[0];
  126. var delay = arguments[1];
  127. var args = Array.slice(arguments, 2);
  128. return addTimer(nextID++, false, func, delay, args, true);
  129. };
  130.  
  131. function clearTimer(ID) {
  132. if(queuedTimers && typeof(queuedTimers[ID]) != "undefined") {
  133. debugLog("clearTimer", "Clearing deferred timer", arguments);
  134. delete queuedTimers[ID];
  135. }
  136. if(typeof(activeTimers[ID]) != "undefined") {
  137. debugLog("clearTimer", "Clearing active timer", arguments);
  138. clearTimeout(activeTimers[ID][4]);
  139. delete activeTimers[ID];
  140. }
  141. };
  142.  
  143. unsafeWindow.clearTimeout = clearTimer;
  144. unsafeWindow.clearInterval = clearTimer;