Bug 1352459 - Collect rich icons in ContentLinkHandler. r?mak draft
authorNan Jiang <najiang@mozilla.com>
Tue, 12 Sep 2017 09:47:51 -0400
changeset 663014 a70038d35085adb132614b1c6632a60979f24931
parent 660738 37b95547f0d27565452136d16b2df2857be840f6
child 731062 b57388068a6df90b6ce5d5e7ab208fd63c5ca322
push id79286
push usernajiang@mozilla.com
push dateTue, 12 Sep 2017 13:49:08 +0000
reviewersmak
bugs1352459
milestone57.0a1
Bug 1352459 - Collect rich icons in ContentLinkHandler. r?mak MozReview-Commit-ID: GrjpuyvY7nm
browser/base/content/test/general/browser_discovery.js
browser/base/content/test/general/browser_favicon_change_not_in_document.js
browser/base/content/test/general/browser_subframe_favicons_not_used.js
browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_tabs.js
browser/components/places/PlacesUIUtils.jsm
browser/components/sessionstore/test/browser_attributes.js
browser/components/sessionstore/test/browser_label_and_icon.js
browser/components/sessionstore/test/browser_tabicon_after_bg_tab_crash.js
browser/modules/ContentLinkHandler.jsm
--- a/browser/base/content/test/general/browser_discovery.js
+++ b/browser/base/content/test/general/browser_discovery.js
@@ -29,27 +29,36 @@ var iconDiscoveryTests = [
   { rel: "shortcut-icon", pass: false, text: "rel shortcut-icon not discovered" },
   { href: "moz.png", text: "relative href works" },
   { href: "notthere.png", text: "404'd icon is removed properly" },
   { href: "data:image/x-icon,%00", type: "image/x-icon", text: "data: URIs work" },
   { type: "image/png; charset=utf-8", text: "type may have optional parameters (RFC2046)" }
 ];
 
 function runIconDiscoveryTest() {
-  var testCase = iconDiscoveryTests[0];
-  var head = doc().getElementById("linkparent");
-  var hasSrc = gBrowser.getIcon() != null;
-  if (testCase.pass)
-    ok(hasSrc, testCase.text);
-  else
-    ok(!hasSrc, testCase.text);
+  let testCase = iconDiscoveryTests[0];
+  let head = doc().getElementById("linkparent");
 
-  head.removeChild(head.getElementsByTagName("link")[0]);
-  iconDiscoveryTests.shift();
-  iconDiscovery(); // Run the next test.
+  // Because there is debounce logic in ContentLinkHandler.jsm to reduce the
+  // favicon loads, we have to wait some time before checking that icon was
+  // stored properly.
+  BrowserTestUtils.waitForCondition(() => {
+    return gBrowser.getIcon() != null;
+  }, "wait for icon load to finish", 100, 5)
+  .then(() => {
+    ok(testCase.pass, testCase.text);
+  })
+  .catch(() => {
+    ok(!testCase.pass, testCase.text);
+  })
+  .then(() => {
+    head.removeChild(head.getElementsByTagName("link")[0]);
+    iconDiscoveryTests.shift();
+    iconDiscovery(); // Run the next test.
+  });
 }
 
 function iconDiscovery() {
   if (iconDiscoveryTests.length) {
     setHandlerFunc(runIconDiscoveryTest);
     gBrowser.setIcon(gBrowser.selectedTab, null,
                      Services.scriptSecurityManager.getSystemPrincipal());
 
@@ -64,16 +73,73 @@ function iconDiscovery() {
     if (testCase.pass == undefined)
       testCase.pass = true;
 
     link.rel = rel;
     link.href = href;
     link.type = type;
     head.appendChild(link);
   } else {
+    richIconDiscovery();
+  }
+}
+
+let richIconDiscoveryTests = [
+  { rel: "apple-touch-icon", text: "apple-touch-icon discovered" },
+  { rel: "apple-touch-icon-precomposed", text: "apple-touch-icon-precomposed discovered" },
+  { rel: "fluid-icon", text: "fluid-icon discovered" },
+  { rel: "unknown-icon", pass: false, text: "unknown icon not discovered" }
+];
+
+function runRichIconDiscoveryTest() {
+  let testCase = richIconDiscoveryTests[0];
+  let head = doc().getElementById("linkparent");
+
+  // Because there is debounce logic in ContentLinkHandler.jsm to reduce the
+  // favicon loads, we have to wait some time before checking that icon was
+  // stored properly.
+  BrowserTestUtils.waitForCondition(() => {
+    return gBrowser.getIcon() != null;
+  }, "wait for icon load to finish", 100, 5)
+  .then(() => {
+    ok(testCase.pass, testCase.text);
+  })
+  .catch(() => {
+    ok(!testCase.pass, testCase.text);
+  })
+  .then(() => {
+    head.removeChild(head.getElementsByTagName("link")[0]);
+    richIconDiscoveryTests.shift();
+    richIconDiscovery(); // Run the next test.
+  });
+}
+
+function richIconDiscovery() {
+  if (richIconDiscoveryTests.length) {
+    setHandlerFunc(runRichIconDiscoveryTest);
+    gBrowser.setIcon(gBrowser.selectedTab, null,
+                     Services.scriptSecurityManager.getSystemPrincipal()
+    );
+
+    let testCase = richIconDiscoveryTests[0];
+    let head = doc().getElementById("linkparent");
+    let link = doc().createElement("link");
+
+    let rel = testCase.rel;
+    let rootDir = getRootDirectory(gTestPath);
+    let href = testCase.href || rootDir + "moz.png";
+    let type = testCase.type || "image/png";
+    if (testCase.pass === undefined)
+      testCase.pass = true;
+
+    link.rel = rel;
+    link.href = href;
+    link.type = type;
+    head.appendChild(link);
+  } else {
     searchDiscovery();
   }
 }
 
 var searchDiscoveryTests = [
   { text: "rel search discovered" },
   { rel: "SEARCH", text: "rel is case insensitive" },
   { rel: "-search-", pass: false, text: "rel -search- not discovered" },
--- a/browser/base/content/test/general/browser_favicon_change_not_in_document.js
+++ b/browser/base/content/test/general/browser_favicon_change_not_in_document.js
@@ -1,34 +1,45 @@
 "use strict";
 
 const TEST_URL = "http://mochi.test:8888/browser/browser/base/content/test/general/file_favicon_change_not_in_document.html"
 
+/*
+ * This test tests a link element won't fire DOMLinkChanged/DOMLinkAdded unless
+ * it is added to the DOM. See more details in bug 1083895.
+ *
+ * Note that there is debounce logic in ContentLinkHandler.jsm, adding a new
+ * icon link after the icon parsing timeout will trigger a new icon extraction
+ * cycle. Hence, there should be two favicons loads in this test as it appends
+ * a new link to the DOM in the timeout callback defined in the test HTML page.
+ * However, the not-yet-added link element with href as "http://example.org/other-icon"
+ * should not fire the DOMLinkAdded event, nor should it fire the DOMLinkChanged
+ * event after its href gets updated later.
+ */
 add_task(async function() {
   let extraTab = gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
-  let tabLoaded = promiseTabLoaded(extraTab);
+  let domLinkAddedFired = 0;
+  let domLinkChangedFired = 0;
+  const linkAddedHandler = event => domLinkAddedFired++;
+  const linkChangedhandler = event => domLinkChangedFired++;
+  gBrowser.addEventListener("DOMLinkAdded", linkAddedHandler);
+  gBrowser.addEventListener("DOMLinkChanged", linkChangedhandler);
   extraTab.linkedBrowser.loadURI(TEST_URL);
-  let expectedFavicon = "http://example.org/one-icon";
-  let haveChanged = PromiseUtils.defer();
-  let observer = new MutationObserver(function(mutations) {
-    for (let mut of mutations) {
-      if (mut.attributeName != "image") {
-        continue;
-      }
-      let imageVal = extraTab.getAttribute("image").replace(/#.*$/, "");
-      if (!imageVal) {
-        // The value gets removed because it doesn't load.
-        continue;
-      }
-      is(imageVal, expectedFavicon, "Favicon image should correspond to expected image.");
-      haveChanged.resolve();
-    }
-  });
-  observer.observe(extraTab, {attributes: true});
-  await tabLoaded;
-  expectedFavicon = "http://example.org/yet-another-icon";
-  haveChanged = PromiseUtils.defer();
-  await haveChanged.promise;
-  observer.disconnect();
+  let expectedFavicon = "http://example.org/yet-another-icon";
+  await promiseTabLoaded(extraTab);
+
+  // Make sure the new added favicon link gets loaded.
+  try {
+    await BrowserTestUtils.waitForCondition(() => {
+      return gBrowser.getIcon(extraTab) === expectedFavicon;
+    }, "wait for favicon load to finish", 1000, 5);
+    ok(true, "Should load the added favicon");
+  } catch (e) {
+    ok(false, "Should've loaded the new added favicon.");
+  }
+
+  is(domLinkAddedFired, 2, "Should fire the correct number of DOMLinkAdded event.");
+  is(domLinkChangedFired, 0, "Should not fire any DOMLinkChanged event.");
+
+  gBrowser.removeEventListener("DOMLinkAdded", linkAddedHandler);
+  gBrowser.removeEventListener("DOMLinkChanged", linkChangedhandler);
   gBrowser.removeTab(extraTab);
 });
-
-
--- a/browser/base/content/test/general/browser_subframe_favicons_not_used.js
+++ b/browser/base/content/test/general/browser_subframe_favicons_not_used.js
@@ -4,15 +4,29 @@ function test() {
   waitForExplicitFinish();
 
   let testPath = getRootDirectory(gTestPath);
 
   let tab = BrowserTestUtils.addTab(gBrowser, testPath + "file_bug970276_popup1.html");
 
   tab.linkedBrowser.addEventListener("load", function() {
     let expectedIcon = testPath + "file_bug970276_favicon1.ico";
-    is(gBrowser.getIcon(tab), expectedIcon, "Correct icon.");
+    let icon;
 
-    gBrowser.removeTab(tab);
-
-    finish();
+    // Because there is debounce logic in ContentLinkHandler.jsm to reduce the
+    // favicon loads, we have to wait some time before checking that icon was
+    // stored properly.
+    BrowserTestUtils.waitForCondition(() => {
+      icon = gBrowser.getIcon(tab);
+      return icon != null;
+    }, "wait for favicon load to finish", 100, 5)
+    .then(() => {
+      is(icon, expectedIcon, "Correct icon.");
+    })
+    .catch(() => {
+      ok(false, "Can't get the correct icon.");
+    })
+    .then(() => {
+      gBrowser.removeTab(tab);
+      finish();
+    });
   }, {capture: true, once: true});
 }
--- a/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_tabs.js
+++ b/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_tabs.js
@@ -37,22 +37,39 @@ add_task(async function test_sessions_ge
     background,
   });
 
   let win = await BrowserTestUtils.openNewBrowserWindow();
   await BrowserTestUtils.loadURI(win.gBrowser.selectedBrowser, "about:mozilla");
   await BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
   let expectedTabs = [];
   let tab = win.gBrowser.selectedTab;
+  // Because there is debounce logic in ContentLinkHandler.jsm to reduce the
+  // favicon loads, we have to wait some time before checking that icon was
+  // stored properly. If that page doesn't have favicon links, let it timeout.
+  try {
+    await BrowserTestUtils.waitForCondition(() => {
+      return gBrowser.getIcon(tab) != null;
+    }, "wait for favicon load to finish", 100, 5);
+  } catch (e) {
+    // This page doesn't have any favicon link, just continue.
+  }
   expectedTabs.push(expectedTabInfo(tab, win));
   let lastAccessedTimes = new Map();
   lastAccessedTimes.set("about:mozilla", tab.lastAccessed);
 
   for (let url of ["about:robots", "about:buildconfig"]) {
     tab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser, url);
+    try {
+      await BrowserTestUtils.waitForCondition(() => {
+        return gBrowser.getIcon(tab) != null;
+      }, "wait for favicon load to finish", 100, 5);
+    } catch (e) {
+      // This page doesn't have any favicon link, just continue.
+    }
     expectedTabs.push(expectedTabInfo(tab, win));
     lastAccessedTimes.set(url, tab.lastAccessed);
   }
 
   await extension.startup();
 
   // Test with a closed tab.
   await BrowserTestUtils.removeTab(tab);
--- a/browser/components/places/PlacesUIUtils.jsm
+++ b/browser/components/places/PlacesUIUtils.jsm
@@ -229,19 +229,16 @@ let InternalFaviconLoader = {
           this.onUnload(win);
         }
       };
       win.addEventListener("unload", unloadHandler, true);
     }
 
     let {innerWindowID, currentURI} = browser;
 
