Bug 1380011 - Make startupRecorder.js' 'before handling user event' phase more reliable, r=mconley.
authorFlorian Quèze <florian@queze.net>
Wed, 12 Jul 2017 23:09:42 +0200
changeset 607781 914e475abbf414b129bc322b843541c5e9b0b6ab
parent 607780 5b77f711e2b4990067452d5dc04f613c42248b6d
child 607782 d3157e928734b9a025254e559210004d13febca1
push id68110
push userbmo:nchen@mozilla.com
push dateWed, 12 Jul 2017 21:26:51 +0000
reviewersmconley
bugs1380011
milestone56.0a1
Bug 1380011 - Make startupRecorder.js' 'before handling user event' phase more reliable, r=mconley.
browser/base/content/test/performance/browser_startup.js
browser/base/content/test/performance/browser_startup_images.js
browser/components/tests/startupRecorder.js
testing/marionette/components/marionette.js
--- a/browser/base/content/test/performance/browser_startup.js
+++ b/browser/base/content/test/performance/browser_startup.js
@@ -56,65 +56,81 @@ const startupPhases = {
     ])
   }},
 
   // We reach this phase right after showing the first browser window.
   // This means that anything already loaded at this point has been loaded
   // before first paint and delayed it.
   "before first paint": {blacklist: {
     components: new Set([
-      "PageIconProtocolHandler.js",
-      "PlacesCategoriesStarter.js",
       "UnifiedComplete.js",
-      "nsPlacesExpiration.js",
       "nsSearchService.js",
     ]),
     modules: new Set([
-      "chrome://webcompat-reporter/content/TabListener.jsm",
       "resource:///modules/AboutNewTab.jsm",
       "resource:///modules/DirectoryLinksProvider.jsm",
-      "resource:///modules/RecentWindow.jsm",
-      "resource://gre/modules/BookmarkHTMLUtils.jsm",
-      "resource://gre/modules/Bookmarks.jsm",
-      "resource://gre/modules/ContextualIdentityService.jsm",
       "resource://gre/modules/NewTabUtils.jsm",
       "resource://gre/modules/PageThumbs.jsm",
-      "resource://gre/modules/PlacesSyncUtils.jsm",
-      "resource://gre/modules/Promise.jsm",
-      "resource://gre/modules/Sqlite.jsm",
+      "resource://gre/modules/Promise.jsm", // imported by devtools during _delayedStartup
     ]),
     services: new Set([
-      "@mozilla.org/browser/annotation-service;1",
-      "@mozilla.org/browser/favicon-service;1",
-      "@mozilla.org/browser/nav-bookmarks-service;1",
       "@mozilla.org/browser/search-service;1",
     ])
   }},
 
   // We are at this phase once we are ready to handle user events.
   // Anything loaded at this phase or before gets in the way of the user
   // interacting with the first browser window.
   "before handling user events": {blacklist: {
+    components: new Set([
+      "PageIconProtocolHandler.js",
+      "PlacesCategoriesStarter.js",
+      "nsPlacesExpiration.js",
+    ]),
     modules: new Set([
+      "chrome://webcompat-reporter/content/TabListener.jsm",
+      "chrome://webcompat-reporter/content/WebCompatReporter.jsm",
+      "resource:///modules/RecentWindow.jsm",
+      "resource://gre/modules/BookmarkHTMLUtils.jsm",
+      "resource://gre/modules/Bookmarks.jsm",
+      "resource://gre/modules/ContextualIdentityService.jsm",
       "resource://gre/modules/FxAccounts.jsm",
       "resource://gre/modules/FxAccountsStorage.jsm",
+      "resource://gre/modules/PlacesSyncUtils.jsm",
+      "resource://gre/modules/Sqlite.jsm",
+    ]),
+    services: new Set([
+      "@mozilla.org/browser/annotation-service;1",
+      "@mozilla.org/browser/favicon-service;1",
+      "@mozilla.org/browser/nav-bookmarks-service;1",
+    ])
+  }},
+
+  // Things that are expected to be completely out of the startup path
+  // and loaded lazily when used for the first time by the user should
+  // be blacklisted here.
+  "before becoming idle": {blacklist: {
+    modules: new Set([
       "resource://gre/modules/LoginManagerContextMenu.jsm",
       "resource://gre/modules/Task.jsm",
     ]),
   }},
 };
 
