Bug 617859 - Cache recommended add-ons list for Start page to improve responsiveness [r=mfinkle]
authorBrad Lassey <blassey@mozilla.com>
Thu, 09 Dec 2010 21:52:57 -0800
changeset 67127 502f5ba01208f104e413dc583b0ef08b1c7d9f13
parent 67126 1885e8a2cce44ef785504b8714096e5e717594d8
child 67128 b604caa9df23aa4ff15a2df54933a5f499ff8b8d
push id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmfinkle
bugs617859
Bug 617859 - Cache recommended add-ons list for Start page to improve responsiveness [r=mfinkle]
mobile/chrome/content/aboutHome.xhtml
mobile/components/AddonUpdateService.js
--- a/mobile/chrome/content/aboutHome.xhtml
+++ b/mobile/chrome/content/aboutHome.xhtml
@@ -116,16 +116,17 @@
     function init() {
       initTabs();
       initWeave();
       initAddons();
     }
 
     function uninit() {
       uninitWeave();
+      uninitAddons();
     }
 
     function _readFile(aFile) {
       try {
         let stream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream);
         stream.init(aFile, 0x01, 0, 0);
         let cvstream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(Ci.nsIConverterInputStream);
 
@@ -263,27 +264,57 @@
         // the search as well
         setTimeout(function () {
           // do the search
           chromeWin.ExtensionsView.doSearch(aSearchString);
         }, 0);
       }
     }
 
-    var RecommendedSearchResults = {
-      searchSucceeded: function(aAddons, aAddonCount, aTotalResults) {
+    var RecommendedAddons = {
+      _getFile: function() {
+        let dirService = Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties);
+        let file = dirService.get("ProfD", Ci.nsILocalFile);
+        file.append("recommended-addons.json");
+        return file;
+      },
+
+      _readFile: function(aFile) {
+        try {
+          let stream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream);
+          stream.init(aFile, 0x01, 0, 0);
+          let cvstream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(Ci.nsIConverterInputStream);
+          
+          let fileSize = stream.available();
+          cvstream.init(stream, "UTF-8", fileSize, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
+          let data = {};
+          cvstream.readString(fileSize, data);
+          let content = data.value;
+          cvstream.close();
+          return content.replace(/\r\n?/g, "\n");
+        }
+        catch (ex) {
+          Cu.reportError(ex);
+        }
+        return null;
+      },
+
+      _loadAddons: function(aAddons, aAddonCount, aTotalResults) {
         let list = document.getElementById("newAddons");
-        if (aAddons.length == 0) {
-          let placeHolder = document.getElementById("loadingAddons");
-          placeHolder.innerHTML = "<div class='no-items'>" + document.getElementById("text-noaddons").textContent + "</div>";
+        let loading = document.getElementById("loadingAddons");
+
+        if (aAddons.length == 0 && loading) {
+          loading.innerHTML = "<div class='no-items'>" + document.getElementById("text-noaddons").textContent + "</div>";
           return;
         }
 
-        let loading = document.getElementById("loadingAddons");
-        loading.parentNode.removeChild(loading);
+        // Clear all content but the header before filling the addons
+        let header = list.firstElementChild;
+        while (header.nextSibling)
+          list.removeChild(header.nextSibling);
 
         for (let i=0; i<aAddons.length; i++) {
           let outer = document.createElement("div");
           outer.setAttribute("role", "button");
           outer.setAttribute("addonID", aAddons[i].id);
 
           let addonName = aAddons[i].name;
           outer.addEventListener("click", function() {
@@ -308,25 +339,38 @@
           versionPart.className = "version";
           inner.appendChild(versionPart);
 
           outer.appendChild(inner);
           list.appendChild(outer);
         }
       },
 
-      searchFailed: function searchFailed() {
-        let loading = document.getElementById("newAddons");
-        loading.parentNode.removeChild(loading);
+      loadFromCacheOrScheduleUpdate: function(aDelay) {
+        let file = this._getFile();
+        if (file.exists()) {
+          let json = JSON.parse(this._readFile(file));
+          this._loadAddons(json.addons, json.addonCount, json.totalResults);
+        } else {
+          setTimeout(function() {
+            let aus = Cc["@mozilla.org/browser/addon-update-service;1"].getService(Ci.nsITimerCallback);
+            aus.notify(null);
+          }, aDelay);
+        }
       }
     }
 
-    const kAddonsMaxDisplay = 2;
+    function updateAddons() {
+      // If a cache does not exist, start an update after 10 seconds
+      RecommendedAddons.loadFromCacheOrScheduleUpdate(10000);
+    }
 
     function initAddons() {
-      Cu.import("resource://gre/modules/AddonRepository.jsm");
-      if (AddonRepository.isSearching)
-        AddonRepository.cancelSearch();
-      AddonRepository.retrieveRecommendedAddons(kAddonsMaxDisplay, RecommendedSearchResults);
+       getChromeWin().Services.obs.addObserver(updateAddons, "recommended-addons-cache-updated", false);
+       updateAddons();
+    }
+
+    function uninitAddons() {
+       getChromeWin().Services.obs.removeObserver(updateAddons, "recommended-addons-cache-updated");
     }
   ]]></script>
 </body>
 </html>
--- a/mobile/components/AddonUpdateService.js
+++ b/mobile/components/AddonUpdateService.js
@@ -41,16 +41,27 @@ const Cu = Components.utils;
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "AddonManager", function() {
   Components.utils.import("resource://gre/modules/AddonManager.jsm");
   return AddonManager;
 });
 
+XPCOMUtils.defineLazyGetter(this, "AddonRepository", function() {
+  Components.utils.import("resource://gre/modules/AddonRepository.jsm");
+  return AddonRepository;
+});
+
+XPCOMUtils.defineLazyGetter(this, "NetUtil", function() {
+  Components.utils.import("resource://gre/modules/NetUtil.jsm");
+  return NetUtil;
+});
+
+
 function getPref(func, preference, defaultValue) {
   try {
     return Services.prefs[func](preference);
   }
   catch (e) {}
   return defaultValue;
 }
 
@@ -92,16 +103,18 @@ AddonUpdateService.prototype = {
           data.data = JSON.stringify({ id: aAddon.id, name: aAddon.name });
           Services.obs.notifyObservers(data, "addon-update-started", null);
 
           let listener = new UpdateCheckListener();
           aAddon.findUpdates(listener, reason);
         }
       });
     });