-    // Immediately cancel any earlier requests
-    this.removeRequestsForInner(innerWindowID);
-
     // First we do the actual setAndFetch call:
     let loadType = PrivateBrowsingUtils.isWindowPrivate(win)
       ? PlacesUtils.favicons.FAVICON_LOAD_PRIVATE
       : PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE;
     let callback = this._makeCompletionCallback(win, innerWindowID);
     let request = PlacesUtils.favicons.setAndFetchFaviconForPage(currentURI, uri, false,
                                                                  loadType, callback, principal);
 
--- a/browser/components/sessionstore/test/browser_attributes.js
+++ b/browser/components/sessionstore/test/browser_attributes.js
@@ -18,16 +18,23 @@ add_task(async function test() {
   // Since we need to test 'activemedia-blocked' attribute.
   Services.prefs.setBoolPref(PREF2, true)
   registerCleanupFunction(() => Services.prefs.clearUserPref(PREF2));
 
   // Add a new tab with a nice icon.
   let tab = BrowserTestUtils.addTab(gBrowser, "about:robots");
   await promiseBrowserLoaded(tab.linkedBrowser);
 
+  // Because there is debounce logic in ContentLinkHandler.jsm to reduce the
+  // favicon loads, we have to wait some time before checking that icon was
+  // stored properly.
+  await BrowserTestUtils.waitForCondition(() => {
+    return gBrowser.getIcon(tab) != null;
+  }, "wait for favicon load to finish", 100, 5);
+
   // Check that the tab has 'image' and 'iconLoadingPrincipal' attributes.
   ok(tab.hasAttribute("image"), "tab.image exists");
   ok(tab.hasAttribute("iconLoadingPrincipal"), "tab.iconLoadingPrincipal exists");
 
   tab.toggleMuteAudio();
   // Check that the tab has a 'muted' attribute.
   ok(tab.hasAttribute("muted"), "tab.muted exists");
 
--- a/browser/components/sessionstore/test/browser_label_and_icon.js
+++ b/browser/components/sessionstore/test/browser_label_and_icon.js
@@ -12,16 +12,22 @@ add_task(async function test_label_and_i
   await SpecialPowers.pushPrefEnv({
     set: [["browser.sessionstore.restore_on_demand", true]],
   });
 
   // Create a new tab.
   let tab = BrowserTestUtils.addTab(gBrowser, "about:robots");
   let browser = tab.linkedBrowser;
   await promiseBrowserLoaded(browser);
+  // Because there is debounce logic in ContentLinkHandler.jsm to reduce the
+  // favicon loads, we have to wait some time before checking that icon was
+  // stored properly.
+  await BrowserTestUtils.waitForCondition(() => {
+    return gBrowser.getIcon(tab) != null;
+  }, "wait for favicon load to finish", 100, 5);
 
   // Retrieve the tab state.
   await TabStateFlusher.flush(browser);
   let state = ss.getTabState(tab);
   await promiseRemoveTab(tab);
   browser = null;
 
   // Open a new tab to restore into.
--- a/browser/components/sessionstore/test/browser_tabicon_after_bg_tab_crash.js
+++ b/browser/components/sessionstore/test/browser_tabicon_after_bg_tab_crash.js
@@ -17,16 +17,22 @@ const PAGE_URL = `data:text/html,
  */
 add_task(async function test_tabicon_after_bg_tab_crash() {
   let originalTab = gBrowser.selectedTab;
 
   await BrowserTestUtils.withNewTab({
     gBrowser,
     url: PAGE_URL,
   }, async function(browser) {
+    // Because there is debounce logic in ContentLinkHandler.jsm to reduce the
+    // favicon loads, we have to wait some time before checking that icon was
+    // stored properly.
+    await BrowserTestUtils.waitForCondition(() => {
+      return gBrowser.getIcon() != null;
+    }, "wait for favicon load to finish", 100, 5);
     Assert.equal(browser.mIconURL, FAVICON, "Favicon is correctly set.");
     await BrowserTestUtils.switchTab(gBrowser, originalTab);
     await BrowserTestUtils.crashBrowser(browser,
                                         false /* shouldShowTabCrashPage */);
     Assert.equal(browser.mIconURL, FAVICON,
                  "Favicon is still set after crash.");
   });
 });
--- a/browser/modules/ContentLinkHandler.jsm
+++ b/browser/modules/ContentLinkHandler.jsm
@@ -20,45 +20,247 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 
 const SIZES_TELEMETRY_ENUM = {
   NO_SIZES: 0,
   ANY: 1,
   DIMENSION: 2,
   INVALID: 3,
 };
 
+const FAVICON_PARSING_TIMEOUT = 100;
+const FAVICON_RICH_ICON_MIN_WIDTH = 96;
+
+/*
+ * Create a nsITimer.
+ *
+ * @param {function} aCallback A timeout callback function.
+ * @param {Number} aDelay A timeout interval in millisecond.
+ * @return {nsITimer} A nsITimer object.
+ */
+function setTimeout(aCallback, aDelay) {
+  let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+  timer.initWithCallback(aCallback, aDelay, Ci.nsITimer.TYPE_ONE_SHOT);
+  return timer;
+}
+
+/*
+ * Extract the icon width from the size attribute. It also sends the telemetry
+ * about the size type and size dimension info.
+ *
+ * @param {Array} aSizes An array of strings about size.
+ * @return {Number} A width of the icon in pixel.
+ */
+function extractIconSize(aSizes) {
+  let width = -1;
+  let sizesType;
+  const re = /^([1-9][0-9]*)x[1-9][0-9]*$/i;
+
+  if (aSizes.length) {
+    for (let size of aSizes) {
+      if (size.toLowerCase() == "any") {
+        sizesType = SIZES_TELEMETRY_ENUM.ANY;
+        break;
+      } else {
+        let values = re.exec(size);
+        if (values && values.length > 1) {
+          sizesType = SIZES_TELEMETRY_ENUM.DIMENSION;
+          width = parseInt(values[1]);
+          break;
+        } else {
+          sizesType = SIZES_TELEMETRY_ENUM.INVALID;
+          break;
+        }
+      }
+    }
+  } else {
+    sizesType = SIZES_TELEMETRY_ENUM.NO_SIZES;
+  }
+
+  // Telemetry probes for measuring the sizes attribute
+  // usage and available dimensions.
+  Services.telemetry.getHistogramById("LINK_ICON_SIZES_ATTR_USAGE").add(sizesType);
+  if (width > 0)
+    Services.telemetry.getHistogramById("LINK_ICON_SIZES_ATTR_DIMENSION").add(width);
+
+  return width;
+}
+
+/*
+ * Get link icon URI from a link dom node.
+ *
+ * @param {DOMNode} aLink A link dom node.
+ * @return {nsIURI} A uri of the icon.
+ */
+function getLinkIconURI(aLink) {
+  let targetDoc = aLink.ownerDocument;
+  let uri = Services.io.newURI(aLink.href, targetDoc.characterSet);
+  try {
+    uri.userPass = "";
+  } catch (e) {
+    // some URIs are immutable
+  }
+  return uri;
+}
+
+/*
+ * Set the icon via sending the "Link:Seticon" message.
+ *
+ * @param {Object} aIconInfo The IconInfo object looks like {
+ *   iconUri: icon URI,
+ *   loadingPrincipal: icon loading principal
+ * }.
+ * @param {Object} aChromeGlobal A global chrome object.
+ */
+function setIconForLink(aIconInfo, aChromeGlobal) {
+  aChromeGlobal.sendAsyncMessage(
+    "Link:SetIcon",
+    { url: aIconInfo.iconUri.spec, loadingPrincipal: aIconInfo.loadingPrincipal });
+}
+
+/*
+ * Timeout callback function for loading favicon.
+ *
+ * @param {Map} aFaviconLoads A map of page URL and FaviconLoad object pairs,
+ *   where the FaviconLoad object looks like {
+ *     timer: a nsITimer object,
+ *     iconInfos: an array of IconInfo objects
+ *   }
+ * @param {String} aPageUrl A page URL string for this callback.
+ * @param {Object} aChromeGlobal A global chrome object.
+ */
+function faviconTimeoutCallback(aFaviconLoads, aPageUrl, aChromeGlobal) {
+  let load = aFaviconLoads.get(aPageUrl);
+  if (!load)
+    return;
+
+  // SVG and ico are the preferred icons
+  let preferredIcon;
+  // Other links with the "icon" tag are the default icons
+  let defaultIcon;
+  // Rich icons are either apple-touch or fluid icons, or the ones of the
+  // dimension 96x96 or greater
+  let largestRichIcon;
+
+  for (let icon of load.iconInfos) {
+    if (icon.type === "image/svg+xml" ||
+      icon.type === "image/x-icon" ||
+      icon.type === "image/vnd.microsoft.icon") {
+      preferredIcon = icon;
+      continue;
+    }
+
+    if (icon.isRichIcon) {
+      if (!largestRichIcon || largestRichIcon.width < icon.width) {
+        largestRichIcon = icon;
+      }
+    } else if (!defaultIcon) {
+      defaultIcon = icon;
+    }
+  }
+
+  // Now set the favicons for the page in the following order:
+  // 1. Set the preferred one if any, otherwise use the default one.
+  // 2. Set the best rich icon if any.
+  if (preferredIcon) {
+    setIconForLink(preferredIcon, aChromeGlobal);
+  } else if (defaultIcon) {
+    setIconForLink(defaultIcon, aChromeGlobal);
+  }
+
+  if (largestRichIcon) {
+    setIconForLink(largestRichIcon, aChromeGlobal);
+  }
+  load.timer = null;
+  aFaviconLoads.delete(aPageUrl);
+}
+
+/*
+ * Favicon link handler.
+ *
+ * @param {DOMNode} aLink A link dom node.
+ * @param {bool} aIsRichIcon A bool to indicate if the link is rich icon.
+ * @param {Object} aChromeGlobal A global chrome object.
+ * @param {Map} aFaviconLoads A map of page URL and FaviconLoad object pairs.
+ * @return {bool} Returns true if the link is successfully handled.
+ */
+function handleFaviconLink(aLink, aIsRichIcon, aChromeGlobal, aFaviconLoads) {
+  let pageUrl = aLink.ownerDocument.documentURI;
+  let iconUri = getLinkIconURI(aLink);
+  if (!iconUri)
+    return false;
+
+  // Extract the size type and width. Note that some sites use hi-res icons
+  // without specifying them as apple-touch or fluid icons.
+  let width = extractIconSize(aLink.sizes);
+  if (width >= FAVICON_RICH_ICON_MIN_WIDTH)
+    aIsRichIcon = true;
+
+  let iconInfo = {
+    iconUri,
+    width,
+    isRichIcon: aIsRichIcon,
+    type: aLink.type,
+    loadingPrincipal: aLink.ownerDocument.nodePrincipal
+  };
+
+  if (aFaviconLoads.has(pageUrl)) {
+    let load = aFaviconLoads.get(pageUrl);
+    load.iconInfos.push(iconInfo)
+    // Re-initialize the timer
+    load.timer.delay = FAVICON_PARSING_TIMEOUT;
+  } else {
+    let timer = setTimeout(() => faviconTimeoutCallback(aFaviconLoads, pageUrl, aChromeGlobal),
+                                                        FAVICON_PARSING_TIMEOUT);
+    let load = { timer, iconInfos: [iconInfo] };
+    aFaviconLoads.set(pageUrl, load);
+  }
+  return true;
+}
+
 this.ContentLinkHandler = {
   init(chromeGlobal) {
-    chromeGlobal.addEventListener("DOMLinkAdded", (event) => {
-      this.onLinkEvent(event, chromeGlobal);
+    const faviconLoads = new Map();
+    chromeGlobal.addEventListener("DOMLinkAdded", event => {
+      this.onLinkEvent(event, chromeGlobal, faviconLoads);
+    });
+    chromeGlobal.addEventListener("DOMLinkChanged", event => {
+      this.onLinkEvent(event, chromeGlobal, faviconLoads);
     });
-    chromeGlobal.addEventListener("DOMLinkChanged", (event) => {
-      this.onLinkEvent(event, chromeGlobal);
+    chromeGlobal.addEventListener("unload", event => {
+      for (const [pageUrl, load] of faviconLoads) {
+        load.timer.cancel();
+        load.timer = null;
+        faviconLoads.delete(pageUrl);
+      }
     });
   },
 
-  onLinkEvent(event, chromeGlobal) {
+  onLinkEvent(event, chromeGlobal, faviconLoads) {
     var link = event.originalTarget;
     var rel = link.rel && link.rel.toLowerCase();
     if (!link || !link.ownerDocument || !rel || !link.href)
       return;
 
     // Ignore sub-frames (bugs 305472, 479408).
     let window = link.ownerGlobal;
     if (window != window.top)
       return;
 
+    // Note: following booleans only work for the current link, not for the
+    // whole content
     var feedAdded = false;
     var iconAdded = false;
     var searchAdded = false;
     var rels = {};
     for (let relString of rel.split(/\s+/))
       rels[relString] = true;
 
     for (let relVal in rels) {
+      let isRichIcon = true;
+
       switch (relVal) {
         case "feed":
         case "alternate":
           if (!feedAdded && event.type == "DOMLinkAdded") {
             if (!rels.feed && rels.alternate && rels.stylesheet)
               break;
 
             if (Feeds.isValidFeed(link, link.ownerDocument.nodePrincipal, "feed" in rels)) {
@@ -66,56 +268,25 @@ this.ContentLinkHandler = {
                                             {type: link.type,
                                              href: link.href,
                                              title: link.title});
               feedAdded = true;
             }
           }
           break;
         case "icon":
+          isRichIcon = false;
+          // Fall through to rich icon handling
+        case "apple-touch-icon":
+        case "apple-touch-icon-precomposed":
+        case "fluid-icon":
           if (iconAdded || !Services.prefs.getBoolPref("browser.chrome.site_icons"))
             break;
 
-          var uri = this.getLinkIconURI(link);
-          if (!uri)
-            break;
-
-          // Telemetry probes for measuring the sizes attribute
-          // usage and available dimensions.
-          let sizeHistogramTypes = Services.telemetry.
-                                   getHistogramById("LINK_ICON_SIZES_ATTR_USAGE");
-          let sizeHistogramDimension = Services.telemetry.
-                                       getHistogramById("LINK_ICON_SIZES_ATTR_DIMENSION");
-          let sizesType;
-          if (link.sizes.length) {
-            for (let size of link.sizes) {
-              if (size.toLowerCase() == "any") {
-                sizesType = SIZES_TELEMETRY_ENUM.ANY;
-                break;
-              } else {
-                let re = /^([1-9][0-9]*)x[1-9][0-9]*$/i;
-                let values = re.exec(size);
-                if (values && values.length > 1) {
-                  sizesType = SIZES_TELEMETRY_ENUM.DIMENSION;
-                  sizeHistogramDimension.add(parseInt(values[1]));
-                } else {
-                  sizesType = SIZES_TELEMETRY_ENUM.INVALID;
-                  break;
-                }
-              }
-            }
-          } else {
-            sizesType = SIZES_TELEMETRY_ENUM.NO_SIZES;
-          }
-          sizeHistogramTypes.add(sizesType);
-
-          chromeGlobal.sendAsyncMessage(
-            "Link:SetIcon",
-            {url: uri.spec, loadingPrincipal: link.ownerDocument.nodePrincipal});
-          iconAdded = true;
+          iconAdded = handleFaviconLink(link, isRichIcon, chromeGlobal, faviconLoads);
           break;
         case "search":
           if (!searchAdded && event.type == "DOMLinkAdded") {
             var type = link.type && link.type.toLowerCase();
             type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
 
             let re = /^(?:https?|ftp):/i;
             if (type == "application/opensearchdescription+xml" && link.title &&
@@ -126,20 +297,9 @@ this.ContentLinkHandler = {
                                              url: link.ownerDocument.documentURI});
               searchAdded = true;
             }
           }
           break;
       }
     }
   },
-
-  getLinkIconURI(aLink) {
-    let targetDoc = aLink.ownerDocument;
-    var uri = Services.io.newURI(aLink.href, targetDoc.characterSet);
-    try {
-      uri.userPass = "";
-    } catch (e) {
-      // some URIs are immutable
-    }
-    return uri;
-  },
 };