Bug 1363059 - Add a test for images loaded at startup vs. images shown at startup. r=florian,jwatt draft
authorJohann Hofmann <jhofmann@mozilla.com>
Thu, 15 Jun 2017 00:11:48 +0200
changeset 601684 5f75fcd1152f569a5b48e21d4e4821a24f768ecd
parent 601524 217b7fcf58944f927118b465769faeb1e613130a
child 635375 0b807871667ecac5e9f665caeb9283887ab35481
push id66185
push userbmo:jhofmann@mozilla.com
push dateThu, 29 Jun 2017 00:54:12 +0000
reviewersflorian, jwatt
bugs1363059
milestone56.0a1
Bug 1363059 - Add a test for images loaded at startup vs. images shown at startup. r=florian,jwatt This patch enables startupRecorder.js to collect data on loaded and shown raster and SVG images on startup via events from native code. It also adds a test that uses this data to find images that are unnecessarily loaded. I've not fixed any of the affected images yet, there's a fairly comprehensive whitelist that I want to gradually decrease by opening bugs in the respective components. MozReview-Commit-ID: 9KqQvKLtZhu
browser/base/content/test/performance/browser.ini
browser/base/content/test/performance/browser_startup.js
browser/base/content/test/performance/browser_startup_images.js
browser/components/tests/startupRecorder.js
image/ImageFactory.cpp
image/RasterImage.cpp
image/VectorImage.cpp
--- a/browser/base/content/test/performance/browser.ini
+++ b/browser/base/content/test/performance/browser.ini
@@ -1,13 +1,15 @@
 [DEFAULT]
 support-files =
   head.js
 [browser_appmenu_reflows.js]
 [browser_startup.js]
+[browser_startup_images.js]
+skip-if = !debug
 [browser_tabclose_grow_reflows.js]
 [browser_tabclose_reflows.js]
 [browser_tabopen_reflows.js]
 [browser_tabopen_squeeze_reflows.js]
 [browser_tabswitch_reflows.js]
 [browser_toolbariconcolor_restyles.js]
 [browser_windowclose_reflows.js]
 [browser_windowopen_reflows.js]
