Bug 1245891 - Changing Session Restore Talos tests to include the time to restore actual tabs;r=avih,mconley draft
authorDavid Rajchenbach-Teller <dteller@mozilla.com>
Tue, 15 Mar 2016 11:10:21 +0100
changeset 340459 04cc97ee43fbf47c4217367ed565f6eda70bdbde
parent 335982 eb25b90a05c194bfd4f498ff3ffee7440f85f1cd
child 516191 4606f972cd603f72222e90b268016f909f2194d0
push id12970
push userdteller@mozilla.com
push dateTue, 15 Mar 2016 10:41:09 +0000
reviewersavih, mconley
bugs1245891
milestone47.0a1
Bug 1245891 - Changing Session Restore Talos tests to include the time to restore actual tabs;r=avih,mconley MozReview-Commit-ID: 3kjG6ixWNiO MozReview-Commit-ID: ITPYkv6Jj4t
browser/components/sessionstore/StartupPerformance.jsm
testing/talos/talos/startup_test/sessionrestore/addon/SessionRestoreTalosTest.js
testing/talos/talos/startup_test/sessionrestore/addon/install.rdf
testing/talos/talos/startup_test/sessionrestore/addon/sessionrestore-signed.xpi
--- a/browser/components/sessionstore/StartupPerformance.jsm
+++ b/browser/components/sessionstore/StartupPerformance.jsm
@@ -18,19 +18,24 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   "resource://gre/modules/Timer.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "clearTimeout",
   "resource://gre/modules/Timer.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
   "resource://gre/modules/Promise.jsm");
 
 const COLLECT_RESULTS_AFTER_MS = 10000;
 
