Hibernate Idle Tabs

If a tab is unused for a long time, it switches to a light holding page until the tab is focused again. This helps the browser to recover memory, and can speed up the re-opening of large sessions.

当前为 2018-04-05 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Hibernate Idle Tabs
  3. // @namespace HIT
  4. // @description If a tab is unused for a long time, it switches to a light holding page until the tab is focused again. This helps the browser to recover memory, and can speed up the re-opening of large sessions.
  5. // @version 1.2.0
  6. // @downstreamURL http://userscripts.org/scripts/source/123252.user.js
  7. // @include *
  8. // ==/UserScript==
  9.  
  10.  
  11. /* +++ Config +++ */
  12.  
  13. var hibernateIfIdleForMoreThan = 36*60*60; // 36 hours
  14. var restoreTime = 0.5; // in seconds
  15.  
  16. // We need an always-available basically blank HTML page we can navigate to
  17. // when we hibernate a tab. The userscript will run on that page, and await
  18. // re-activation.
  19. //
  20. // This page is not really ideal, since it provides an image and unneeded CSS.
  21. //
  22. // Also NOTE FOR SECURITY that whatever page you navigate to, the server admin
  23. // will be able to see which page you hibernated, in their logfile!
  24. //
  25. // If you have a blank page somewhere on the net, belonging to an admin you
  26. // trust, I recommend using that instead.
  27. //
  28. var holdingPage = "http://www.google.com/hibernated_tab";
  29.  
  30. // If you do change the holding, put the old one here, so that any existing
  31. // hibernated tabs still on the old page will be able to unhibernate.
  32. //
  33. var oldHoldingPage = "http://neuralyte.org/~joey/hibernated_tab.html";
  34.  
  35. var passFaviconToHoldingPage = true;
  36. var fadeHibernatedFavicons = true;
  37.  
  38. var forceHibernateWhenRunTwice = true;
  39.  
  40.  
  41.  
  42. /* +++ Documentation +++ */
  43. //
  44. // On a normal page, checks to see if the user goes idle. (Mouse movements,
  45. // key actions and page focus reset the idle timer.) If the page is left idle
  46. // past the timeout, then the window navigates to a lighter holding page,
  47. // hopefully freeing up memory in the browser.
  48. //
  49. // On a holding page, if the user focuses the window, the window navigates back
  50. // to the original page. (This can be cancelled in Chrome by clicking on
  51. // another tab, but not by paging to another tab with the keyboard!)
  52. //
  53. // I think single-click is also a cancellation now.
  54. //
  55. // In order for the tab of the holding page to present the same favicon as the
  56. // original page, we must capture this image before leaving the original page,
  57. // and pass it to the holding page as a CGI parameter.
  58. //
  59. // (A simpler alternative might be to aim for a 404 on the same domain and use
  60. // that as the holding page.)
  61. //
  62. // If you use Google Chrome or Chromium, then I would recommend using this
  63. // extension instead, which provides exactly the same functionality, but with
  64. // better security and probaly better performance too:
  65. //
  66. // https://chrome.google.com/webstore/detail/the-great-suspender/klbibkeccnjlkjkiokjodocebajanakg
  67.  
  68.  
  69. // (TODO: This aforementioned security concern could probably be fixed by passing data to the target page using # rather than ? - although it would only prevent the data from being passed over HTTP, but Javascript running on the target page could still read it.)
  70. //
  71. // Sadly, userscripts do not run on about:blank in Firefox 6.0 or Chromium 2011. I doubt a file:///... URL would work either.
  72.  
  73. // BUG: Sometimes when un-hibernating, the webserver of the page we return to
  74. // complains that the referrer URL header is too long!
  75.  
  76. // TODO: Some users may want the hibernated page to restore immediately when the tab is *refocused*, rather than waiting for a mouseover.
  77.  
  78. // TESTING: Expose a function to allow a bookmarklet to force-hibernate the current tab?
  79.  
  80. // CONSIDER: If we forget about fading the favicon, couldn't we simplify things by just sending the favicon URL rather than its image data? I think I tested this, and although I could load the favicon into the document, I was not successful at getting it into the browser's title tab by adding a new <link rel="icon">.
  81.  
  82.  
  83.  
  84. /* +++ Main +++ */
  85.  
  86. var onHoldingPage = document.location.href.match(holdingPage+"?") != null;
  87.  
  88. // If you change holding page, this keeps the old one working for a while, for
  89. // the sake of running browsers or saved sessions.
  90. if (!onHoldingPage && oldHoldingPage) {
  91. onHoldingPage = document.location.href.match(oldHoldingPage+"?") != null;
  92. }
  93.  
  94. function handleNormalPage() {
  95.  
  96. whenIdleDo(hibernateIfIdleForMoreThan,hibernatePage);
  97.  
  98. function hibernatePage() {
  99.  
  100. var params = {
  101. title: document.title,
  102. url: document.location.href
  103. };
  104.  
  105. function processFavicon(canvas) {
  106. document.body.appendChild(canvas);
  107. if (canvas) {
  108. try {
  109. if (fadeHibernatedFavicons) {
  110. makeCanvasMoreTransparent(canvas);
  111. }
  112. var faviconDataURL = canvas.toDataURL("image/png");
  113. params.favicon_data = faviconDataURL;
  114. } catch (e) {
  115. var extra = ( window != top ? " (running in frame or iframe)" : "" );
  116. console.error("[HIT] Got error"+extra+": "+e+" doc.loc="+document.location.href);
  117. // We get "Error: SECURITY_ERR: DOM Exception 18" (Chrome) if
  118. // the favicon is from a different host.
  119. }
  120. }
  121. reallyHibernatePage();
  122. }
  123.  
  124. function reallyHibernatePage() {
  125. var queryString = buildQueryParameterString(params);
  126. document.location = holdingPage + "?" + queryString;
  127. }
  128.  
  129. if (passFaviconToHoldingPage) {
  130. // I don't know how to grab the contents of the current favicon, so we
  131. // try to directly load a copy for ourselves.
  132. var url = document.location.href;
  133. var targetHost = url.replace(/.*:\/\//,'').replace(/\/.*/,'');
  134. loadFaviconIntoCanvas(targetHost,processFavicon);
  135. } else {
  136. reallyHibernatePage();
  137. }
  138.  
  139. }
  140.  
  141. function makeCanvasMoreTransparent(canvas) {
  142. var wid = canvas.width;
  143. var hei = canvas.height;
  144. var ctx = canvas.getContext("2d");
  145. var img = ctx.getImageData(0,0,wid,hei);
  146. var data = img.data;
  147. var len = 4*wid*hei;
  148. for (var ptr=0;ptr<len;ptr+=4) {
  149. data[ptr+3] /= 4; // alpha channel
  150. }
  151. // May or may not be needed:
  152. ctx.putImageData(img,0,0);
  153. }
  154.  
  155.  
  156.  
  157. if (forceHibernateWhenRunTwice) {
  158. if (window.hibernate_idle_tabs_loaded) {
  159. hibernatePage();
  160. }
  161. window.hibernate_idle_tabs_loaded = true;
  162. }
  163.  
  164.  
  165. }
  166.  
  167. function handleHoldingPage() {
  168.  
  169. var params = getQueryParameters();
  170.  
  171. // setHibernateStatus("Holding page for " + params.title + "\n with URL: "+params.url);
  172. // var titleReport = params.title + " (Holding Page)";
  173. var titleReport = "(" + (params.title || params.url) + " :: Hibernated)";
  174. setWindowTitle(titleReport);
  175.  
  176. var mainReport = titleReport;
  177. if (params.title) {
  178. /*
  179. statusElement.appendChild(document.createElement("P"));
  180. var div = document.createElement("tt");
  181. div.style.fontSize = "0.8em";
  182. div.appendChild(document.createTextNode(params.url));
  183. statusElement.appendChild(div);
  184. */
  185. mainReport += "\n" + params.url;
  186. }
  187.  
  188. setHibernateStatus(mainReport);
  189.  
  190. try {
  191. var faviconDataURL = params.favicon_data;
  192. if (!faviconDataURL) {
  193. // If we do not have a favicon, it is preferable to present an empty/transparent favicon, rather than let the browser show the favicon of the holding page site!
  194. faviconDataURL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAEklEQVQ4jWNgGAWjYBSMAggAAAQQAAF/TXiOAAAAAElFTkSuQmCC";
  195. }
  196. writeFaviconFromDataString(faviconDataURL);
  197. } catch (e) {
  198. console.error(""+e);
  199. }
  200.  
  201. function restoreTab(evt) {
  202. var url = decodeURIComponent(params.url);
  203. setHibernateStatus("Returning to: "+url);
  204. document.location.replace(url);
  205. /*
  206. // Alternative; preserves "forward"
  207. window.history.back(); // TESTING! With the fallback below, this seemed to work 90% of the time?
  208. // Sometimes it doesn't work. So we fallback to old method:
  209. setTimeout(function(){
  210. setHibernateStatus("window.history.back() FAILED - setting document.location");
  211. setTimeout(function(){
  212. document.location.replace(url); // I once saw this put ':'s when it should have put '%35's or whatever. (That broke 'Up' bookmarklet.)
  213. },1000);
  214. },2500);
  215. */
  216. evt.preventDefault(); // Accept responsibility for the double-click.
  217. return false; // Prevent browser from doing anything else with it (e.g. selecting the word under the cursor).
  218. }
  219.  
  220. checkForActivity();
  221.  
  222. function checkForActivity() {
  223.  
  224. var countdownTimer = null;
  225.  
  226. // listen(document.body,'mousemove',startCountdown); // In Firefox this ignore mousemove on empty space (outside the document content), so trying window...
  227. listen(window,'mousemove',startCountdown); // Likewise for click below!
  228. // listen(document.body,'blur',clearCountdown); // Does not fire in Chrome?
  229. listen(window,'blur',clearCountdown); // For Chrome
  230. //listen(window,'mouseout',clearCountdown); // Firefox appears to be firing this when my mouse is still over the window, preventing navigation! Let's just rely on 'blur' instead.
  231. // listen(document.body,'click',clearCountdown);
  232. listen(window,'click',clearCountdown);
  233. listen(window,'dblclick',restoreTab);
  234.  
  235. function startCountdown(e) {
  236. if (countdownTimer != null) {
  237. // There is already a countdown running - do not start.
  238. return;
  239. }
  240. var togo = restoreTime*1000;
  241. function countDown() {
  242. setHibernateStatus(mainReport +
  243. '\n' + "Tab will restore in "+(togo/1000).toFixed(1)+" seconds." +
  244. ' ' + "Click or defocus to pause." +
  245. ' ' + "Or double click to restore now!"
  246. );
  247. if (togo <= 0) {
  248. restoreTab();
  249. } else {
  250. togo -= 1000;
  251. if (countdownTimer)
  252. clearTimeout(countdownTimer);
  253. countdownTimer = setTimeout(countDown,1000);
  254. }
  255. }
  256. countDown();
  257. }
  258.  
  259. function clearCountdown(ev) {
  260. if (countdownTimer) {
  261. clearTimeout(countdownTimer);
  262. }
  263. countdownTimer = null;
  264. var evReport = "";
  265. if (ev) {
  266. evReport = " by "+ev.type+" on "+this;
  267. }
  268. var report = mainReport + '\n' + "Paused" + evReport + "";
  269. setHibernateStatus(report);
  270. }
  271.  
  272. }
  273.  
  274. }
  275.  
  276. if (onHoldingPage) {
  277. handleHoldingPage();
  278. } else {
  279. handleNormalPage();
  280. }
  281.  
  282.  
  283.  
  284. /* +++ Library Functions +++ */
  285.  
  286. function listen(target,eventType,handler,capture) {
  287. target.addEventListener(eventType,handler,capture);
  288. }
  289.  
  290. function ignore(target,eventType,handler,capture) {
  291. target.removeEventListener(eventType,handler,capture);
  292. }
  293.  
  294. // Given an object, encode its properties and values into a URI-ready CGI query string.
  295. function buildQueryParameterString(params) {
  296. return Object.keys(params).map( function(key) { return key+"="+encodeURIComponent(params[key]); } ).join("&");
  297. }
  298.  
  299. // Returns an object whose keys and values match those of the CGI query string of the current document.
  300. function getQueryParameters() {
  301. var queryString = document.location.search;
  302. var params = {};
  303. queryString.replace(/^\?/,'').split("&").map( function(s) {
  304. var part = s.split("=");
  305. var key = part[0];
  306. var value = decodeURIComponent(part[1]);
  307. params[key] = value;
  308. });
  309. return params;
  310. }
  311.  
  312. function whenIdleDo(idleTimeoutSecs,activateIdleEvent) {
  313.  
  314. var timer = null;
  315. var pageLastUsed = new Date().getTime();
  316.  
  317. function setNotIdle() {
  318. pageLastUsed = new Date().getTime();
  319. }
  320.  
  321. function checkForIdle() {
  322. var msSinceLastUsed = new Date().getTime() - pageLastUsed;
  323. if (msSinceLastUsed > idleTimeoutSecs * 1000) {
  324. activateIdleEvent();
  325. }
  326. setTimeout(checkForIdle,idleTimeoutSecs/5*1000);
  327. }
  328.  
  329. setTimeout(checkForIdle,idleTimeoutSecs*1000);
  330.  
  331. listen(document.body,'mousemove',setNotIdle);
  332. listen(document.body,'focus',setNotIdle);
  333. listen(document.body,'keydown',setNotIdle);
  334.  
  335. }
  336.  
  337.  
  338.  
  339. /* +++ Local Convenience Functions +++ */
  340.  
  341. var statusElement = null;
  342. function checkStatusElement() {
  343. if (!statusElement) {
  344. while (document.body.firstChild) {
  345. document.body.removeChild(document.body.firstChild);
  346. }
  347. statusElement = document.createElement("div");
  348. document.body.insertBefore(statusElement,document.body.firstChild);
  349. statusElement.style.textAlign = "center";
  350. }
  351. }
  352.  
  353. function setWindowTitle(msg) {
  354. msg = ""+msg;
  355. document.title = msg;
  356. }
  357.  
  358. function setHibernateStatus(msg) {
  359. msg = ""+msg;
  360. checkStatusElement();
  361. statusElement.textContent = msg;
  362. statusElement.innerText = msg; // IE
  363. // Currently '\n' works in Chrome, but not in Firefox.
  364. }
  365.  
  366.  
  367.  
  368.  
  369.  
  370.  
  371. /* +++ Favicon, Canvas and DataURL Magic +++ */
  372.  
  373. function loadFaviconForHost(targetHost,callback) {
  374.  
  375. // Try to load a favicon image for the given host, and pass it to callback.
  376. // Except: If there is a link with rel="icon" in the page, with host
  377. // matching the current page location, load that image file instead of
  378. // guessing the extension!
  379.  
  380. var favicon = document.createElement('img');
  381. favicon.addEventListener('load',function() {
  382. callback(favicon);
  383. });
  384.  
  385. var targetProtocol = document.location.protocol || "http:";
  386.  
  387. // If there is a <link rel="icon" ...> in the current page, then I think that overrides the site-global favicon.
  388. // NOTE: This is not appropriate if a third party targetHost was requested, only if they really wanted the favicon for the current page!
  389. var foundLink = null;
  390. var linkElems = document.getElementsByTagName("link");
  391. for (var i=0;i<linkElems.length;i++) {
  392. var link = linkElems[i];
  393. if (link.rel === "icon" || link.rel === "shortcut icon") {
  394. // Since we can't read the image data of images from third-party hosts, we skip them and keep searching.
  395. if (link.host == document.location.host) {
  396. foundLink = link;
  397. break;
  398. }
  399. }
  400. }
  401. if (foundLink) {
  402. favicon.addEventListener('error',function(){ callback(favicon); });
  403. favicon.src = foundLink.href; // Won't favicon.onload cause an additional callback to the one below?
  404. // NOTE: If we made the callback interface pass favicon as 'this' rather than an argument, then we wouldn't need to wrap it here (the argument may be evt).
  405. favicon = foundLink;
  406. callback(favicon);
  407. return;
  408. }
  409.  
  410. var extsToTry = ["jpg","gif","png","ico"]; // iterated in reverse order
  411. function tryNextExtension() {
  412. var ext = extsToTry.pop();
  413. if (ext == null) {
  414. console.log("Ran out of extensions to try for "+targetHost+"/favicon.???");
  415. // We run the callback anyway!
  416. callback(null);
  417. } else {
  418. favicon.src = targetProtocol+"//"+targetHost+"/favicon."+ext;
  419. }
  420. }
  421. favicon.addEventListener('error',tryNextExtension);
  422. tryNextExtension();
  423. // When the favicon is working we can remove the canvas, but until then we may as well keep it visible!
  424. }
  425.  
  426. function writeFaviconFromCanvas(canvas) {
  427. var faviconDataURL = canvas.toDataURL("image/png");
  428. // var faviconDataURL = canvas.toDataURL("image/x-icon;base64");
  429. // console.log("Got data URL: "+faviconDataURL.substring(0,10+"... (length "+faviconDataURL.length+")");
  430. writeFaviconFromDataString(faviconDataURL);
  431. }
  432.  
  433. function writeFaviconFromDataString(faviconDataURL) {
  434.  
  435. var d = document, h = document.getElementsByTagName('head')[0];
  436.  
  437. // Create this favicon
  438. var ss = d.createElement('link');
  439. ss.rel = 'shortcut icon';
  440. ss.type = 'image/x-icon';
  441. ss.href = faviconDataURL;
  442. /*
  443. // Remove any existing favicons
  444. var links = h.getElementsByTagName('link');
  445. for (var i=0; i<links.length; i++) {
  446. if (links[i].href == ss.href) return;
  447. if (links[i].rel == "shortcut icon" || links[i].rel=="icon")
  448. h.removeChild(links[i]);
  449. }
  450. */
  451. // Add this favicon to the head
  452. h.appendChild(ss);
  453.  
  454. // Force browser to acknowledge
  455. // I saw this trick somewhere. I don't know what browser requires it. But I just left it in!
  456. var shim = document.createElement('iframe');
  457. shim.width = shim.height = 0;
  458. document.body.appendChild(shim);
  459. shim.src = "icon";
  460. document.body.removeChild(shim);
  461.  
  462. }
  463.  
  464. function loadFaviconIntoCanvas(targetHost,callback) {
  465.  
  466. // console.log("Getting favicon for: "+targetHost);
  467.  
  468. var canvas = document.createElement('canvas');
  469. var ctx = canvas.getContext('2d');
  470.  
  471. loadFaviconForHost(targetHost,gotFavicon);
  472.  
  473. function gotFavicon(favicon) {
  474. if (favicon) {
  475. // console.log("Got favicon from: "+favicon.src);
  476. canvas.width = favicon.width;
  477. canvas.height = favicon.height;
  478. ctx.drawImage( favicon, 0, 0 );
  479. }
  480. callback(canvas);
  481. }
  482.  
  483. }
  484.  
  485. /* This throws a security error from canvas.toDataURL(), I think because we are
  486. trying to read something from a different domain than the script!
  487. In Chrome: "SECURITY_ERR: DOM Exception 18"
  488. */
  489. // loadFaviconIntoCanvas(document.location.host,writeFaviconFromCanvas);
  490.