Bug 614416: Building add-on lists is too slow. r=Unfocused, a=blocks-final
authorDave Townsend <dtownsend@oxymoronical.com>
Tue, 30 Nov 2010 14:18:00 -0800
changeset 58575 61deb05747d6
parent 58574 d12aec78e086
child 58576 24685c64a5c8
push id17365
push userdtownsend@mozilla.com
push dateFri, 03 Dec 2010 21:50:46 +0000
treeherdermozilla-central@24685c64a5c8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersUnfocused, blocks-final
bugs614416
milestone2.0b8pre
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 614416: Building add-on lists is too slow. r=Unfocused, a=blocks-final
toolkit/mozapps/extensions/content/extensions.js
toolkit/mozapps/extensions/content/extensions.xml
toolkit/mozapps/extensions/content/extensions.xul
toolkit/mozapps/extensions/test/browser/browser_recentupdates.js
toolkit/mozapps/extensions/test/browser/browser_sorting.js
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -69,18 +69,16 @@ const SEARCH_SCORE_MATCH_SUBSTRING = 3;
 
 const UPDATES_RECENT_TIMESPAN = 2 * 24 * 3600000; // 2 days (in milliseconds)
 const UPDATES_RELEASENOTES_TRANSFORMFILE = "chrome://mozapps/content/extensions/updateinfo.xsl";
 
 const XMLURI_PARSE_ERROR = "http://www.mozilla.org/newlayout/xml/parsererror.xml"
 
 const VIEW_DEFAULT = "addons://list/extension";
 
-const INTEGER_FIELDS = ["dateUpdated", "size", "relevancescore"];
-
 var gStrings = {};
 XPCOMUtils.defineLazyServiceGetter(gStrings, "bundleSvc",
                                    "@mozilla.org/intl/stringbundle;1",
                                    "nsIStringBundleService");
 
 XPCOMUtils.defineLazyGetter(gStrings, "brand", function() {
   return this.bundleSvc.createBundle("chrome://branding/locale/brand.properties");
 });
@@ -1158,26 +1156,91 @@ function createItem(aObj, aIsInstall, aI
   item.mAddon = aObj;
 
   item.setAttribute("status", "installed");
 
   // set only attributes needed for sorting and XBL binding,
   // the binding handles the rest
   item.setAttribute("value", aObj.id);
 
-  // The XUL sort service only supports 32 bit integers so we strip the
-  // milliseconds to make this small enough
-  if (aObj.updateDate)
-    item.setAttribute("dateUpdated", aObj.updateDate.getTime() / 1000);
-
-  if (aObj.size)
-    item.setAttribute("size", aObj.size);
   return item;
 }
 
