Bug 1352459 - Collect rich icons in ContentLinkHandler. r=mak,Mardak
authorNan Jiang <najiang@mozilla.com>
Tue, 12 Sep 2017 09:47:51 -0400
changeset 433686 6d5fe3151e733d6ac818728f44f5985f1aa63f8c
parent 433685 4dc8f5f9259c1413dcd2755ae01bad8dc8139050
child 433687 4823939d5e4bf44463881c4644c88ce5d8c6209c
push id1567
push userjlorenzo@mozilla.com
push dateThu, 02 Nov 2017 12:36:05 +0000
treeherdermozilla-release@e512c14a0406 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak, Mardak
bugs1352459
milestone57.0a1
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 1352459 - Collect rich icons in ContentLinkHandler. r=mak,Mardak 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
@@ -231,19 +231,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;
-  },
 };