Bug 617851 - Restore session history on crash or OOM [r=mfinkle r=mbrubeck r=vingtetun]
authorDoug Turner <dougt@dougt.org>
Wed, 15 Dec 2010 20:47:38 -0500
changeset 67125 e6c70153e799cd112bb7214efab1e6689b240b58
parent 67124 3bd55fe56b30e977a658d71da880c2a11eaf2555
child 67126 1885e8a2cce44ef785504b8714096e5e717594d8
push id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmfinkle, mbrubeck, vingtetun
bugs617851
Bug 617851 - Restore session history on crash or OOM [r=mfinkle r=mbrubeck r=vingtetun]
mobile/app/mobile.js
mobile/chrome/content/browser.js
mobile/chrome/content/tabs.xml
mobile/components/SessionStore.idl
mobile/components/SessionStore.js
--- a/mobile/app/mobile.js
+++ b/mobile/app/mobile.js
@@ -107,16 +107,22 @@ pref("network.autodial-helper.enabled", 
 
 /* history max results display */
 pref("browser.display.history.maxresults", 100);
 
 /* session history */
 pref("browser.sessionhistory.max_total_viewers", 1);
 pref("browser.sessionhistory.max_entries", 50);
 
+/* session store */
+pref("browser.sessionstore.resume_from_crash", true);
+pref("browser.sessionstore.resume_from_crash_timeout", 60); // minutes
+pref("browser.sessionstore.interval", 10000); // milliseconds
+pref("browser.sessionstore.max_tabs_undo", 5);
+
 /* these should help performance */
 pref("mozilla.widget.force-24bpp", true);
 pref("mozilla.widget.use-buffer-pixmap", true);
 pref("mozilla.widget.disable-native-theme", true);
 
 /* download manager (don't show the window or alert) */
 pref("browser.download.useDownloadDir", true);
 pref("browser.download.folderList", 1); // Default to ~/Downloads
--- a/mobile/chrome/content/browser.js
+++ b/mobile/chrome/content/browser.js
@@ -360,18 +360,23 @@ var Browser = {
     Util.forceOnline();
 
     // If this is an intial window launch the commandline handler passes us the default
     // page as an argument
     let defaultURL = this.getHomePage();
     if (window.arguments && window.arguments[0])
       defaultURL = window.arguments[0];
 
-    this.addTab(defaultURL, true);
-
+    // Should we restore the previous session (crash or some other event)
+    let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
+    if (ss.shouldRestore())
+      ss.restoreLastSession();
+    else
+      this.addTab(defaultURL, true);
+    
     // JavaScript Error Console
     if (Services.prefs.getBoolPref("browser.console.showInPanel")){
       let button = document.getElementById("tool-console");
       button.hidden = false;
     }
 
     // If some add-ons were disabled during during an application update, alert user
     if (Services.prefs.prefHasUserValue("extensions.disabledAddons")) {
@@ -2749,16 +2754,20 @@ Tab.prototype = {
   isLoading: function isLoading() {
     return this._loading;
   },
 
   create: function create(aURI, aParams) {
     this._chromeTab = document.getElementById("tabs").addTab();
     let browser = this._createBrowser(aURI, null);
 
+    // Should we fully load the new browser, or wait until later
+    if ("delayLoad" in aParams && aParams.delayLoad)
+      return;
+
     let flags = aParams.flags || Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
     browser.loadURIWithFlags(aURI, flags, aParams.referrerURI, aParams.charset, aParams.postData);
   },
 
   destroy: function destroy() {
     document.getElementById("tabs").removeTab(this._chromeTab);
     this._chromeTab = null;
     this._destroyBrowser();
--- a/mobile/chrome/content/tabs.xml
+++ b/mobile/chrome/content/tabs.xml
@@ -60,27 +60,44 @@
         <parameter name="height"/>
         <body>
           <![CDATA[
             const tabWidth = 104;
             const tabHeight = 65;
 
             let ratio = tabHeight / tabWidth;
             height = width * ratio;
-            
+
             let thumbnail = this.thumbnail;
             thumbnail.removeAttribute("empty");
 
+            // Recreate the canvas as it may be tainted and not useable for remote pages
+            if (thumbnail.hasAttribute("restored")) {
+              thumbnail.removeAttribute("restored");
+              thumbnail = this.thumbnail.cloneNode(false);
+              this.thumbnail.parentNode.replaceChild(thumbnail, this.thumbnail);
+              this.thumbnail = thumbnail;
+            }
+
+            let self = this;
             let renderer = rendererFactory(browser, thumbnail)
             renderer.drawContent(function(ctx, callback) {
               ctx.save();
               ctx.clearRect(0, 0, tabWidth, tabHeight);
               ctx.scale(tabWidth / width, tabHeight / height);
               callback(browser, 0, 0, width, height, "white");
               ctx.restore();
+
+              // We don't have an event for the async drawContent anymore, so hack it
+              setTimeout(function() {
+                // Save the thumbnail to the session in case we need to use it in a restore
+                let data = thumbnail.toDataURL("image/png");
+                let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
+                ss.setTabValue(self, "thumbnail", data);
+              }, 800);
             });
           ]]>
         </body>
       </method>
     </implementation>
   </binding>
 
   <binding id="tablist">
--- a/mobile/components/SessionStore.idl
+++ b/mobile/components/SessionStore.idl
@@ -97,9 +97,19 @@ interface nsISessionStore : nsISupports
    */
   void setTabValue(in nsIDOMNode aTab, in AString aKey, in AString aStringValue);
 
   /**
    * @param aTab is the browser tab to get the value for.
    * @param aKey is the value's name.
    */
   void deleteTabValue(in nsIDOMNode aTab, in AString aKey);
+
+  /**
+   * @returns A boolean indicating we should restore previous browser session
+   */
+  boolean shouldRestore();
+
+  /**
+   *  Restores the previous browser session using a fast, lightweight strategy
+   */
+  void restoreLastSession();
 };
--- a/mobile/components/SessionStore.js
+++ b/mobile/components/SessionStore.js
@@ -13,17 +13,17 @@
  *
  * The Original Code is Session Store.
  *
  * The Initial Developer of the Original Code is Mozilla Foundation.
  * Portions created by the Initial Developer are Copyright (C) 2009
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
- *   Mark Finkle <mfinkle@mozilla.com>
+ *   Mark 'evil' Finkle <mfinkle@mozilla.com>
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
@@ -67,41 +67,55 @@ SessionStore.prototype = {
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISessionStore,
                                          Ci.nsIDOMEventListener,
                                          Ci.nsIObserver,
                                          Ci.nsISupportsWeakReference]),
 
   _windows: {},
   _lastSaveTime: 0,
-  _interval: 15000,
+  _lastSessionTime: 0,
+  _interval: 10000,
   _maxTabsUndo: 5,
-  
+  _shouldRestore: false,
+
   init: function ss_init() {
     // Get file references
     this._sessionFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
     this._sessionFileBackup = this._sessionFile.clone();
     this._sessionFile.append("sessionstore.js");
     this._sessionFileBackup.append("sessionstore.bak");
 
     this._loadState = STATE_STOPPED;
 
     try {
-      if (this._sessionFileBackup.exists())
+      if (this._sessionFileBackup.exists()) {
+        this._shouldRestore = true;
         this._sessionFileBackup.remove(false);
-      if (this._sessionFile.exists())
+      }
+      if (this._sessionFile.exists()) {
+        // Disable crash recovery if we have exceeded the timeout
+        this._lastSessionTime = this._sessionFile.lastModifiedTime;
+        let delta = Date.now() - this._lastSessionTime;
+        let timeout = Services.prefs.getIntPref("browser.sessionstore.resume_from_crash_timeout");
+        if (delta > (timeout * 60000))
+          this._shouldRestore = false;
+
         this._sessionFile.copyTo(null, this._sessionFileBackup.leafName);
+      }
     } catch (ex) {
-      Cu.reportError(ex);  // file was write-locked?
+      Cu.reportError(ex); // file was write-locked?
     }
 
-    try {
-      this._interval = Services.prefs.getIntPref("sessionstore.interval");
-      this._maxTabsUndo = Services.prefs.getIntPref("sessionstore.max_tabs_undo");
-    } catch (e) {}
+    this._interval = Services.prefs.getIntPref("browser.sessionstore.interval");
+    this._maxTabsUndo = Services.prefs.getIntPref("browser.sessionstore.max_tabs_undo");
+
+    // Disable crash recovery if it has been turned off
+    if (!Services.prefs.getBoolPref("browser.sessionstore.resume_from_crash"))
+      this._shouldRestore = false;
   },
   
   observe: function ss_observe(aSubject, aTopic, aData) {
     let self = this;
     let observerService = Services.obs;
     switch (aTopic) {
       case "app-startup": 
         observerService.addObserver(this, "final-ui-startup", true);
@@ -151,16 +165,19 @@ SessionStore.prototype = {
 
         // Freeze the data at what we've got (ignoring closing windows)
         this._loadState = STATE_QUITTING;
         break;
       case "quit-application":
         // Freeze the data at what we've got (ignoring closing windows)
         this._loadState = STATE_QUITTING;
 
+        // No need for this back up, we are shutting down just fine
+        this._sessionFileBackup.remove(false);
+
         observerService.removeObserver(this, "domwindowopened");
         observerService.removeObserver(this, "domwindowclosed");
         observerService.removeObserver(this, "browser-lastwindow-close-granted");
         observerService.removeObserver(this, "quit-application-requested");
         observerService.removeObserver(this, "quit-application-granted");
         observerService.removeObserver(this, "quit-application");
 
         // Make sure to break our cycle with the save timer
@@ -262,79 +279,78 @@ SessionStore.prototype = {
     for (let i = 0; i < tabs.length; i++)
       this.onTabRemove(aWindow, tabs[i].browser, true);
     
     delete aWindow.__SSID;
   },
   
   onTabAdd: function ss_onTabAdd(aWindow, aBrowser, aNoNotification) {
     aBrowser.messageManager.addMessageListener("pageshow", this);
-    
+
     if (!aNoNotification)
       this.saveStateDelayed();
     this._updateCrashReportURL(aWindow);
   },
 
   onTabRemove: function ss_onTabRemove(aWindow, aBrowser, aNoNotification) {
     aBrowser.messageManager.removeMessageListener("pageshow", this);
-    
+
+    // If this browser is being restored, skip any session save activity
+    if (aBrowser.__SS_restore)
+      return;
+
     delete aBrowser.__SS_data;
     
     if (!aNoNotification)
       this.saveStateDelayed();
   },
 
   onTabClose: function ss_onTabClose(aWindow, aBrowser) {
     if (this._maxTabsUndo == 0)
       return;
 
     if (aWindow.Browser.tabs.length > 0) {
       // Bundle this browser's data and extra data and save in the closedTabs
       // window property
       let data = aBrowser.__SS_data;
-      data.extraData = aBrowser.__SS_extdata;
+      data.extData = aBrowser.__SS_extdata;
 
       this._windows[aWindow.__SSID].closedTabs.unshift(data);
       let length = this._windows[aWindow.__SSID].closedTabs.length;
       if (length > this._maxTabsUndo)
         this._windows[aWindow.__SSID].closedTabs.splice(this._maxTabsUndo, length - this._maxTabsUndo);
     }
   },
 
   onTabLoad: function ss_onTabLoad(aWindow, aBrowser, aMessage) { 
+    // If this browser is being restored, skip any session save activity
+    if (aBrowser.__SS_restore)
+      return;
+
     delete aBrowser.__SS_data;
     this._collectTabData(aBrowser);
 
     this.saveStateDelayed();
     this._updateCrashReportURL(aWindow);
   },
 
   onTabSelect: function ss_onTabSelect(aWindow, aBrowser) {
     if (this._loadState != STATE_RUNNING)
       return;
 
-    let index = 0;
-    let browser = aWindow.Browser;
-    let tabs = browser.tabs;
-    for (let i = 0; i < tabs.length; i++) {
-      if (tabs[i].browser == aBrowser) {
-        index = i;
-        break;
-      }
-    }
-
-    this._windows[aWindow.__SSID].selected = index + 1; // 1-based
+    let index = aWindow.Elements.browsers.selectedIndex;
+    this._windows[aWindow.__SSID].selected = parseInt(index) + 1; // 1-based
 
     // Restore the resurrected browser
     // * currently we only load the last URL into the browser
     if (aBrowser.__SS_restore) {
       let data = aBrowser.__SS_data;
       if (data.entries.length > 0)
         aBrowser.loadURI(data.entries[0].url, null, null);
-
+  
       delete aBrowser.__SS_restore;
     }
 
     this._updateCrashReportURL(aWindow);
   },
 
   saveStateDelayed: function ss_saveStateDelayed() {
     if (!this._saveTimer) {
@@ -369,32 +385,39 @@ SessionStore.prototype = {
     let data = { windows: [] };
     let index;
     for (index in this._windows)
       data.windows.push(this._windows[index]);
     return data;
   },
 
   _collectTabData: function ss__collectTabData(aBrowser) {
+    // If this browser is being restored, skip any session save activity
+    if (aBrowser.__SS_restore)
+      return;
+
     let tabData = { entries: [{}] };
     tabData.entries[0] = { url: aBrowser.currentURI.spec, title: aBrowser.contentTitle };
     tabData.index = 1;
     tabData.attributes = { image: aBrowser.mIconURL };
 
     aBrowser.__SS_data = tabData;
   },
   
   _collectWindowData: function ss__collectWindowData(aWindow) {
     // Ignore windows not tracked by SessionStore
     if (!aWindow.__SSID || !this._windows[aWindow.__SSID])
       return;
 
     let winData = this._windows[aWindow.__SSID];
     winData.tabs = [];
 
+    let index = aWindow.Elements.browsers.selectedIndex;
+    winData.selected = parseInt(index) + 1; // 1-based
+
     let tabs = aWindow.Browser.tabs;
     for (let i = 0; i < tabs.length; i++) {
       if (tabs[i].browser.__SS_data) {
         let browser = tabs[i].browser;
         let tabData = browser.__SS_data;
         if (browser.__SS_extdata)
           tabData.extData = browser.__SS_extdata;
         winData.tabs.push(tabData);
@@ -432,16 +455,37 @@ SessionStore.prototype = {
     let istream = converter.convertToInputStream(aData);
     NetUtil.asyncCopy(istream, ostream, function(rc) {
       if (Components.isSuccessCode(rc)) {
         Services.obs.notifyObservers(null, "sessionstore-state-write-complete", "");
       }
     });
   },
 
+  _readFile: function ss_readFile(aFile) {
+    try {
+      let stream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream);
+      stream.init(aFile, 0x01, 0, 0);
+      let cvstream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(Ci.nsIConverterInputStream);
+    
+      let fileSize = stream.available();
+      cvstream.init(stream, "UTF-8", fileSize, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
+
+      let data = {};
+      cvstream.readString(fileSize, data);
+      let content = data.value;
+      cvstream.close();
+
+      return content.replace(/\r\n?/g, "\n");
+    } catch (ex) {
+      Cu.reportError(ex);
+    }
+    return null;
+  },
+
   _updateCrashReportURL: function ss_updateCrashReportURL(aWindow) {
 #ifdef MOZ_CRASH_REPORTER
     try {
       let currentURI = aWindow.Browser.selectedBrowser.currentURI.clone();
       // if the current URI contains a username/password, remove it
       try {
         currentURI.userPass = "";
       }
@@ -491,17 +535,17 @@ SessionStore.prototype = {
     
     // fetch the data of closed tab, while removing it from the array
     let closedTab = closedTabs.splice(aIndex, 1).shift();
 
     // create a new tab and bring to front
     let tab = aWindow.Browser.addTab(closedTab.entries[0].url, true);
 
     // Put back the extra data
-    tab.browser.__SS_extdata = closedTab.extraData;
+    tab.browser.__SS_extdata = closedTab.extData;
 
     // TODO: save and restore more data (position, field values, etc)
 
     return tab.chromeTab;
   },
 
   forgetClosedTab: function ss_forgetClosedTab(aWindow, aIndex) {
     if (!aWindow.__SSID)
@@ -533,12 +577,63 @@ SessionStore.prototype = {
   },
 
   deleteTabValue: function ss_deleteTabValue(aTab, aKey) {
     let browser = aTab.linkedBrowser;
     if (browser.__SS_extdata && browser.__SS_extdata[aKey])
       delete browser.__SS_extdata[aKey];
     else
       throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+  },
+
+  shouldRestore: function ss_shouldRestore() {
+    return this._shouldRestore;
+  },
+
+  restoreLastSession: function ss_restoreLastSession() {
+    // The previous session data has already been renamed to the backup file
+    let dirService = Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties);
+    let session = dirService.get("ProfD", Ci.nsILocalFile);
+    session.append("sessionstore.bak");
+
+    let data = JSON.parse(this._readFile(session));
+    if (!data || data.windows.length == 0)
+      return;
+
+    let window = Services.wm.getMostRecentWindow("navigator:browser");
+
+    let selected = data.windows[0].selected;
+    let tabs = data.windows[0].tabs;
+    for (let i=0; i<tabs.length; i++) {
+      let tabData = tabs[i];
+
+      // Add a tab, but don't load the URL until we need to
+      let params = { getAttention: false, delayLoad: true };
+      if (i + 1 == selected)
+        params.delayLoad = false;
+
+      // We must have selected tabs as soon as possible, so we let all tabs be selected
+      // until we get the real selected tab. Then we stop selecting tabs. The end result
+      // is that the right tab is selected, but we also don't get a bunch of errors
+      let bringToFront = (i + 1 <= selected);
+      let tab = window.Browser.addTab(tabData.entries[0].url, bringToFront, null, params);
+
+      // Recreate the thumbnail if we are delay loading the tab
+      if (tabData.extData && params.delayLoad) {
+          let canvas = tab.chromeTab.thumbnail;
+          canvas.setAttribute("restored", "true");
+
+          let image = new window.Image();
+          image.onload = function() {
+            if (canvas)
+              canvas.getContext("2d").drawImage(image, 0, 0);
+          };
+          image.src = tabData.extData.thumbnail;
+      }
+      
+      tab.browser.__SS_data = tabData;
+      tab.browser.__SS_extdata = tabData.extData;
+      tab.browser.__SS_restore = params.delayLoad;
+    }
   }
 };
 
 const NSGetFactory = XPCOMUtils.generateNSGetFactory([SessionStore]);