+function sortElements(aElements, aSortBy, aAscending) {
+  const DATE_FIELDS = ["updateDate"];
+  const INTEGER_FIELDS = ["size", "relevancescore"];
+
+  function dateCompare(a, b) {
+    var aTime = a.getTime();
+    var bTime = b.getTime();
+    if (aTime < bTime)
+      return -1;
+    if (aTime > bTime)
+      return 1;
+    return 0;
+  }
+
+  function intCompare(a, b) {
+    return a - b;
+  }
+
+  function stringCompare(a, b) {
+    return a.localeCompare(b);
+  }
+
+  function getValue(aObj) {
+    if (!aObj)
+      return null;
+
+    if (aObj.hasAttribute(aSortBy))
+      return aObj.getAttribute(aSortBy);
+
+    addon = aObj.mAddon || aObj.mInstall;
+    if (!addon)
+      return null;
+
+    return addon[aSortBy];
+  }
+
+  var sortFunc = stringCompare;
+  if (DATE_FIELDS.indexOf(aSortBy) != -1)
+    sortFunc = dateCompare;
+  else if (INTEGER_FIELDS.indexOf(aSortBy) != -1)
+    sortFunc = intCompare;
+
+  aElements.sort(function(a, b) {
+    if (!aAscending)
+      [a, b] = [b, a];
+
+    var aValue = getValue(a);
+    var bValue = getValue(b);
+
+    if (!aValue && !bValue)
+      return 0;
+    if (!aValue)
+      return -1;
+    if (!bValue)
+      return 1;
+
+    return sortFunc(aValue, bValue);
+  });
+}
+
+function sortList(aList, aSortBy, aAscending) {
+  var elements = Array.slice(aList.childNodes, 0);
+  sortElements(elements, aSortBy, aAscending);
+
+  while (aList.listChild)
+    aList.removeChild(aList.lastChild);
+
+  elements.forEach(function(aElement) {
+    aList.appendChild(aElement);
+  });
+}
+
 function getAddonsAndInstalls(aType, aCallback) {
   var addonTypes = null, installTypes = null;
   if (aType != null) {
     addonTypes = [aType];
     installTypes = [aType];
     if (aType == "extension")
       installTypes = addonTypes.concat("");
   }
@@ -1557,56 +1620,59 @@ var gSearchView = {
     while (this._listBox.firstChild.localName == "richlistitem")
       this._listBox.removeChild(this._listBox.firstChild);
 
     var self = this;
     gCachedAddons = {};
     this._pendingSearches = 2;
     this._sorters.setSort("relevancescore", false);
 
+    var elements = [];
+
     function createSearchResults(aObjsList, aIsInstall, aIsRemote) {
-      var createdCount = 0;
       aObjsList.forEach(function(aObj) {
         let score = 0;
         if (aQuery.length > 0) {
           score = self.getMatchScore(aObj, aQuery);
           if (score == 0 && !aIsRemote)
             return;
         }
 
         let item = createItem(aObj, aIsInstall, aIsRemote);
         item.setAttribute("relevancescore", score);
         if (aIsRemote)
           gCachedAddons[aObj.id] = aObj;
 
-        self._listBox.insertBefore(item, self._listBox.lastChild);
-        createdCount++;
+        elements.push(item);
       });
-
-      return createdCount;
     }
 
     function finishSearch(createdCount) {
-      if (createdCount > 0)
-        self.onSortChanged(self._sorters.sortBy, self._sorters.ascending);
+      if (elements.length > 0) {
+        sortElements(elements, self._sorters.sortBy, self._sorters.ascending);
+        elements.forEach(function(aElement) {
+          self._listBox.insertBefore(aElement, self._listBox.lastChild);
+        });
+        self.updateListAttributes();
+      }
 
       self._pendingSearches--;
       self.updateView();
 
       if (!self.isSearching)
         gViewController.notifyViewChanged();
     }
 
     getAddonsAndInstalls(null, function(aAddons, aInstalls) {
       if (gViewController && aRequest != gViewController.currentViewRequest)
         return;
 
-      var createdCount = createSearchResults(aAddons, false, false);
-      createdCount += createSearchResults(aInstalls, true, false);
-      finishSearch(createdCount);
+      createSearchResults(aAddons, false, false);
+      createSearchResults(aInstalls, true, false);
+      finishSearch();
     });
 
     var maxRemoteResults = 0;
     try {
       maxRemoteResults = Services.prefs.getIntPref(PREF_MAXRESULTS);
     } catch(e) {}
 
     if (maxRemoteResults <= 0) {
@@ -1743,28 +1809,17 @@ var gSearchView = {
     linkStr = linkStr.replace("#1", aTotalResults);
     this._allResultsLink.setAttribute("value", linkStr);
 
     this._allResultsLink.setAttribute("href",
                                       AddonRepository.getSearchURL(this._lastQuery));
     this._allResultsLink.hidden = false;
  },
 
-  onSortChanged: function(aSortBy, aAscending) {
-    var footer = this._listBox.lastChild;
-    this._listBox.removeChild(footer);
-
-    var hints = aAscending ? "ascending" : "descending";
-    if (INTEGER_FIELDS.indexOf(aSortBy) >= 0)
-      hints += " integer";
-
-    var sortService = Cc["@mozilla.org/xul/xul-sort-service;1"].
-                      getService(Ci.nsIXULSortService);
-    sortService.sort(this._listBox, aSortBy, hints);
-
+  updateListAttributes: function() {
     var item = this._listBox.querySelector("richlistitem[remote='true'][first]");
     if (item)
       item.removeAttribute("first");
     item = this._listBox.querySelector("richlistitem[remote='true'][last]");
     if (item)
       item.removeAttribute("last");
     var items = this._listBox.querySelectorAll("richlistitem[remote='true']");
     if (items.length > 0) {
@@ -1779,16 +1834,25 @@ var gSearchView = {
     if (item)
       item.removeAttribute("last");
     items = this._listBox.querySelectorAll("richlistitem:not([remote='true'])");
     if (items.length > 0) {
       items[0].setAttribute("first", true);
       items[items.length - 1].setAttribute("last", true);
     }
 
+  },
+
+  onSortChanged: function(aSortBy, aAscending) {
+    var footer = this._listBox.lastChild;
+    this._listBox.removeChild(footer);
+
+    sortList(this._listBox, aSortBy, aAscending);
+    this.updateListAttributes();
+
     this._listBox.appendChild(footer);
   },
 
   onDownloadCancelled: function(aInstall) {
     this.removeInstall(aInstall);
   },
 
   onInstallCancelled: function(aInstall) {
@@ -1855,31 +1919,33 @@ var gListView = {
     while (this._listBox.itemCount > 0)
       this._listBox.removeItemAt(0);
 
     var self = this;
     var types = getAddonsAndInstalls(aType, function(aAddonsList, aInstallsList) {
       if (gViewController && aRequest != gViewController.currentViewRequest)
         return;
 
-      for (let i = 0; i < aAddonsList.length; i++) {
-        let item = createItem(aAddonsList[i]);
-        self._listBox.appendChild(item);
+      var elements = [];
+
+      for (let i = 0; i < aAddonsList.length; i++)
+        elements.push(createItem(aAddonsList[i]));
+
+      for (let i = 0; i < aInstallsList.length; i++)
+        elements.push(createItem(aInstallsList[i], true));
+
+      if (elements.length > 0) {
+        sortElements(elements, self._sorters.sortBy, self._sorters.ascending);
+        elements.forEach(function(aElement) {
+          self._listBox.appendChild(aElement);
+        });
+      } else {
+        self.showEmptyNotice(true);
       }
 
-      for (let i = 0; i < aInstallsList.length; i++) {
-        let item = createItem(aInstallsList[i], true);
-        self._listBox.appendChild(item);
-      }
-
-      if (self._listBox.childElementCount > 0)
-        self.onSortChanged(self._sorters.sortBy, self._sorters.ascending);
-      else
-        self.showEmptyNotice(true);
-
       gEventManager.registerInstallListener(self);
       gViewController.updateCommands();
       gViewController.notifyViewChanged();
     });
 
     this._types = types.addon;
     this._installTypes = types.install;
   },
@@ -1901,23 +1967,17 @@ var gListView = {
     items.forEach(function(aAddon) { aAddon.uninstall(); });
   },
 
   showEmptyNotice: function(aShow) {
     this._emptyNotice.hidden = !aShow;
   },
 
   onSortChanged: function(aSortBy, aAscending) {
-    var hints = aAscending ? "ascending" : "descending";
-    if (INTEGER_FIELDS.indexOf(aSortBy) >= 0)
-      hints += " integer";
-
-    var sortService = Cc["@mozilla.org/xul/xul-sort-service;1"].
-                      getService(Ci.nsIXULSortService);
-    sortService.sort(this._listBox, aSortBy, hints);
+    sortList(this._listBox, aSortBy, aAscending);
   },
 
   onNewInstall: function(aInstall) {
     // Ignore any upgrade installs
     if (aInstall.existingAddon)
       return;
 
     var item = createItem(aInstall, true);
@@ -2397,29 +2457,33 @@ var gUpdatesView = {
   },
 
   _showRecentUpdates: function(aRequest) {
     var self = this;
     AddonManager.getAllAddons(function(aAddonsList) {
       if (gViewController && aRequest != gViewController.currentViewRequest)
         return;
 
+      var elements = [];
       let threshold = Date.now() - UPDATES_RECENT_TIMESPAN;
       aAddonsList.forEach(function(aAddon) {
         if (!aAddon.updateDate || aAddon.updateDate.getTime() < threshold)
           return;
 
-        let item = createItem(aAddon);
-        self._listBox.appendChild(item);
+        elements.push(createItem(aAddon));
       });
 
-      if (self._listBox.itemCount > 0)
-        self.onSortChanged(self._sorters.sortBy, self._sorters.ascending);
-      else
+      if (elements.length > 0) {
+        sortElements(elements, self._sorters.sortBy, self._sorters.ascending);
+        elements.forEach(function(aElement) {
+          self._listBox.appendChild(aElement);
+        });
+      } else {
         self.showEmptyNotice(true);
+      }
 
       gViewController.notifyViewChanged();
     });
   },
 
   _showAvailableUpdates: function(aIsRefresh, aRequest) {
     /* Disable the Update Selected button so it can't get clicked
        before everything is initialized asynchronously.
@@ -2435,31 +2499,36 @@ var gUpdatesView = {
       if (aIsRefresh) {
         self.showEmptyNotice(false);
         self._updateSelected.hidden = true;
 
         while (self._listBox.itemCount > 0)
           self._listBox.removeItemAt(0);
       }
 
+      var elements = [];
+
       aInstallsList.forEach(function(aInstall) {
         if (!self.isManualUpdate(aInstall))
           return;
 
         let item = createItem(aInstall.existingAddon);
         item.setAttribute("upgrade", true);
         item.addEventListener("IncludeUpdateChanged", function() {
           self.maybeDisableUpdateSelected();
         }, false);
-        self._listBox.appendChild(item);
+        elements.push(item);
       });
 
-      if (self._listBox.itemCount > 0) {
+      if (elements.length > 0) {
         self._updateSelected.hidden = false;
-        self.onSortChanged(self._sorters.sortBy, self._sorters.ascending);
+        sortElements(elements, self._sorters.sortBy, self._sorters.ascending);
+        elements.forEach(function(aElement) {
+          self._listBox.appendChild(aElement);
+        });
       } else {
         self.showEmptyNotice(true);
       }
 
       // ensure badge count is in sync
       self._categoryItem.badgeCount = self._listBox.itemCount;
 
       gViewController.notifyViewChanged();
@@ -2570,23 +2639,17 @@ var gUpdatesView = {
       if (listitem.mAddon.id == aId)
         return listitem;
       listitem = listitem.nextSibling;
     }
     return null;
   },
 
   onSortChanged: function(aSortBy, aAscending) {
-    var hints = aAscending ? "ascending" : "descending";
-    if (INTEGER_FIELDS.indexOf(aSortBy) >= 0)
-      hints += " integer";
-
-    var sortService = Cc["@mozilla.org/xul/xul-sort-service;1"].
-                      getService(Ci.nsIXULSortService);
-    sortService.sort(this._listBox, aSortBy, hints);
+    sortList(this._listBox, aSortBy, aAscending);
   },
 
   onNewInstall: function(aInstall) {
     if (!this.isManualUpdate(aInstall))
       return;
     this.maybeRefresh();
   },
 
--- a/toolkit/mozapps/extensions/content/extensions.xml
+++ b/toolkit/mozapps/extensions/content/extensions.xml
@@ -241,17 +241,17 @@
   <binding id="sorters">
     <content orient="horizontal">
       <xul:button anonid="name-btn" class="sorter"
                   label="&sort.name.label;" tooltiptext="&sort.name.tooltip;"
                   oncommand="this.parentNode._handleChange('name');"/>
       <xul:button anonid="date-btn" class="sorter"
                   label="&sort.dateUpdated.label;"
                   tooltiptext="&sort.dateUpdated.tooltip;"
-                  oncommand="this.parentNode._handleChange('dateUpdated');"/>
+                  oncommand="this.parentNode._handleChange('updateDate');"/>
       <xul:button anonid="relevance-btn" class="sorter" hidden="true"
                   label="&sort.relevance.label;"
                   tooltiptext="&sort.relevance.tooltip;"
                   oncommand="this.parentNode._handleChange('relevancescore');"/>
     </content>
 
     <implementation>
       <constructor><![CDATA[
@@ -343,17 +343,17 @@
           if (sortBy == "name") {
             this._btnName.checkState = checkState;
             this._btnName.checked = true;
           } else {
             this._btnName.checkState = 0;
             this._btnName.checked = false;
           }
 
-          if (sortBy == "dateUpdated") {
+          if (sortBy == "updateDate") {
             this._btnDate.checkState = checkState;
             this._btnDate.checked = true;
           } else {
             this._btnDate.checkState = 0;
             this._btnDate.checked = false;
           }
 
           if (sortBy == "relevancescore") {
@@ -1029,21 +1029,16 @@
           this.mAddon = aAddon;
 
           this._installStatus.mAddon = this.mAddon;
           this._updateDates();
           this._updateState();
 
           this.setAttribute("name", aAddon.name);
 
-          if (aAddon.size)
-            this.setAttribute("size", aAddon.size);
-          else
-            this.removeAttribute("size");
-
           var iconURL = this.mAddon.iconURL;
           if (iconURL)
             this._icon.src = iconURL;
           else
             this._icon.src = null;
 
           if (this.mAddon.version)
             this._version.value = this.mAddon.version;
@@ -1106,23 +1101,20 @@
                      .FormatDate("",
                                  Ci.nsIScriptableDateFormat.dateFormatLong,
                                  aDate.getFullYear(),
                                  aDate.getMonth() + 1,
                                  aDate.getDate()
                                  );
           }
 
-          if (this.mAddon.updateDate) {
+          if (this.mAddon.updateDate)
             this._dateUpdated.value = formatDate(this.mAddon.updateDate);
-            this.setAttribute("dateUpdated", this.mAddon.updateDate.getTime() / 1000);
-          } else {
+          else
             this._dateUpdated.value = this._dateUpdated.getAttribute("unknown");
-            this.removeAttribute("dateUpdated");
-          }
         ]]></body>
       </method>
 
       <method name="_updateState">
         <body><![CDATA[
           var pending = this.mAddon.pendingOperations;
           if (pending != AddonManager.PENDING_NONE) {
             this.removeAttribute("notification");
--- a/toolkit/mozapps/extensions/content/extensions.xul
+++ b/toolkit/mozapps/extensions/content/extensions.xul
@@ -411,17 +411,17 @@
               </hbox>
               <button class="button-link global-warning-updatesecurity"
                       label="&warning.updatesecurity.enable.label;"
                       tooltiptext="&warning.updatesecurity.enable.tooltip;"
                       command="cmd_enableUpdateSecurity"/>
               <spacer flex="5000"/> <!-- Necessary to allow the message to wrap -->
             </hbox>
             <spacer flex="1"/>
-            <hbox id="updates-sorters" class="sort-controls" sortby="dateUpdated"
+            <hbox id="updates-sorters" class="sort-controls" sortby="updateDate"
                   ascending="false"/>
           </hbox>
           <vbox id="updates-list-empty" class="alert-container"
                 flex="1" hidden="true">
             <spacer class="alert-spacer-before"/>
             <vbox class="alert">
               <label id="empty-availableUpdates-msg" value="&listEmpty.availableUpdates.label;"/>
               <label id="empty-recentUpdates-msg" value="&listEmpty.recentUpdates.label;"/>
--- a/toolkit/mozapps/extensions/test/browser/browser_recentupdates.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_recentupdates.js
@@ -63,25 +63,25 @@ add_test(function() {
   }, false);
   EventUtils.synthesizeMouse(utilsBtn, 2, 2, { }, gManagerWindow);
 });
 
 
 add_test(function() {
   var updatesList = gManagerWindow.document.getElementById("updates-list");
   var items = updatesList.getElementsByTagName("richlistitem");
-  var possible = ["addon1@tests.mozilla.org", "addon2@tests.mozilla.org", "addon2@tests.mozilla.org"];
+  var possible = ["addon1@tests.mozilla.org", "addon2@tests.mozilla.org", "addon3@tests.mozilla.org"];
   var expected = ["addon2@tests.mozilla.org", "addon1@tests.mozilla.org"];
   for (let i = 0; i < items.length; i++) {
     let item = items[i];
     let itemId = item.mAddon.id;
     if (possible.indexOf(itemId) == -1)
       continue; // skip over any other addons, such as shipped addons that would update on every build
     isnot(expected.length, 0, "Should be expecting more items");
-    is(itemId, expected.shift(), "Should get expected item based on recenty of update");
+    is(itemId, expected.shift(), "Should get expected item based on recentness of update");
     if (itemId == "addon1@tests.mozilla.org")
       is_element_visible(item._relNotesToggle, "Release notes toggle should be visible for addon with release notes");
     else
       is_element_hidden(item._relNotesToggle, "Release notes toggle should be hidden for addon with no release notes");
   }
   run_next_test();
 });
 
--- a/toolkit/mozapps/extensions/test/browser/browser_sorting.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_sorting.js
@@ -55,19 +55,18 @@ function end_test() {
 }
 
 function check_order(aExpectedOrder) {
   var order = [];
   var list = gManagerWindow.document.getElementById("addon-list");
   var node = list.firstChild;
   while (node) {
     var id = node.getAttribute("value");
-    if (id && id.substring(id.length - 18) != "@tests.mozilla.org")
-      return;
-    order.push(node.getAttribute("value"));
+    if (id && id.substring(id.length - 18) == "@tests.mozilla.org")
+      order.push(node.getAttribute("value"));
     node = node.nextSibling;
   }
 
   is(order.toSource(), aExpectedOrder.toSource(), "Should have seen the right order");
 }
 
 // Tests that ascending name ordering was the default
 add_test(function() {