-function test() {
+add_task(async function() {
   if (!AppConstants.NIGHTLY_BUILD && !AppConstants.DEBUG) {
     ok(!("@mozilla.org/test/startuprecorder;1" in Cc),
        "the startup recorder component shouldn't exist in this non-nightly non-debug build.");
     return;
   }
 
-  let data = Cc["@mozilla.org/test/startuprecorder;1"].getService().wrappedJSObject.data.code;
+  let startupRecorder = Cc["@mozilla.org/test/startuprecorder;1"].getService().wrappedJSObject;
+  await startupRecorder.done;
+
+  let data = startupRecorder.data.code;
   // Keep only the file name for components, as the path is an absolute file
   // URL rather than a resource:// URL like for modules.
   for (let phase in data) {
     data[phase].components =
       data[phase].components.map(f => f.replace(/.*\//, ""))
                             .filter(c => c != "startupRecorder.js");
   }
 
@@ -162,9 +178,9 @@ function test() {
     if (blacklist) {
       for (let scriptType in blacklist) {
         for (let file of blacklist[scriptType]) {
           ok(!loadedList[scriptType].includes(file), `${file} is not allowed ${phase}`);
         }
       }
     }
   }
-}
+});
--- a/browser/base/content/test/performance/browser_startup_images.js
+++ b/browser/base/content/test/performance/browser_startup_images.js
@@ -210,18 +210,21 @@ const whitelist = [
   },
   {
     file: "chrome://browser/skin/tabbrowser/tab-arrow-right.png",
     hidpi: "chrome://browser/skin/tabbrowser/tab-arrow-right@2x.png",
     platforms: ["macosx"],
   },
 ];
 
-function test() {
-  let data = Cc["@mozilla.org/test/startuprecorder;1"].getService().wrappedJSObject.data.images;
+add_task(async function() {
+  let startupRecorder = Cc["@mozilla.org/test/startuprecorder;1"].getService().wrappedJSObject;
+  await startupRecorder.done;
+
+  let data = startupRecorder.data.images;
   let filteredWhitelist = whitelist.filter(el => {
     return el.platforms.includes(AppConstants.platform) &&
            (el.photon === undefined || el.photon == AppConstants.MOZ_PHOTON_THEME);
   });
 
   let loadedImages = data["image-loading"];
   let shownImages = data["image-drawing"];
 
@@ -250,9 +253,9 @@ function test() {
         if (item.hidpi != "<not loaded>") {
           ok(loadedImages.has(item.hidpi), `Whitelisted image ${item.hidpi} should have been loaded.`);
         }
       } else {
         ok(loadedImages.has(item.file), `Whitelisted image ${item.file} should have been loaded.`);
       }
     }
   }
-}
+});
--- a/browser/components/tests/startupRecorder.js
+++ b/browser/components/tests/startupRecorder.js
@@ -28,16 +28,17 @@ function startupRecorder() {
   this.loader = Cc["@mozilla.org/moz/jsloader;1"].getService(Ci.xpcIJSModuleLoader);
   this.data = {
     images: {
       "image-drawing": new Set(),
       "image-loading": new Set(),
     },
     code: {}
   };
+  this.done = new Promise(resolve => { this._resolve = resolve });
 }
 startupRecorder.prototype = {
   classID: Components.ID("{11c095b2-e42e-4bdf-9dd0-aed87595f6a4}"),
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
 
   record(name) {
     this.data.code[name] = {
@@ -79,21 +80,33 @@ startupRecorder.prototype = {
     }
 
     Services.obs.removeObserver(this, topic);
 
     if (topic == "sessionstore-windows-restored") {
       // We use idleDispatchToMainThread here to record the set of
       // loaded scripts after we are fully done with startup and ready
       // to react to user events.
-      Services.tm.idleDispatchToMainThread(
+      Services.tm.dispatchToMainThread(
         this.record.bind(this, "before handling user events"));
 
-      Services.obs.removeObserver(this, "image-drawing");
-      Services.obs.removeObserver(this, "image-loading");
+      // 10 is an arbitrary value here, it needs to be at least 2 to avoid
+      // races with code initializing itself using idle callbacks.
+      (function waitForIdle(callback, count = 10) {
+        if (count)
+          Services.tm.idleDispatchToMainThread(() => waitForIdle(callback, count - 1));
+        else
+          callback();
+      })(() => {
+        this.record("before becoming idle");
+        Services.obs.removeObserver(this, "image-drawing");
+        Services.obs.removeObserver(this, "image-loading");
+        this._resolve();
+        this._resolve = null;
+      });
     } else {
       const topicsToNames = {
         "profile-do-change": "before profile selection",
         "toplevel-window-ready": "before opening first browser window",
       };
       topicsToNames[firstPaintNotification] = "before first paint";
       this.record(topicsToNames[topic]);
     }
--- a/testing/marionette/components/marionette.js
+++ b/testing/marionette/components/marionette.js
@@ -257,17 +257,20 @@ MarionetteComponent.prototype.suppressSa
 MarionetteComponent.prototype.init = function() {
   if (this.running || !this.enabled || !this.finalUIStartup) {
     return;
   }
 
   // Delay initialization until we are done with delayed startup...
   Services.tm.idleDispatchToMainThread(() => {
     // ... and with startup tests.
-    Services.tm.idleDispatchToMainThread(() => {
+    let promise = Promise.resolve();
+    if ("@mozilla.org/test/startuprecorder;1" in Cc)
+      promise = Cc["@mozilla.org/test/startuprecorder;1"].getService().wrappedJSObject.done;
+    promise.then(() => {
       let s;
       try {
         Cu.import("chrome://marionette/content/server.js");
         s = new server.TCPListener(prefs.port);
         s.start();
         this.logger.info(`Listening on port ${s.port}`);
       } finally {
         if (s) {