--- a/browser/base/content/test/performance/browser_startup.js
+++ b/browser/base/content/test/performance/browser_startup.js
@@ -100,17 +100,17 @@ const startupPhases = {
 
 function test() {
   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;
+  let data = Cc["@mozilla.org/test/startuprecorder;1"].getService().wrappedJSObject.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");
   }
 
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup_images.js
@@ -0,0 +1,229 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* A whitelist of images that are loaded at startup but not shown.
+ * List items support the following attributes:
+ *  - file: The location of the loaded image file.
+ *  - hidpi: An alternative hidpi file location for retina screens, if one exists.
+ *           May be the magic string <not loaded> in strange cases where
+ *           only the low-resolution image is loaded but not shown.
+ *  - platforms: An array of the platforms where the issue is occurring.
+ *               Possible values are linux, win, macosx.
+ *  - intermittentNotLoaded: an array of platforms where this image is
+ *                           intermittently not loaded, e.g. because it is
+ *                           loaded during the time we stop recording.
+ *  - intermittentShown: An array of platforms where this image is
+ *                       intermittently shown, contrary to what our
+ *                       whitelist says.
+ *
+ * Please don't add items to this list. Please remove items from this list.
+ */
+const whitelist = [
+  {
+    file: "chrome://browser/skin/fxa/sync-illustration.svg",
+    platforms: ["linux", "win", "macosx"],
+  },
+  {
+    file: "chrome://browser/skin/tabbrowser/tab-overflow-indicator.png",
+    platforms: ["linux", "win", "macosx"],
+  },
+  {
+    file: "chrome://browser/skin/stop.svg",
+    platforms: ["linux", "win", "macosx"],
+  },
+  {
+    file: "chrome://browser/skin/sidebars.svg",
+    platforms: ["linux", "win", "macosx"],
+    intermittentNotLoaded: ["macosx"],
+  },
+  {
+    file: "chrome://pocket-shared/skin/pocket.svg",
+    platforms: ["linux", "win", "macosx"],
+    intermittentNotLoaded: ["macosx"],
+  },
+  {
+    file: "chrome://browser/skin/places/toolbarDropMarker.png",
+    platforms: ["linux", "win", "macosx"],
+  },
+  {
+    file: "chrome://browser/skin/bookmark-hollow.svg",
+    platforms: ["linux", "win", "macosx"],
+  },
+  {
+    file: "chrome://browser/skin/tracking-protection-16.svg#enabled",
+    platforms: ["linux", "win", "macosx"],
+  },
+  {
+    file: "chrome://browser/skin/toolbarbutton-dropdown-arrow.png",
+    platforms: ["win"],
+  },
+  {
+    file: "chrome://global/skin/icons/autoscroll.png",
+    platforms: ["linux", "win", "macosx"],
+  },
+
+  {
+    file: "chrome://browser/skin/tabbrowser/tab-background-end.png",
+    hidpi: "chrome://browser/skin/tabbrowser/tab-background-end@2x.png",
+    platforms: ["linux", "win", "macosx"],
+  },
+  {
+    file: "chrome://browser/skin/tabbrowser/tab-background-middle.png",
+    hidpi: "chrome://browser/skin/tabbrowser/tab-background-middle@2x.png",
+    platforms: ["linux", "win", "macosx"],
+  },
+  {
+    file: "chrome://browser/skin/tabbrowser/tab-background-start.png",
+    hidpi: "chrome://browser/skin/tabbrowser/tab-background-start@2x.png",
+    platforms: ["linux", "win", "macosx"],
+  },
+  {
+    file: "chrome://browser/skin/tabbrowser/tabDragIndicator.png",
+    hidpi: "chrome://browser/skin/tabbrowser/tabDragIndicator@2x.png",
+    platforms: ["linux", "win", "macosx"],
+  },
+
+  {
+    file: "resource://gre-resources/loading-image.png",
+    platforms: ["win", "macosx"],
+    intermittentNotLoaded: ["win", "macosx"],
+  },
+  {
+    file: "resource://gre-resources/broken-image.png",
+    platforms: ["win", "macosx"],
+    intermittentNotLoaded: ["win", "macosx"],
+  },
+
+  {
+    file: "chrome://browser/skin/places/unfiledBookmarks.png",
+    hidpi: "<not loaded>",
+    platforms: ["win", "macosx"],
+    intermittentNotLoaded: ["win", "macosx"],
+  },
+  {
+    file: "chrome://browser/skin/urlbar-history-dropmarker.png",
+    hidpi: "<not loaded>",
+    platforms: ["win", "macosx"],
+    intermittentShown: ["win", "macosx"],
+  },
+
+  {
+    file: "chrome://browser/skin/yosemite/tab-selected-start-inactive.svg",
+    platforms: ["macosx"],
+  },
+  {
+    file: "chrome://browser/skin/yosemite/tab-active-middle-inactive.png",
+    platforms: ["macosx"],
+  },
+  {
+    file: "chrome://browser/skin/yosemite/tab-selected-end-inactive.svg",
+    platforms: ["macosx"],
+  },
+  {
+    file: "chrome://browser/skin/yosemite/tab-stroke-start-inactive.png",
+    platforms: ["macosx"],
+  },
+  {
+    file: "chrome://browser/skin/yosemite/tab-stroke-end-inactive.png",
+    platforms: ["macosx"],
+  },
+
+  {
+    file: "chrome://browser/skin/tabbrowser/newtab.png",
+    platforms: ["macosx"],
+  },
+
+  {
+    file: "chrome://global/skin/icons/chevron.png",
+    hidpi: "chrome://global/skin/icons/chevron@2x.png",
+    platforms: ["macosx"],
+  },
+
+  {
+    file: "chrome://browser/skin/tabbrowser/alltabs-box-bkgnd-icon.png",
+    hidpi: "chrome://browser/skin/tabbrowser/alltabs-box-bkgnd-icon@2x.png",
+    platforms: ["macosx"],
+  },
+
+  {
+    file: "chrome://global/skin/toolbar/chevron.gif",
+    platforms: ["win", "linux"],
+  },
+  {
+    file: "chrome://browser/skin/reload-stop-go.png",
+    platforms: ["win", "linux"],
+    intermittentShown: ["win", "linux"],
+  },
+
+  {
+    file: "chrome://browser/skin/tabbrowser/alltabs.png",
+    platforms: ["linux"],
+  },
+
+  {
+    file: "chrome://browser/skin/tabbrowser/tab-arrow-left.svg",
+    platforms: ["win"],
+  },
+
+  {
+    file: "chrome://global/skin/icons/resizer.png",
+    platforms: ["win"],
+  },
+
+  {
+    file: "chrome://global/skin/icons/resizer.png",
+    platforms: ["win"],
+  },
+
+  {
+    file: "chrome://browser/skin/tabbrowser/tab-arrow-left.png",
+    hidpi: "chrome://browser/skin/tabbrowser/tab-arrow-left@2x.png",
+    platforms: ["linux", "macosx"],
+  },
+  {
+    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;
+  let platformWhitelist = whitelist.filter(el => el.platforms.includes(AppConstants.platform));
+
+  let loadedImages = data["image-loading"];
+  let shownImages = data["image-drawing"];
+
+  for (let loaded of loadedImages.values()) {
+    let whitelistItem = platformWhitelist.find(el => {
+      if (window.devicePixelRatio >= 2 && el.hidpi && el.hidpi == loaded) {
+        return true;
+      }
+      return el.file == loaded;
+    });
+    if (whitelistItem) {
+      if (!whitelistItem.intermittentShown ||
+          !whitelistItem.intermittentShown.includes(AppConstants.platform)) {
+        todo(shownImages.has(loaded), `Whitelisted image ${loaded} should not have been shown.`);
+      }
+      continue;
+    }
+    ok(shownImages.has(loaded), `Loaded image ${loaded} should have been shown.`);
+  }
+
+  // Check for unneeded whitelist entries.
+  for (let item of platformWhitelist) {
+    if (!item.intermittentNotLoaded ||
+        !item.intermittentNotLoaded.includes(AppConstants.platform)) {
+      if (window.devicePixelRatio >= 2 && item.hidpi) {
+        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
@@ -21,25 +21,31 @@ if (AppConstants.platform == "linux")
   * The records are meant to be used by startup tests in
   * browser/base/content/test/performance
   * This component only exists in nightly and debug builds, it doesn't ship in
   * our release builds.
   */
 function startupRecorder() {
   this.wrappedJSObject = this;
   this.loader = Cc["@mozilla.org/moz/jsloader;1"].getService(Ci.xpcIJSModuleLoader);
-  this.data = {};
+  this.data = {
+    images: {
+      "image-drawing": new Set(),
+      "image-loading": new Set(),
+    },
+    code: {}
+  };
 }
 startupRecorder.prototype = {
   classID: Components.ID("{11c095b2-e42e-4bdf-9dd0-aed87595f6a4}"),
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
 
   record(name) {
-    this.data[name] = {
+    this.data.code[name] = {
       components: this.loader.loadedComponents(),
       modules: this.loader.loadedModules(),
       services: Object.keys(Cc).filter(c => {
         try {
           Cm.isServiceInstantiatedByContractID(c, Ci.nsISupports);
           return true;
         } catch (e) {
           return false;
@@ -52,32 +58,42 @@ startupRecorder.prototype = {
 
     if (topic == "app-startup") {
       // We can't ensure our observer will be called first or last, so the list of
       // topics we observe here should avoid the topics used to trigger things
       // during startup (eg. the topics observed by nsBrowserGlue.js).
       let topics = [
         "profile-do-change", // This catches stuff loaded during app-startup
         "toplevel-window-ready", // Catches stuff from final-ui-startup
+        "image-loading",
+        "image-drawing",
         firstPaintNotification,
         "sessionstore-windows-restored",
       ];
       for (let t of topics)
         Services.obs.addObserver(this, t);
       return;
     }
 
+    if (topic == "image-drawing" || topic == "image-loading") {
+      this.data.images[topic].add(data);
+      return;
+    }
+
     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(
         this.record.bind(this, "before handling user events"));
+
+      Services.obs.removeObserver(this, "image-drawing");
+      Services.obs.removeObserver(this, "image-loading");
     } 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/image/ImageFactory.cpp
+++ b/image/ImageFactory.cpp
@@ -87,16 +87,28 @@ ImageFactory::CreateImage(nsIRequest* aR
                           uint32_t aInnerWindowId)
 {
   MOZ_ASSERT(gfxPrefs::SingletonExists(),
              "Pref observers should have been initialized already");
 
   // Compute the image's initialization flags.
   uint32_t imageFlags = ComputeImageFlags(aURI, aMimeType, aIsMultiPart);
 
+#ifdef DEBUG
+  // Record the image load for startup performance testing.
+  if (NS_IsMainThread()) {
+    nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+    if (NS_WARN_IF(obs)) {
+      nsAutoCString spec;
+      aURI->GetSpec(spec);
+      obs->NotifyObservers(nullptr, "image-loading", NS_ConvertUTF8toUTF16(spec).get());
+    }
+  }
+#endif
+
   // Select the type of image to create based on MIME type.
   if (aMimeType.EqualsLiteral(IMAGE_SVG_XML)) {
     return CreateVectorImage(aRequest, aProgressTracker, aMimeType,
                              aURI, imageFlags, aInnerWindowId);
   } else {
     return CreateRasterImage(aRequest, aProgressTracker, aMimeType,
                              aURI, imageFlags, aInnerWindowId);
   }
--- a/image/RasterImage.cpp
+++ b/image/RasterImage.cpp
@@ -1390,16 +1390,29 @@ RasterImage::DrawInternal(DrawableSurfac
                           SamplingFilter aSamplingFilter,
                           uint32_t aFlags,
                           float aOpacity)
 {
   gfxContextMatrixAutoSaveRestore saveMatrix(aContext);
   ImageRegion region(aRegion);
   bool frameIsFinished = aSurface->IsFinished();
 
+#ifdef DEBUG
+  // Record the image drawing for startup performance testing.
+  if (NS_IsMainThread()) {
+    nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+    if (NS_WARN_IF(obs)) {
+      nsCOMPtr<nsIURI> imageURI = mURI->ToIURI();
+      nsAutoCString spec;
+      imageURI->GetSpec(spec);
+      obs->NotifyObservers(nullptr, "image-drawing", NS_ConvertUTF8toUTF16(spec).get());
+    }
+  }
+#endif
+
   // By now we may have a frame with the requested size. If not, we need to
   // adjust the drawing parameters accordingly.
   IntSize finalSize = aSurface->GetImageSize();
   bool couldRedecodeForBetterFrame = false;
   if (finalSize != aSize) {
     gfx::Size scale(double(aSize.width) / finalSize.width,
                     double(aSize.height) / finalSize.height);
     aContext->Multiply(gfxMatrix::Scaling(scale.width, scale.height));
--- a/image/VectorImage.cpp
+++ b/image/VectorImage.cpp
@@ -1020,16 +1020,29 @@ VectorImage::Show(gfxDrawable* aDrawable
   MOZ_ASSERT(aDrawable, "Should have a gfxDrawable by now");
   gfxUtils::DrawPixelSnapped(aParams.context, aDrawable,
                              aParams.size,
                              aParams.region,
                              SurfaceFormat::B8G8R8A8,
                              aParams.samplingFilter,
                              aParams.flags, aParams.opacity);
 
+#ifdef DEBUG
+  // Record the image drawing for startup performance testing.
+  if (NS_IsMainThread()) {
+    nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+    if (NS_WARN_IF(obs)) {
+      nsCOMPtr<nsIURI> imageURI = mURI->ToIURI();
+      nsAutoCString spec;
+      imageURI->GetSpec(spec);
+      obs->NotifyObservers(nullptr, "image-drawing", NS_ConvertUTF8toUTF16(spec).get());
+    }
+  }
+#endif
+
   MOZ_ASSERT(mRenderingObserver, "Should have a rendering observer by now");
   mRenderingObserver->ResumeHonoringInvalidations();
 }
 
 void
 VectorImage::RecoverFromLossOfSurfaces()
 {
   NS_WARNING("An imgFrame became invalid. Attempting to recover...");