+
+    RecommendedSearchResults.search();
   }
 };
 
 // -----------------------------------------------------------------------
 // Add-on update listener. Starts a download for any add-on with a viable
 // update waiting
 // -----------------------------------------------------------------------
 
@@ -135,10 +148,75 @@ UpdateCheckListener.prototype = {
 
     if (aError)
       this._status = "error";
 
     Services.obs.notifyObservers(data, "addon-update-ended", this._status);
   }
 };
 
+// -----------------------------------------------------------------------
+// RecommendedSearchResults fetches add-on data and saves it to a cache
+// -----------------------------------------------------------------------
+
+var RecommendedSearchResults = {
+  _getFile: function() {
+    let dirService = Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties);
+    let file = dirService.get("ProfD", Ci.nsILocalFile);
+    file.append("recommended-addons.json");
+    return file;
+  },
+
+  _writeFile: function (aFile, aData) {
+    if (!aData)
+      return;
+
+    let stateString = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
+    stateString.data = aData;
+
+    // Initialize the file output stream.
+    let ostream = Cc["@mozilla.org/network/safe-file-output-stream;1"].createInstance(Ci.nsIFileOutputStream);
+    ostream.init(aFile, 0x02 | 0x08 | 0x20, 0600, 0);
+
+    // Obtain a converter to convert our data to a UTF-8 encoded input stream.
+    let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter);
+    converter.charset = "UTF-8";
+
+    // Asynchronously copy the data to the file.
+    let istream = converter.convertToInputStream(aData);
+    NetUtil.asyncCopy(istream, ostream, function(rc) {
+      if (Components.isSuccessCode(rc))
+        Services.obs.notifyObservers(null, "recommended-addons-cache-updated", "");
+    });
+  },
+  
+  searchSucceeded: function(aAddons, aAddonCount, aTotalResults) {
+    let json = {
+      addons: aAddons,
+      addonCount: aAddonCount,
+      totalResults: aTotalResults
+    };
+
+    // Avoid any NSS costs. Convert https to http.
+    json.addons.forEach(function(aAddon){
+      aAddon.iconURL = aAddon.iconURL.replace(/^https/, "http");
+    });
+
+    let file = this._getFile();
+    this._writeFile(file, JSON.stringify(json));
+  },
+  
+  searchFailed: function searchFailed() {
+    let loading = document.getElementById("newAddons");
+    loading.parentNode.removeChild(loading);
+  },
+  
+  search: function() {
+    const kAddonsMaxDisplay = 2;
+
+    if (AddonRepository.isSearching)
+      AddonRepository.cancelSearch();
+    AddonRepository.retrieveRecommendedAddons(kAddonsMaxDisplay, RecommendedSearchResults);
+  }
+}
+
 const NSGetFactory = XPCOMUtils.generateNSGetFactory([AddonUpdateService]);