-const TOPICS = ["sessionstore-restoring-on-startup", "sessionstore-initiating-manual-restore"];
+const OBSERVED_TOPICS = ["sessionstore-restoring-on-startup", "sessionstore-initiating-manual-restore"];
 
 this.StartupPerformance = {
+  /**
+   * Once we have finished restoring initial tabs, we broadcast on this topic.
+   */
+  RESTORED_TOPIC: "sessionstore-finished-restoring-initial-tabs",
+
   // Instant at which we have started restoration (notification "sessionstore-restoring-on-startup")
   _startTimeStamp: null,
 
   // Latest instant at which we have finished restoring a tab (DOM event "SSTabRestored")
   _latestRestoredTimeStamp: null,
 
   // A promise resolved once we have finished restoring all the startup tabs.
   _promiseFinished: null,
@@ -39,68 +44,90 @@ this.StartupPerformance = {
   _resolveFinished: null,
 
   // A timer
   _deadlineTimer: null,
 
   // `true` once the timer has fired
   _hasFired: false,
 
+  // `true` once we are restored
+  _isRestored: false,
+
   // Statistics on the session we need to restore.
   _totalNumberOfEagerTabs: 0,
   _totalNumberOfTabs: 0,
   _totalNumberOfWindows: 0,
 
   init: function() {
-    for (let topic of TOPICS) {
+    for (let topic of OBSERVED_TOPICS) {
       Services.obs.addObserver(this, topic, false);
     }
   },
 
+  /**
+   * Return the timestamp at which we finished restoring the latest tab.
+   *
+   * This information is not really interesting until we have finished restoring
+   * tabs.
+   */
+  get latestRestoredTimeStamp() {
+    return this._latestRestoredTimeStamp;
+  },
+
+  /**
+   * `true` once we have finished restoring startup tabs.
+   */
+  get isRestored() {
+    return this._isRestored;
+  },
+
   // Called when restoration starts.
   // Record the start timestamp, setup the timer and `this._promiseFinished`.
   // Behavior is unspecified if there was already an ongoing measure.
   _onRestorationStarts: function(isAutoRestore) {
-    this._startTimeStamp = Date.now();
+    this._latestRestoredTimeStamp = this._startTimeStamp = Date.now();
     this._totalNumberOfEagerTabs = 0;
     this._totalNumberOfTabs = 0;
     this._totalNumberOfWindows = 0;
 
     // While we may restore several sessions in a single run of the browser,
     // that's a very unusual case, and not really worth measuring, so let's
     // stop listening for further restorations.
 
-    for (let topic of TOPICS) {
+    for (let topic of OBSERVED_TOPICS) {
       Services.obs.removeObserver(this, topic);
     }
 
     Services.obs.addObserver(this, "sessionstore-single-window-restored", false);
     this._promiseFinished = new Promise(resolve => {
       this._resolveFinished = resolve;
     });
     this._promiseFinished.then(() => {
       try {
-        if (!this._latestRestoredTimeStamp) {
+        this._isRestored = true;
+        Services.obs.notifyObservers(null, this.RESTORED_TOPIC, "");
+
+        if (this._latestRestoredTimeStamp == this._startTimeStamp) {
           // Apparently, we haven't restored any tab.
           return;
         }
 
         // Once we are done restoring tabs, update Telemetry.
         let histogramName = isAutoRestore ?
           "FX_SESSION_RESTORE_AUTO_RESTORE_DURATION_UNTIL_EAGER_TABS_RESTORED_MS" :
           "FX_SESSION_RESTORE_MANUAL_RESTORE_DURATION_UNTIL_EAGER_TABS_RESTORED_MS";
         let histogram = Services.telemetry.getHistogramById(histogramName);
         let delta = this._latestRestoredTimeStamp - this._startTimeStamp;
         histogram.add(delta);
 
         Services.telemetry.getHistogramById("FX_SESSION_RESTORE_NUMBER_OF_EAGER_TABS_RESTORED").add(this._totalNumberOfEagerTabs);
         Services.telemetry.getHistogramById("FX_SESSION_RESTORE_NUMBER_OF_TABS_RESTORED").add(this._totalNumberOfTabs);
         Services.telemetry.getHistogramById("FX_SESSION_RESTORE_NUMBER_OF_WINDOWS_RESTORED").add(this._totalNumberOfWindows);
 
-
         // Reset
         this._startTimeStamp = null;
      } catch (ex) {
         console.error("StartupPerformance: error after resolving promise", ex);
       }
     });
   },
 
--- a/testing/talos/talos/startup_test/sessionrestore/addon/SessionRestoreTalosTest.js
+++ b/testing/talos/talos/startup_test/sessionrestore/addon/SessionRestoreTalosTest.js
@@ -11,26 +11,27 @@ const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
   "resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
   "resource://gre/modules/Timer.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "StartupPerformance",
+  "resource:///modules/sessionstore/StartupPerformance.jsm");
 
 // Observer Service topics.
 const STARTUP_TOPIC = "profile-after-change";
-const RESTORED_TOPIC = "sessionstore-windows-restored";
 
 // Process Message Manager topics.
 const MSG_REQUEST = "session-restore-test?duration";
 const MSG_PROVIDE = "session-restore-test:duration";
 
-function nsSessionRestoreTalosTest() {}
+function nsSessionRestoreTalosTest() { }
 
 nsSessionRestoreTalosTest.prototype = {
   classID: Components.ID("{716346e5-0c45-4aa2-b601-da36f3c74bd8}"),
 
   _xpcom_factory: XPCOMUtils.generateSingletonFactory(nsSessionRestoreTalosTest),
 
   //////////////////////////////////////////////////////////////////////////////
   //// nsISupports
@@ -40,50 +41,75 @@ nsSessionRestoreTalosTest.prototype = {
   //////////////////////////////////////////////////////////////////////////////
   //// nsIObserver
 
   observe: function DS_observe(aSubject, aTopic, aData) {
     switch (aTopic) {
       case STARTUP_TOPIC:
         this.init();
         break;
-      case RESTORED_TOPIC:
-        this.onRestored();
+      case StartupPerformance.RESTORED_TOPIC:
+        this.onReady(true);
         break;
       default:
         throw new Error(`Unknown topic ${aTopic}`);
     }
   },
 
   /**
    * Perform initialization on profile-after-change.
    */
   init: function() {
-    Services.obs.addObserver(this, RESTORED_TOPIC, false);
+    if (StartupPerformance.isRestored) {
+      this.onReady(true);
+    } else {
+      let sessionStartup = Cc["@mozilla.org/browser/sessionstartup;1"]
+                                 .getService(Ci.nsISessionStartup);
+      sessionStartup.onceInitialized.then(() => {
+        if (sessionStartup.sessionType == Ci.nsISessionStartup.NO_SESSION
+        || sessionStartup.sessionType == Ci.nsISessionStartup.DEFER_SESSION) {
+          this.onReady(false);
+        } else {
+          Services.obs.addObserver(this, StartupPerformance.RESTORED_TOPIC, false);
+        }
+      });
+    }
   },
 
   /**
    * Session Restore is complete, hurray.
    */
-  onRestored: function() {
-    setTimeout(function() {
-      // `sessionRestored` actually becomes available only on the next tick.
-      let startup_info = Services.startup.getStartupInfo();
-      let duration = startup_info.sessionRestored - startup_info.sessionRestoreInit;
+  onReady: function(hasRestoredTabs) {
+    if (hasRestoredTabs) {
+      Services.obs.removeObserver(this, StartupPerformance.RESTORED_TOPIC);
+    }
+    try {
+      setTimeout(function() {
+        // `StartupPerformance.latestRestoredTimeStamp` actually becomes available only on the next tick.
+        let startup_info = Services.startup.getStartupInfo();
+        let duration =
+          hasRestoredTabs
+            ? StartupPerformance.latestRestoredTimeStamp - startup_info.sessionRestoreInit
+            : startup_info.sessionRestored - startup_info.sessionRestoreInit;
 
-      // Broadcast startup duration information immediately, in case the talos
-      // page is already loaded.
-      Services.ppmm.broadcastAsyncMessage(MSG_PROVIDE, {duration});
+        // Broadcast startup duration information immediately, in case the talos
+        // page is already loaded.
+        Services.ppmm.broadcastAsyncMessage(MSG_PROVIDE, {duration});
 
-      // Now, in case the talos page isn't loaded yet, prepare to respond if it
-      // requestions the duration information.
-      Services.ppmm.addMessageListener(MSG_REQUEST, function listener() {
-        Services.ppmm.removeMessageListener(MSG_REQUEST, listener);
-        Services.ppmm.broadcastAsyncMessage(MSG_PROVIDE, {duration});
-      });
-    }, 0);
+        // Now, in case the talos page isn't loaded yet, prepare to respond if it
+        // requestions the duration information.
+        Services.ppmm.addMessageListener(MSG_REQUEST, function listener() {
+          Services.ppmm.removeMessageListener(MSG_REQUEST, listener);
+          Services.ppmm.broadcastAsyncMessage(MSG_PROVIDE, {duration});
+        });
+      }, 0);
+    } catch (ex) {
+      dump(`SessionRestoreTalosTest: error ${ex}\n`);
+      dump(ex.stack);
+      dump("\n");
+    }
   },
 };
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Module
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([nsSessionRestoreTalosTest]);
--- a/testing/talos/talos/startup_test/sessionrestore/addon/install.rdf
+++ b/testing/talos/talos/startup_test/sessionrestore/addon/install.rdf
@@ -1,14 +1,14 @@
 <?xml version="1.0"?><RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"     xmlns:em="http://www.mozilla.org/2004/em-rdf#"><Description about="urn:mozilla:install-manifest">
 
 <!-- Required Items -->
-<em:id>session-restore-test@mozilla.org</em:id>
+<em:id>session-restore-test-2@mozilla.org</em:id>
 <em:name>Session Restore Startup Performance Test</em:name>
-<em:version>1.2.0</em:version>
+<em:version>2.0.9</em:version>
 
 <em:targetApplication>
     <Description>
         <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
         <em:minVersion>1.5</em:minVersion>
         <em:maxVersion>*</em:maxVersion>
     </Description>
 </em:targetApplication>
index 3dcd0124a5b03b2ed7aeeedb0cedca2688af7d0e..a4bc441bff09fe2b8c1d3bbc758ef78b5cbfa4ab
GIT binary patch
literal 7586
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
OBLnPSwxuQe!}>3eAch41