merge m-c to fx-team
authorTim Taubert <ttaubert@mozilla.com>
Wed, 01 May 2013 10:18:22 +0200
changeset 141362 07ce0c1d04188854e0007c46a08e1bf4bfec7732
parent 141357 02aa81c59df65d69dab43cb5e6981067f38c3e23 (current diff)
parent 141361 e0559502553c56dd02f9eada235525625f253edc (diff)
child 141363 219557012d4b74c7fbff1b02bd9657f170d7d116
push id2579
push userakeybl@mozilla.com
push dateMon, 24 Jun 2013 18:52:47 +0000
treeherdermozilla-beta@b69b7de8a05a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone23.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge m-c to fx-team
browser/components/sessionstore/test/Makefile.in
--- a/browser/components/sessionstore/src/SessionStore.jsm
+++ b/browser/components/sessionstore/src/SessionStore.jsm
@@ -454,23 +454,20 @@ let SessionStoreInternal = {
           this._initialState.windows.forEach(function(aWindow) {
             delete aWindow.__lastSessionWindowID;
           });
         }
       }
       catch (ex) { debug("The session file is invalid: " + ex); }
     }
 
-    if (this._resume_from_crash) {
-      // Launch background copy of the session file. Note that we do
-      // not have race conditions here as _SessionFile guarantees
-      // that any I/O operation is completed before proceeding to
-      // the next I/O operation.
-      _SessionFile.createBackupCopy();
-    }
+    // A Lazy getter for the sessionstore.js backup promise.
+    XPCOMUtils.defineLazyGetter(this, "_backupSessionFileOnce", function () {
+      return _SessionFile.createBackupCopy();
+    });
 
     // at this point, we've as good as resumed the session, so we can
     // clear the resume_session_once flag, if it's set
     if (this._loadState != STATE_QUITTING &&
         this._prefBranch.getBoolPref("sessionstore.resume_session_once"))
       this._prefBranch.setBoolPref("sessionstore.resume_session_once", false);
 
     this._initEncoding();
