Bug 650968 - Enabling a lightweight theme (Persona) causes significant startup slowness; r=felipe
authorTim Taubert <tim.taubert@gmx.de>
Sat, 22 Sep 2012 21:24:26 +0200
changeset 107954 87a7a1c60a3ad344ae9e1b8f0f70997e4ea91e1c
parent 107953 4e6eb2cb00b5246bd71af68b56faf67667bd6dff
child 107955 95f1163d4fe35a603634647046da9ff2500135ef
push id82
push usershu@rfrn.org
push dateFri, 05 Oct 2012 13:20:22 +0000
reviewersfelipe
bugs650968
milestone18.0a1
Bug 650968 - Enabling a lightweight theme (Persona) causes significant startup slowness; r=felipe
toolkit/content/LightweightThemeConsumer.jsm
toolkit/mozapps/extensions/LightweightThemeImageOptimizer.jsm
toolkit/mozapps/extensions/LightweightThemeManager.jsm
toolkit/mozapps/extensions/Makefile.in
--- a/toolkit/content/LightweightThemeConsumer.jsm
+++ b/toolkit/content/LightweightThemeConsumer.jsm
@@ -1,47 +1,76 @@
 /* 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/. */
 
 let EXPORTED_SYMBOLS = ["LightweightThemeConsumer"];
 
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeImageOptimizer",
+  "resource://gre/modules/LightweightThemeImageOptimizer.jsm");
+
 function LightweightThemeConsumer(aDocument) {
   this._doc = aDocument;
+  this._win = aDocument.defaultView;
   this._footerId = aDocument.documentElement.getAttribute("lightweightthemesfooter");
 
+  let screen = this._win.screen;
+  this._lastScreenWidth = screen.width;
+  this._lastScreenHeight = screen.height;
+
   Components.classes["@mozilla.org/observer-service;1"]
             .getService(Components.interfaces.nsIObserverService)
             .addObserver(this, "lightweight-theme-styling-update", false);
 
   var temp = {};
   Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", temp);
   this._update(temp.LightweightThemeManager.currentThemeForDisplay);
+  this._win.addEventListener("resize", this);
 }
 
 LightweightThemeConsumer.prototype = {
+  _lastData: null,
+  _lastScreenWidth: null,
+  _lastScreenHeight: null,
+
   observe: function (aSubject, aTopic, aData) {
     if (aTopic != "lightweight-theme-styling-update")
       return;
 
     this._update(JSON.parse(aData));
   },
 
+  handleEvent: function (aEvent) {
+    let {width, height} = this._win.screen;
+
+    if (this._lastScreenWidth != width || this._lastScreenHeight != height) {
+      this._lastScreenWidth = width;
+      this._lastScreenHeight = height;
+      this._update(this._lastData);
+    }
+  },
+
   destroy: function () {
     Components.classes["@mozilla.org/observer-service;1"]
               .getService(Components.interfaces.nsIObserverService)
               .removeObserver(this, "lightweight-theme-styling-update");
 
-    this._doc = null;
+    this._win.removeEventListener("resize", this);
+    this._win = this._doc = null;
   },
 
   _update: function (aData) {
     if (!aData)
       aData = { headerURL: "", footerURL: "", textcolor: "", accentcolor: "" };
 
+    this._lastData = aData;
+    aData = LightweightThemeImageOptimizer.optimize(aData, this._win.screen);
+
     var root = this._doc.documentElement;
     var active = !!aData.headerURL;
 
     if (active) {
       root.style.color = aData.textcolor || "black";
       root.style.backgroundColor = aData.accentcolor || "white";
       let [r, g, b] = _parseRGB(this._doc.defaultView.getComputedStyle(root, "").color);
       let luminance = 0.2125 * r + 0.7154 * g + 0.0721 * b;
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/LightweightThemeImageOptimizer.jsm
@@ -0,0 +1,186 @@
+/* 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";
+
+let EXPORTED_SYMBOLS = ["LightweightThemeImageOptimizer"];
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+  "resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+  "resource://gre/modules/FileUtils.jsm");
+
+const ORIGIN_TOP_RIGHT = 1;
+const ORIGIN_BOTTOM_LEFT = 2;
+
+let LightweightThemeImageOptimizer = {
+  optimize: function LWTIO_optimize(aThemeData, aScreen) {
+    let data = Utils.createCopy(aThemeData);
+    if (!data.headerURL) {
+      return data;
+    }
+
+    data.headerURL = ImageCropper.getCroppedImageURL(
+      data.headerURL, aScreen, ORIGIN_TOP_RIGHT);
+
+    if (data.footerURL) {
+      data.footerURL = ImageCropper.getCroppedImageURL(
+        data.footerURL, aScreen, ORIGIN_BOTTOM_LEFT);
+    }
+
+    return data;
+  },
+
+  purge: function LWTIO_purge() {
+    let dir = FileUtils.getDir("ProfD", ["lwtheme"]);
+    dir.followLinks = false;
+    try {
+      dir.remove(true);
+    } catch (e) {}
+  }
+};
+
+Object.freeze(LightweightThemeImageOptimizer);
+
+let ImageCropper = {
+  _inProgress: {},
+
+  getCroppedImageURL:
+  function ImageCropper_getCroppedImageURL(aImageURL, aScreen, aOrigin) {
+    // We can crop local files, only.
+    if (!aImageURL.startsWith("file://")) {
+      return aImageURL;
+    }
+
+    // Generate the cropped image's file name using its
+    // base name and the current screen size.
+    let uri = Services.io.newURI(aImageURL, null, null);
+    let file = uri.QueryInterface(Ci.nsIFileURL).file;
+
+    // Make sure the source file exists.
+    if (!file.exists()) {
+      return aImageURL;
+    }
+
+    let fileName = file.leafName + "-" + aScreen.width + "x" + aScreen.height;
+    let croppedFile = FileUtils.getFile("ProfD", ["lwtheme", fileName]);
+
+    // If we have a local file that is not in progress, return it.
+    if (croppedFile.exists() && !(croppedFile.path in this._inProgress)) {
+      let fileURI = Services.io.newFileURI(croppedFile);
+
+      // Copy the query part to avoid wrong caching.
+      fileURI.QueryInterface(Ci.nsIURL).query = uri.query;
+      return fileURI.spec;
+    }
+
+    // Crop the given image in the background.
+    this._crop(uri, croppedFile, aScreen, aOrigin);
+
+    // Return the original image while we're waiting for the cropped version
+    // to be written to disk.
+    return aImageURL;
+  },
+
+  _crop: function ImageCropper_crop(aURI, aTargetFile, aScreen, aOrigin) {
+    let inProgress = this._inProgress;
+    inProgress[aTargetFile.path] = true;
+
+    function resetInProgress() {
+      delete inProgress[aTargetFile.path];
+    }
+
+    ImageFile.read(aURI, function (aInputStream, aContentType) {
+      if (aInputStream && aContentType) {
+        let image = ImageTools.decode(aInputStream, aContentType);
+        if (image && image.width && image.height) {
+          let stream = ImageTools.encode(image, aScreen, aOrigin, aContentType);
+          if (stream) {
+            ImageFile.write(aTargetFile, stream, resetInProgress);
+            return;
+          }
+        }
+      }
+
+      resetInProgress();
+    });
+  }
+};
+
+let ImageFile = {
+  read: function ImageFile_read(aURI, aCallback) {
+    this._netUtil.asyncFetch(aURI, function (aInputStream, aStatus, aRequest) {
+      if (Components.isSuccessCode(aStatus) && aRequest instanceof Ci.nsIChannel) {
+        let channel = aRequest.QueryInterface(Ci.nsIChannel);
+        aCallback(aInputStream, channel.contentType);
+      } else {
+        aCallback();
+      }
+    });
+  },
+
+  write: function ImageFile_write(aFile, aInputStream, aCallback) {
+    let fos = FileUtils.openSafeFileOutputStream(aFile);
+    this._netUtil.asyncCopy(aInputStream, fos, function (aResult) {
+      FileUtils.closeSafeFileOutputStream(fos);
+
+      // Remove the file if writing was not successful.
+      if (!Components.isSuccessCode(aResult)) {
+        try {
+          aFile.remove(false);
+        } catch (e) {}
+      }
+
+      aCallback();
+    });
+  }
+};
+
+XPCOMUtils.defineLazyModuleGetter(ImageFile, "_netUtil",
+  "resource://gre/modules/NetUtil.jsm", "NetUtil");
+
+let ImageTools = {
+  decode: function ImageTools_decode(aInputStream, aContentType) {
+    let outParam = {value: null};
+
+    try {
+      this._imgTools.decodeImageData(aInputStream, aContentType, outParam);
+    } catch (e) {}
+
+    return outParam.value;
+  },
+
+  encode: function ImageTools_encode(aImage, aScreen, aOrigin, aContentType) {
+    let stream;
+    let width = Math.min(aImage.width, aScreen.width);
+    let height = Math.min(aImage.height, aScreen.height);
+    let x = aOrigin == ORIGIN_TOP_RIGHT ? aImage.width - width : 0;
+
+    try {
+      stream = this._imgTools.encodeCroppedImage(aImage, aContentType, x, 0,
+                                                 width, height);
+    } catch (e) {}
+
+    return stream;
+  }
+};
+
+XPCOMUtils.defineLazyServiceGetter(ImageTools, "_imgTools",
+  "@mozilla.org/image/tools;1", "imgITools");
+
+let Utils = {
+  createCopy: function Utils_createCopy(aData) {
+    let copy = {};
+    for (let [k, v] in Iterator(aData)) {
+      copy[k] = v;
+    }
+    return copy;
+  }
+};
--- a/toolkit/mozapps/extensions/LightweightThemeManager.jsm
+++ b/toolkit/mozapps/extensions/LightweightThemeManager.jsm
@@ -4,16 +4,17 @@
 
 "use strict";
 
 var EXPORTED_SYMBOLS = ["LightweightThemeManager"];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 Components.utils.import("resource://gre/modules/AddonManager.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
 
 const ID_SUFFIX              = "@personas.mozilla.org";
 const PREF_LWTHEME_TO_SELECT = "extensions.lwThemeToSelect";
 const PREF_GENERAL_SKINS_SELECTEDSKIN = "general.skins.selectedSkin";
 const PREF_EM_DSS_ENABLED    = "extensions.dss.enabled";
 const ADDON_TYPE             = "theme";
@@ -33,16 +34,19 @@ const OPTIONAL = ["footerURL", "textcolo
 
 const PERSIST_ENABLED = true;
 const PERSIST_BYPASS_CACHE = false;
 const PERSIST_FILES = {
   headerURL: "lightweighttheme-header",
   footerURL: "lightweighttheme-footer"
 };
 
+XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeImageOptimizer",
+  "resource://gre/modules/LightweightThemeImageOptimizer.jsm");
+
 __defineGetter__("_prefs", function () {
   delete this._prefs;
   return this._prefs = Services.prefs.getBranch("lightweightThemes.");
 });
 
 __defineGetter__("_maxUsedThemes", function() {
   delete this._maxUsedThemes;
   try {
@@ -222,18 +226,22 @@ var LightweightThemeManager = {
       _previewTimer.cancel();
       _previewTimer = null;
     }
 
     if (aData) {
       let usedThemes = _usedThemesExceptId(aData.id);
       usedThemes.unshift(aData);
       _updateUsedThemes(usedThemes);
-      if (PERSIST_ENABLED)
-        _persistImages(aData);
+      if (PERSIST_ENABLED) {
+        LightweightThemeImageOptimizer.purge();
+        _persistImages(aData, function () {
+          _notifyWindows(this.currentThemeForDisplay);
+        }.bind(this));
+      }
     }
 
     _prefs.setBoolPref("isThemeSelected", aData != null);
     _notifyWindows(aData);
     Services.obs.notifyObservers(null, "lightweight-theme-changed", null);
   },
 
   /**
@@ -711,27 +719,34 @@ function _prefObserver(aSubject, aTopic,
         _maxUsedThemes = DEFAULT_MAX_USED_THEMES_COUNT;
       }
       // Update the theme list to remove any themes over the number we keep
       _updateUsedThemes(LightweightThemeManager.usedThemes);
       break;
   }
 }
 
-function _persistImages(aData) {
+function _persistImages(aData, aCallback) {
   function onSuccess(key) function () {
     let current = LightweightThemeManager.currentTheme;
-    if (current && current.id == aData.id)
+    if (current && current.id == aData.id) {
       _prefs.setBoolPref("persisted." + key, true);
+    }
+    if (--numFilesToPersist == 0 && aCallback) {
+      aCallback();
+    }
   };
 
+  let numFilesToPersist = 0;
   for (let key in PERSIST_FILES) {
     _prefs.setBoolPref("persisted." + key, false);
-    if (aData[key])
+    if (aData[key]) {
+      numFilesToPersist++;
       _persistImage(aData[key], PERSIST_FILES[key], onSuccess(key));
+    }
   }
 }
 
 function _getLocalImageURI(localFileName) {
   var localFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
   localFile.append(localFileName);
   return Services.io.newFileURI(localFile);
 }
--- a/toolkit/mozapps/extensions/Makefile.in
+++ b/toolkit/mozapps/extensions/Makefile.in
@@ -50,16 +50,17 @@ EXTRA_PP_JS_MODULES = \
   XPIProviderUtils.js \
   $(NULL)
 
 EXTRA_JS_MODULES = \
   AddonLogging.jsm \
   AddonRepository.jsm \
   AddonUpdateChecker.jsm \
   ChromeManifestParser.jsm \
+  LightweightThemeImageOptimizer.jsm \
   LightweightThemeManager.jsm \
   PluginProvider.jsm \
   SpellCheckDictionaryBootstrap.js \
   $(NULL)
 
 TEST_DIRS += test
 
 EXTRA_DSO_LDOPTS = \