@@ -3753,23 +3750,43 @@ let SessionStoreInternal = {
     Services.obs.notifyObservers(stateString, "sessionstore-state-write", "");
     data = stateString.data;
 
     // Don't touch the file if an observer has deleted all state data.
     if (!data) {
       return;
     }
 
-    let self = this;
-    _SessionFile.write(data).then(
-      function onSuccess() {
-        self._lastSaveTime = Date.now();
-        Services.obs.notifyObservers(null, "sessionstore-state-write-complete", "");
-      }
-    );
+    let promise;
+    // If "sessionstore.resume_from_crash" is true, attempt to backup the
+    // session file first, before writing to it.
+    if (this._resume_from_crash) {
+      // Note that we do not have race conditions here as _SessionFile
+      // guarantees that any I/O operation is completed before proceeding to
+      // the next I/O operation.
+      // Note backup happens only once, on initial save.
+      promise = this._backupSessionFileOnce;
+    } else {
+      promise = Promise.resolve();
+    }
+
+    // Attempt to write to the session file (potentially, depending on
+    // "sessionstore.resume_from_crash" preference, after successful backup).
+    promise = promise.then(function onSuccess() {
+      // Write (atomically) to a session file, using a tmp file.
+      return _SessionFile.write(data);
+    });
+
+    // Once the session file is successfully updated, save the time stamp of the
+    // last save and notify the observers.
+    promise = promise.then(() => {
+      this._lastSaveTime = Date.now();
+      Services.obs.notifyObservers(null, "sessionstore-state-write-complete",
+        "");
+    });
   },
 
   /* ........ Auxiliary Functions .............. */
 
   // Wrap a string as a nsISupports
   _createSupportsString: function ssi_createSupportsString(aData) {
     let string = Cc["@mozilla.org/supports-string;1"]
                    .createInstance(Ci.nsISupportsString);
--- a/browser/components/sessionstore/src/_SessionFile.jsm
+++ b/browser/components/sessionstore/src/_SessionFile.jsm
@@ -44,16 +44,20 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyServiceGetter(this, "Telemetry",
   "@mozilla.org/base/telemetry;1", "nsITelemetry");
 
 // An encoder to UTF-8.
 XPCOMUtils.defineLazyGetter(this, "gEncoder", function () {
   return new TextEncoder();
 });
+// A decoder.
+XPCOMUtils.defineLazyGetter(this, "gDecoder", function () {
+  return new TextDecoder();
+});
 
 this._SessionFile = {
   /**
    * A promise fulfilled once initialization (either synchronous or
    * asynchronous) is complete.
    */
   promiseInitialized: function SessionFile_initialized() {
     return SessionFileInternal.promiseInitialized;
@@ -140,70 +144,127 @@ let SessionFileInternal = {
   path: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js"),
 
   /**
    * The path to sessionstore.bak
    */
   backupPath: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak"),
 
   /**
+   * Utility function to safely read a file synchronously.
+   * @param aPath
+   *        A path to read the file from.
+   * @returns string if successful, undefined otherwise.
+   */
+  readAuxSync: function ssfi_readAuxSync(aPath) {
+    let text;
+    try {
+      let file = new FileUtils.File(aPath);
+      let chan = NetUtil.newChannel(file);
+      let stream = chan.open();
+      text = NetUtil.readInputStreamToString(stream, stream.available(),
+        {charset: "utf-8"});
+    } catch (e if e.result == Components.results.NS_ERROR_FILE_NOT_FOUND) {
+      // Ignore exceptions about non-existent files.
+    } catch (ex) {
+      // Any other error.
+      Cu.reportError(ex);
+    } finally {
+      return text;
+    }
+  },
+
+  /**
    * Read the sessionstore file synchronously.
    *
    * This function is meant to serve as a fallback in case of race
    * between a synchronous usage of the API and asynchronous
    * initialization.
+   *
+   * In case if sessionstore.js file does not exist or is corrupted (something
+   * happened between backup and write), attempt to read the sessionstore.bak
+   * instead.
    */
   syncRead: function ssfi_syncRead() {
-    let text;
-    let exn;
+    // Start measuring the duration of the synchronous read.
     TelemetryStopwatch.start("FX_SESSION_RESTORE_SYNC_READ_FILE_MS");
-    try {
-      let file = new FileUtils.File(this.path);
-      let chan = NetUtil.newChannel(file);
-      let stream = chan.open();
-      text = NetUtil.readInputStreamToString(stream, stream.available(), {charset: "utf-8"});
-    } catch (e if e.result == Components.results.NS_ERROR_FILE_NOT_FOUND) {
-      return "";
-    } catch(ex) {
-      exn = ex;
-    } finally {
-      TelemetryStopwatch.finish("FX_SESSION_RESTORE_SYNC_READ_FILE_MS");
+    // First read the sessionstore.js.
+    let text = this.readAuxSync(this.path);
+    if (typeof text === "undefined") {
+      // If sessionstore.js does not exist or is corrupted, read sessionstore.bak.
+      text = this.readAuxSync(this.backupPath);
     }
-    if (exn) {
-      Cu.reportError(exn);
-      return "";
-    }
-    return text;
+    // Finish the telemetry probe and return an empty string.
+    TelemetryStopwatch.finish("FX_SESSION_RESTORE_SYNC_READ_FILE_MS");
+    return text || "";
   },
 
-  read: function ssfi_read() {
-    let refObj = {};
+  /**
+   * Utility function to safely read a file asynchronously.
+   * @param aPath
+   *        A path to read the file from.
+   * @param aReadOptions
+   *        Read operation options.
+   *        |outExecutionDuration| option will be reused and can be
+   *        incrementally updated by the worker process.
+   * @returns string if successful, undefined otherwise.
+   */
+  readAux: function ssfi_readAux(aPath, aReadOptions) {
     let self = this;
-    return TaskUtils.spawn(function task() {
-      TelemetryStopwatch.start("FX_SESSION_RESTORE_READ_FILE_MS", refObj);
+    return TaskUtils.spawn(function () {
       let text;
       try {
-        let bytes = yield OS.File.read(self.path);
-        text = new TextDecoder().decode(bytes);
-        TelemetryStopwatch.finish("FX_SESSION_RESTORE_READ_FILE_MS", refObj);
+        let bytes = yield OS.File.read(aPath, undefined, aReadOptions);
+        text = gDecoder.decode(bytes);
+        // If the file is read successfully, add a telemetry probe based on
+        // the updated duration value of the |outExecutionDuration| option.
+        let histogram = Telemetry.getHistogramById(
+          "FX_SESSION_RESTORE_READ_FILE_MS");
+        histogram.add(aReadOptions.outExecutionDuration);
+      } catch (ex if self._isNoSuchFile(ex)) {
+        // Ignore exceptions about non-existent files.
       } catch (ex) {
-        if (self._isNoSuchFile(ex)) {
-          // The file does not exist, this is perfectly valid
-          TelemetryStopwatch.finish("FX_SESSION_RESTORE_READ_FILE_MS", refObj);
-        } else {
-          // Any other error: let's not measure with telemetry
-          TelemetryStopwatch.cancel("FX_SESSION_RESTORE_READ_FILE_MS", refObj);
-          Cu.reportError(ex);
-        }
-        text = "";
+        Cu.reportError(ex);
       }
       throw new Task.Result(text);
     });
   },
 
+  /**
+   * Read the sessionstore file asynchronously.
+   *
+   * In case sessionstore.js file does not exist or is corrupted (something
+   * happened between backup and write), attempt to read the sessionstore.bak
+   * instead.
+   */
+  read: function ssfi_read() {
+    let self = this;
+    return TaskUtils.spawn(function task() {
+      // Specify |outExecutionDuration| option to hold the combined duration of
+      // the asynchronous reads off the main thread (of both sessionstore.js and
+      // sessionstore.bak, if necessary). If sessionstore.js does not exist or
+      // is corrupted, |outExecutionDuration| will register the time it took to
+      // attempt to read the file. It will then be subsequently incremented by
+      // the read time of sessionsore.bak.
+      let readOptions = {
+        outExecutionDuration: null
+      };
+      // First read the sessionstore.js.
+      let text = yield self.readAux(self.path, readOptions);
+      if (typeof text === "undefined") {
+        // If sessionstore.js does not exist or is corrupted, read the
+        // sessionstore.bak.
+        text = yield self.readAux(self.backupPath, readOptions);
+      }
+      // Return either the content of the sessionstore.bak if it was read
+      // successfully or an empty string otherwise.
+      throw new Task.Result(text || "");
+    });
+  },
+
   write: function ssfi_write(aData) {
     let refObj = {};
     let self = this;
     return TaskUtils.spawn(function task() {
       TelemetryStopwatch.start("FX_SESSION_RESTORE_WRITE_FILE_MS", refObj);
       TelemetryStopwatch.start("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
 
       let bytes = gEncoder.encode(aData);
@@ -227,17 +288,17 @@ let SessionFileInternal = {
 
   createBackupCopy: function ssfi_createBackupCopy() {
     let backupCopyOptions = {
       outExecutionDuration: null
     };
     let self = this;
     return TaskUtils.spawn(function task() {
       try {
-        yield OS.File.copy(self.path, self.backupPath, backupCopyOptions);
+        yield OS.File.move(self.path, self.backupPath, backupCopyOptions);
         Telemetry.getHistogramById("FX_SESSION_RESTORE_BACKUP_FILE_MS").add(
           backupCopyOptions.outExecutionDuration);
       } catch (ex if self._isNoSuchFile(ex)) {
         // Ignore exceptions about non-existent files.
       } catch (ex) {
         Cu.reportError("Could not backup session state file: " + ex);
         throw ex;
       }
--- a/browser/components/sessionstore/test/Makefile.in
+++ b/browser/components/sessionstore/test/Makefile.in
@@ -134,16 +134,17 @@ MOCHITEST_BROWSER_FILES = \
 	browser_694378.js \
 	browser_701377.js \
 	browser_705597.js \
 	browser_707862.js \
 	browser_739531.js \
 	browser_739531_sample.html \
 	browser_739805.js \
 	browser_819510_perwindowpb.js \
+	browser_833286_atomic_backup.js \
 	$(filter disabled-for-intermittent-failures--bug-766044, browser_459906_empty.html) \
 	$(filter disabled-for-intermittent-failures--bug-766044, browser_459906_sample.html) \
 	$(filter disabled-for-intermittent-failures--bug-765389, browser_461743_sample.html) \
 	$(NULL)
 
 # Disabled on Windows for frequent intermittent failures
 ifneq ($(OS_ARCH), WINNT)
 MOCHITEST_FILES += \
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_833286_atomic_backup.js
@@ -0,0 +1,143 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This tests are for a sessionstore.js atomic backup.
+
+let tmp = {};
+Cu.import("resource://gre/modules/osfile.jsm", tmp);
+Cu.import("resource://gre/modules/Task.jsm", tmp);
+Cu.import("resource:///modules/sessionstore/_SessionFile.jsm", tmp);
+
+const {OS, Task, _SessionFile} = tmp;
+
+const PREF_SS_INTERVAL = "browser.sessionstore.interval";
+// Full paths for sessionstore.js and sessionstore.bak.
+const path = OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js");
+const backupPath = OS.Path.join(OS.Constants.Path.profileDir,
+  "sessionstore.bak");
+
+// A text decoder.
+let gDecoder = new TextDecoder();
+// Global variables that contain sessionstore.js and sessionstore.bak data for
+// comparison between tests.
+let gSSData;
+let gSSBakData;
+
+// waitForSaveStateComplete waits for a state write completion.
+function waitForSaveStateComplete(aSaveStateCallback) {
+  let topic = "sessionstore-state-write-complete";
+
+  function observer() {
+    Services.prefs.clearUserPref(PREF_SS_INTERVAL);
+    Services.obs.removeObserver(observer, topic);
+    executeSoon(function taskCallback() {
+      Task.spawn(aSaveStateCallback);
+    });
+  }
+
+  Services.obs.addObserver(observer, topic, false);
+}
+
+// Register next test callback and trigger state saving change.
+function nextTest(testFunc) {
+  waitForSaveStateComplete(testFunc);
+  Services.prefs.setIntPref(PREF_SS_INTERVAL, 0);
+}
+
+registerCleanupFunction(function() {
+  // Cleaning up after the test: removing the sessionstore.bak file.
+  Task.spawn(function cleanupTask() {
+    yield OS.File.remove(backupPath);
+  });
+});
+
+function test() {
+  waitForExplicitFinish();
+  nextTest(testInitialWriteNoBackup);
+}
+
+function testInitialWriteNoBackup() {
+  // Ensure that sessionstore.js is created, but not sessionstore.bak.
+  let ssExists = yield OS.File.exists(path);
+  let ssBackupExists = yield OS.File.exists(backupPath);
+  ok(ssExists, "sessionstore.js should be created.");
+  ok(!ssBackupExists, "sessionstore.bak should not have been created, yet.");
+
+  nextTest(testWriteNoBackup);
+}
+
+function testWriteNoBackup() {
+  // Ensure sessionstore.bak is not created.
+  let ssExists = yield OS.File.exists(path);
+  let ssBackupExists = yield OS.File.exists(backupPath);
+  ok(ssExists, "sessionstore.js should exist.");
+  ok(!ssBackupExists, "sessionstore.bak should not have been created, yet");
+
+  // Save sessionstore.js data to compare to the sessionstore.bak data in the
+  // next test.
+  let array = yield OS.File.read(path);
+  gSSData = gDecoder.decode(array);
+
+  // Manually trigger _SessionFile.createBackupCopy since the backup once
+  // promise is already resolved and backup would not be triggered again.
+  yield _SessionFile.createBackupCopy();
+
+  nextTest(testWriteBackup);
+}
+
+function testWriteBackup() {
+  // Ensure sessionstore.bak is finally created.
+  let ssExists = yield OS.File.exists(path);
+  let ssBackupExists = yield OS.File.exists(backupPath);
+  ok(ssExists, "sessionstore.js exists.");
+  ok(ssBackupExists, "sessionstore.bak should now be created.");
+
+  // Read sessionstore.bak data.
+  let array = yield OS.File.read(backupPath);
+  gSSBakData = gDecoder.decode(array);
+
+  // Make sure that the sessionstore.bak is identical to the last
+  // sessionstore.js.
+  is(gSSBakData, gSSData, "sessionstore.js is backed up correctly.");
+
+  // Read latest sessionstore.js.
+  array = yield OS.File.read(path);
+  gSSData = gDecoder.decode(array);
+
+  // Read sessionstore.js with _SessionFile.read.
+  let ssDataRead = yield _SessionFile.read();
+  is(ssDataRead, gSSData, "_SessionFile.read read sessionstore.js correctly.");
+
+  // Read sessionstore.js with _SessionFile.syncRead.
+  ssDataRead = _SessionFile.syncRead();
+  is(ssDataRead, gSSData,
+    "_SessionFile.syncRead read sessionstore.js correctly.");
+
+  // Remove sessionstore.js to test fallback onto sessionstore.bak.
+  yield OS.File.remove(path);
+  ssExists = yield OS.File.exists(path);
+  ok(!ssExists, "sessionstore.js should be removed now.");
+
+  // Read sessionstore.bak with _SessionFile.read.
+  ssDataRead = yield _SessionFile.read();
+  is(ssDataRead, gSSBakData,
+    "_SessionFile.read read sessionstore.bak correctly.");
+
+  // Read sessionstore.bak with _SessionFile.syncRead.
+  ssDataRead = _SessionFile.syncRead();
+  is(ssDataRead, gSSBakData,
+    "_SessionFile.syncRead read sessionstore.bak correctly.");
+  nextTest(testNoWriteBackup);
+}
+
+function testNoWriteBackup() {
+  // Ensure sessionstore.bak is backed up only once.
+
+  // Read sessionstore.bak data.
+  let array = yield OS.File.read(backupPath);
+  let ssBakData = gDecoder.decode(array);
+  // Ensure the sessionstore.bak did not change.
+  is(ssBakData, gSSBakData, "sessionstore.bak is unchanged.");
+
+  executeSoon(finish);
+}
\ No newline at end of file
--- a/browser/devtools/framework/connect/connect.js
+++ b/browser/devtools/framework/connect/connect.js
@@ -2,18 +2,16 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const Cu = Components.utils;
-Cu.import("resource:///modules/devtools/Target.jsm");
-Cu.import("resource:///modules/devtools/Toolbox.jsm");
 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
 let {devtools, gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
 
 let gClient;
 let gConnectionTimeout;
 
--- a/browser/devtools/styleeditor/test/browser_styleeditor_new.js
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_new.js
@@ -20,16 +20,18 @@ function test()
 
   content.location = TESTCASE_URI;
 }
 
 let gAddedCount = 0;  // to add new stylesheet after the 2 initial stylesheets
 let gNewEditor;       // to make sure only one new stylesheet got created
 let gOriginalHref;
 
+let checksCompleted = 0;
+
 function testEditorAdded(aEvent, aEditor)
 {
   gAddedCount++;
   if (gAddedCount == 2) {
     waitForFocus(function () {// create a new style sheet
       let newButton = gPanelWindow.document.querySelector(".style-editor-newButton");
       ok(newButton, "'new' button exists");
 
@@ -41,28 +43,39 @@ function testEditorAdded(aEvent, aEditor
   }
 
   ok(!gNewEditor, "creating a new stylesheet triggers one EditorAdded event");
   gNewEditor = aEditor; // above test will fail if we get a duplicate event
 
   is(gUI.editors.length, 3,
      "creating a new stylesheet added a new StyleEditor instance");
 
-  aEditor.getSourceEditor().then(testEditor);
-
   aEditor.styleSheet.once("style-applied", function() {
     // when changes have been completely applied to live stylesheet after transisiton
-    let summary = aEditor.summary;
-    let ruleCount = summary.querySelector(".stylesheet-rule-count").textContent;
-    is(parseInt(ruleCount), 1,
-       "new editor shows 1 rule after modification");
-
     ok(!content.document.documentElement.classList.contains(TRANSITION_CLASS),
        "StyleEditor's transition class has been removed from content");
+
+    if (++checksCompleted == 3) {
+      cleanup();
+    }
   });
+
+  aEditor.styleSheet.on("property-change", function(event, property) {
+    if (property == "ruleCount") {
+      let ruleCount = aEditor.summary.querySelector(".stylesheet-rule-count").textContent;
+      is(parseInt(ruleCount), 1,
+         "new editor shows 1 rule after modification");
+
+      if (++checksCompleted == 3) {
+        cleanup();
+      }
+    }
+  });
+
+  aEditor.getSourceEditor().then(testEditor);
 }
 
 function testEditor(aEditor) {
   waitForFocus(function () {
   gOriginalHref = aEditor.styleSheet.href;
 
   let summary = aEditor.summary;
 
@@ -111,14 +124,20 @@ function onTransitionEnd() {
 
   let computedStyle = content.getComputedStyle(content.document.body, null);
   is(computedStyle.backgroundColor, "rgb(255, 0, 0)",
      "content's background color has been updated to red");
 
   if (gNewEditor) {
     is(gNewEditor.styleSheet.href, gOriginalHref,
        "style sheet href did not change");
+  }
 
-    gNewEditor = null;
-    gUI = null;
-    finish();
+  if (++checksCompleted == 3) {
+    cleanup();
   }
 }
+
+function cleanup() {
+  gNewEditor = null;
+  gUI = null;
+  finish();
+}
--- a/toolkit/devtools/styleeditor/dbg-styleeditor-actors.js
+++ b/toolkit/devtools/styleeditor/dbg-styleeditor-actors.js
@@ -670,23 +670,24 @@ StyleSheetActor.prototype = {
    *
    * @param  {object} request
    *         'text' - new text
    *         'transition' - whether to do CSS transition for change.
    */
   onUpdate: function(request) {
     DOMUtils.parseStyleSheet(this.styleSheet, request.text);
 
+    this._notifyPropertyChanged("ruleCount");
+
     if (request.transition) {
       this._insertTransistionRule();
     }
     else {
       this._notifyStyleApplied();
     }
-    this._notifyPropertyChanged("ruleCount");
 
     return {};
   },
 
   /**
    * Insert a catch-all transition rule into the document. Set a timeout
    * to remove the rule after a certain time.
    */