Bug 1480319: Split favicon loading code out of ContentLinkHandler.jsm. r?Mossop draft
authorKris Maglione <maglione.k@gmail.com>
Wed, 01 Aug 2018 21:36:12 -0700
changeset 829189 70c01d8b9c7d3649a71764c80394b04ba6bbed5b
parent 829188 915cf7ff016c413c3fff80dea2acd0bb87bdac81
push id118745
push usermaglione.k@gmail.com
push dateTue, 14 Aug 2018 20:34:55 +0000
reviewersMossop
bugs1480319
milestone63.0a1
Bug 1480319: Split favicon loading code out of ContentLinkHandler.jsm. r?Mossop MozReview-Commit-ID: KyP42P5FJRq
browser/actors/LinkHandlerChild.jsm
browser/actors/moz.build
browser/base/content/content.js
browser/base/content/test/performance/browser_startup_content.js
browser/components/nsBrowserGlue.js
browser/modules/ContentLinkHandler.jsm
browser/modules/FaviconLoader.jsm
browser/modules/moz.build
rename from browser/modules/ContentLinkHandler.jsm
rename to browser/actors/LinkHandlerChild.jsm
--- a/browser/modules/ContentLinkHandler.jsm
+++ b/browser/actors/LinkHandlerChild.jsm
@@ -1,573 +1,93 @@
 /* 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";
 
-const EXPORTED_SYMBOLS = ["ContentLinkHandler"];
+const EXPORTED_SYMBOLS = ["LinkHandlerChild"];
 
-ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
-
-XPCOMUtils.defineLazyGlobalGetters(this, ["Blob", "FileReader"]);
+ChromeUtils.import("resource://gre/modules/ActorChild.jsm");
 
 ChromeUtils.defineModuleGetter(this, "Feeds",
   "resource:///modules/Feeds.jsm");
-ChromeUtils.defineModuleGetter(this, "DeferredTask",
-  "resource://gre/modules/DeferredTask.jsm");
-ChromeUtils.defineModuleGetter(this, "PromiseUtils",
-  "resource://gre/modules/PromiseUtils.jsm");
-
-const BinaryInputStream = Components.Constructor("@mozilla.org/binaryinputstream;1",
-                                                 "nsIBinaryInputStream", "setInputStream");
-
-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;
-const PREFERRED_WIDTH = 16;
-
-// URL schemes that we don't want to load and convert to data URLs.
-const LOCAL_FAVICON_SCHEMES = [
-  "chrome",
-  "about",
-  "resource",
-  "data",
-];
-
-const MAX_FAVICON_EXPIRATION = 7 * 24 * 60 * 60 * 1000;
-
-const TYPE_ICO = "image/x-icon";
-const TYPE_SVG = "image/svg+xml";
-
-function promiseBlobAsDataURL(blob) {
-  return new Promise((resolve, reject) => {
-    let reader = new FileReader();
-    reader.addEventListener("load", () => resolve(reader.result));
-    reader.addEventListener("error", reject);
-    reader.readAsDataURL(blob);
-  });
-}
-
-function promiseBlobAsOctets(blob) {
-  return new Promise((resolve, reject) => {
-    let reader = new FileReader();
-    reader.addEventListener("load", () => {
-      resolve(Array.from(reader.result).map(c => c.charCodeAt(0)));
-    });
-    reader.addEventListener("error", reject);
-    reader.readAsBinaryString(blob);
-  });
-}
-
-class FaviconLoad {
-  constructor(iconInfo) {
-    this.buffers = [];
-    this.icon = iconInfo;
-
-    this.channel = Services.io.newChannelFromURI2(
-      iconInfo.iconUri,
-      iconInfo.node,
-      iconInfo.node.nodePrincipal,
-      iconInfo.node.nodePrincipal,
-      (Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS |
-       Ci.nsILoadInfo.SEC_ALLOW_CHROME |
-       Ci.nsILoadInfo.SEC_DISALLOW_SCRIPT),
-      Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE_FAVICON);
-
-    this.channel.loadFlags |= Ci.nsIRequest.LOAD_BACKGROUND;
-    // Sometimes node is a document and sometimes it is an element. This is
-    // the easiest single way to get to the load group in both those cases.
-    this.channel.loadGroup = iconInfo.node.ownerGlobal.document.documentLoadGroup;
-    this.channel.notificationCallbacks = this;
-
-    if (Services.prefs.getBoolPref("network.http.tailing.enabled", true) &&
-        this.channel instanceof Ci.nsIClassOfService) {
-      this.channel.addClassFlags(Ci.nsIClassOfService.Tail | Ci.nsIClassOfService.Throttleable);
-    }
-  }
-
-  load() {
-    this._deferred = PromiseUtils.defer();
-    // Clear the channel reference when we succeed or fail.
-    this._deferred.promise.then(
-      () => this.channel = null,
-      () => this.channel = null
-    );
-
-    try {
-      this.channel.asyncOpen2(this);
-    } catch (e) {
-      this._deferred.reject(e);
-    }
-
-    return this._deferred.promise;
-  }
-
-  cancel() {
-    if (!this.channel) {
-      return;
-    }
-
-    this.channel.cancel(Cr.NS_BINDING_ABORTED);
-  }
-
-  onStartRequest(request, context) {
-  }
+ChromeUtils.defineModuleGetter(this, "FaviconLoader",
+  "resource:///modules/FaviconLoader.jsm");
 
-  onDataAvailable(request, context, inputStream, offset, count) {
-    let stream = new BinaryInputStream(inputStream);
-    let buffer = new ArrayBuffer(count);
-    stream.readArrayBuffer(buffer.byteLength, buffer);
-    this.buffers.push(new Uint8Array(buffer));
-  }
-
-  asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
-    if (oldChannel == this.channel) {
-      this.channel = newChannel;
-    }
-
-    callback.onRedirectVerifyCallback(Cr.NS_OK);
-  }
-
-  async onStopRequest(request, context, statusCode) {
-    if (request != this.channel) {
-      // Indicates that a redirect has occurred. We don't care about the result
-      // of the original channel.
-      return;
-    }
-
-    if (!Components.isSuccessCode(statusCode)) {
-      if (statusCode == Cr.NS_BINDING_ABORTED) {
-        this._deferred.reject(Components.Exception(`Favicon load from ${this.icon.iconUri.spec} was cancelled.`, statusCode));
-      } else {
-        this._deferred.reject(Components.Exception(`Favicon at "${this.icon.iconUri.spec}" failed to load.`, statusCode));
-      }
-      return;
-    }
-
-    if (this.channel instanceof Ci.nsIHttpChannel) {
-      if (!this.channel.requestSucceeded) {
-        this._deferred.reject(Components.Exception(`Favicon at "${this.icon.iconUri.spec}" failed to load: ${this.channel.responseStatusText}.`, Cr.NS_ERROR_FAILURE));
-        return;
-      }
-    }
-
-    // Attempt to get an expiration time from the cache.  If this fails, we'll
-    // use this default.
-    let expiration = Date.now() + MAX_FAVICON_EXPIRATION;
-
-    // This stuff isn't available after onStopRequest returns (so don't start
-    // any async operations before this!).
-    if (this.channel instanceof Ci.nsICacheInfoChannel) {
-      try {
-        expiration = Math.min(this.channel.cacheTokenExpirationTime * 1000, expiration);
-      } catch (e) {
-        // Ignore failures to get the expiration time.
-      }
-    }
-
-    try {
-      let type = this.channel.contentType;
-      let blob = new Blob(this.buffers, { type });
+class LinkHandlerChild extends ActorChild {
+  constructor(mm) {
+    super(mm);
 
-      if (type != "image/svg+xml") {
-        let octets = await promiseBlobAsOctets(blob);
-        let sniffer = Cc["@mozilla.org/image/loader;1"].
-                      createInstance(Ci.nsIContentSniffer);
-        type = sniffer.getMIMETypeFromContent(this.channel, octets, octets.length);
-
-        if (!type) {
-          throw Components.Exception(`Favicon at "${this.icon.iconUri.spec}" did not match a known mimetype.`, Cr.NS_ERROR_FAILURE);
-        }
-
-        blob = blob.slice(0, blob.size, type);
-      }
-
-      let dataURL = await promiseBlobAsDataURL(blob);
-
-      this._deferred.resolve({
-        expiration,
-        dataURL,
-      });
-    } catch (e) {
-      this._deferred.reject(e);
-    }
-  }
-
-  getInterface(iid) {
-    if (iid.equals(Ci.nsIChannelEventSink)) {
-      return this;
-    }
-    throw Cr.NS_ERROR_NO_INTERFACE;
-  }
-}
-
-/*
- * 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;
+    this.seenTabIcon = false;
+    this._iconLoader = null;
   }
 
-  // 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 = uri.mutate().setUserPass("").finalize();
-  } catch (e) {
-    // some URIs are immutable
-  }
-  return uri;
-}
-
-/**
- * Guess a type for an icon based on its declared type or file extension.
- */
-function guessType(icon) {
-  // No type with no icon
-  if (!icon) {
-    return "";
-  }
-
-  // Use the file extension to guess at a type we're interested in
-  if (!icon.type) {
-    let extension = icon.iconUri.filePath.split(".").pop();
-    switch (extension) {
-      case "ico":
-        return TYPE_ICO;
-      case "svg":
-        return TYPE_SVG;
+  get iconLoader() {
+    if (!this._iconLoader) {
+      this._iconLoader = new FaviconLoader(this.mm);
     }
-  }
-
-  // Fuzzily prefer the type or fall back to the declared type
-  return icon.type == "image/vnd.microsoft.icon" ? TYPE_ICO : icon.type || "";
-}
-
-/*
- * Selects the best rich icon and tab icon from a list of IconInfo objects.
- *
- * @param {Array} iconInfos A list of IconInfo objects.
- * @param {integer} preferredWidth The preferred width for tab icons.
- */
-function selectIcons(iconInfos, preferredWidth) {
-  if (iconInfos.length == 0) {
-    return {
-      richIcon: null,
-      tabIcon: null,
-    };
-  }
-
-  let preferredIcon;
-  let bestSizedIcon;
-  // 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 iconInfos) {
-    if (!icon.isRichIcon) {
-      // First check for svg. If it's not available check for an icon with a
-      // size adapt to the current resolution. If both are not available, prefer
-      // ico files. When multiple icons are in the same set, the latest wins.
-      if (guessType(icon) == TYPE_SVG) {
-        preferredIcon = icon;
-      } else if (icon.width == preferredWidth && guessType(preferredIcon) != TYPE_SVG) {
-        preferredIcon = icon;
-      } else if (guessType(icon) == TYPE_ICO && (!preferredIcon || guessType(preferredIcon) == TYPE_ICO)) {
-        preferredIcon = icon;
-      }
-
-      // Check for an icon larger yet closest to preferredWidth, that can be
-      // downscaled efficiently.
-      if (icon.width >= preferredWidth &&
-          (!bestSizedIcon || bestSizedIcon.width >= icon.width)) {
-        bestSizedIcon = icon;
-      }
-    }
-
-    // Note that some sites use hi-res icons without specifying them as
-    // apple-touch or fluid icons.
-    if (icon.isRichIcon || icon.width >= FAVICON_RICH_ICON_MIN_WIDTH) {
-      if (!largestRichIcon || largestRichIcon.width < icon.width) {
-        largestRichIcon = icon;
-      }
-    } else {
-      defaultIcon = icon;
-    }
-  }
-
-  // Now set the favicons for the page in the following order:
-  // 1. Set the best rich icon if any.
-  // 2. Set the preferred one if any, otherwise check if there's a better
-  //    sized fit.
-  // This order allows smaller icon frames to eventually override rich icon
-  // frames.
-
-  let tabIcon = null;
-  if (preferredIcon) {
-    tabIcon = preferredIcon;
-  } else if (bestSizedIcon) {
-    tabIcon = bestSizedIcon;
-  } else if (defaultIcon) {
-    tabIcon = defaultIcon;
+    return this._iconLoader;
   }
 
-  return {
-    richIcon: largestRichIcon,
-    tabIcon
-  };
-}
-
-function makeFaviconFromLink(aLink, aIsRichIcon) {
-  let iconUri = getLinkIconURI(aLink);
-  if (!iconUri)
-    return null;
-
-  // Extract the size type and width.
-  let width = extractIconSize(aLink.sizes);
-
-  return {
-    iconUri,
-    width,
-    isRichIcon: aIsRichIcon,
-    type: aLink.type,
-    node: aLink,
-  };
-}
-
-class IconLoader {
-  constructor(chromeGlobal) {
-    this.chromeGlobal = chromeGlobal;
-  }
-
-  async load(iconInfo) {
-    if (this._loader) {
-      this._loader.cancel();
-    }
-
-    if (LOCAL_FAVICON_SCHEMES.includes(iconInfo.iconUri.scheme)) {
-      this.chromeGlobal.sendAsyncMessage("Link:SetIcon", {
-        originalURL: iconInfo.iconUri.spec,
-        canUseForTab: !iconInfo.isRichIcon,
-        expiration: undefined,
-        iconURL: iconInfo.iconUri.spec,
-      });
-      return;
-    }
-
-    try {
-      this._loader = new FaviconLoad(iconInfo);
-      let { dataURL, expiration } = await this._loader.load();
-
-      this.chromeGlobal.sendAsyncMessage("Link:SetIcon", {
-        originalURL: iconInfo.iconUri.spec,
-        canUseForTab: !iconInfo.isRichIcon,
-        expiration,
-        iconURL: dataURL,
-      });
-    } catch (e) {
-      if (e.resultCode != Cr.NS_BINDING_ABORTED) {
-        Cu.reportError(e);
-
-        // Used mainly for tests currently.
-        this.chromeGlobal.sendAsyncMessage("Link:SetFailedIcon", {
-          originalURL: iconInfo.iconUri.spec,
-          canUseForTab: !iconInfo.isRichIcon,
-        });
+  addRootIcon() {
+    if (!this.seenTabIcon && Services.prefs.getBoolPref("browser.chrome.guess_favicon", true) &&
+        Services.prefs.getBoolPref("browser.chrome.site_icons", true)) {
+      // Inject the default icon. Use documentURIObject so that we do the right
+      // thing with about:-style error pages. See bug 453442
+      let baseURI = this.content.document.documentURIObject;
+      if (["http", "https"].includes(baseURI.scheme)) {
+        this.iconLoader.addDefaultIcon(baseURI);
       }
-    } finally {
-      this._loader = null;
-    }
-  }
-
-  cancel() {
-    if (!this._loader) {
-      return;
-    }
-
-    this._loader.cancel();
-    this._loader = null;
-  }
-}
-
-class ContentLinkHandler {
-  constructor(chromeGlobal) {
-    this.chromeGlobal = chromeGlobal;
-    this.iconInfos = [];
-    this.seenTabIcon = false;
-
-    chromeGlobal.addEventListener("DOMLinkAdded", this);
-    chromeGlobal.addEventListener("DOMLinkChanged", this);
-    chromeGlobal.addEventListener("pageshow", this);
-    chromeGlobal.addEventListener("pagehide", this);
-    chromeGlobal.addEventListener("DOMHeadElementParsed", this);
-
-    // For every page we attempt to find a rich icon and a tab icon. These
-    // objects take care of the load process for each.
-    this.richIconLoader = new IconLoader(chromeGlobal);
-    this.tabIconLoader = new IconLoader(chromeGlobal);
-
-    this.iconTask = new DeferredTask(() => this.loadIcons(), FAVICON_PARSING_TIMEOUT);
-  }
-
-  loadIcons() {
-    let preferredWidth = PREFERRED_WIDTH * Math.ceil(this.chromeGlobal.content.devicePixelRatio);
-    let { richIcon, tabIcon } = selectIcons(this.iconInfos, preferredWidth);
-    this.iconInfos = [];
-
-    if (richIcon) {
-      this.richIconLoader.load(richIcon);
-    }
-
-    if (tabIcon) {
-      this.tabIconLoader.load(tabIcon);
-    }
-  }
-
-  addIcon(iconInfo) {
-    if (!Services.prefs.getBoolPref("browser.chrome.site_icons", true)) {
-      return;
-    }
-
-    if (!iconInfo.isRichIcon) {
-      this.seenTabIcon = true;
-    }
-    this.iconInfos.push(iconInfo);
-    this.iconTask.arm();
-  }
-
-  addRootIcon(document) {
-    // If we've already seen a tab icon or if root favicons are disabled then
-    // bail out.
-    if (this.seenTabIcon || !Services.prefs.getBoolPref("browser.chrome.guess_favicon", true)) {
-      return;
-    }
-
-    // Currently ImageDocuments will just load the default favicon, see bug
-    // 403651 for discussion.
-
-    // Inject the default icon. Use documentURIObject so that we do the right
-    // thing with about:-style error pages. See bug 453442
-    let baseURI = document.documentURIObject;
-    if (baseURI.schemeIs("http") || baseURI.schemeIs("https")) {
-      let iconUri = baseURI.mutate().setPathQueryRef("/favicon.ico").finalize();
-      this.addIcon({
-        iconUri,
-        width: -1,
-        isRichIcon: false,
-        type: TYPE_ICO,
-        node: document,
-      });
     }
   }
 
   onHeadParsed(event) {
-    let document = this.chromeGlobal.content.document;
-    if (event.target.ownerDocument != document) {
+    if (event.target.ownerDocument != this.content.document) {
       return;
     }
 
     // Per spec icons are meant to be in the <head> tag so we should have seen
     // all the icons now so add the root icon if no other tab icons have been
     // seen.
-    this.addRootIcon(document);
+    this.addRootIcon();
 
     // We're likely done with icon parsing so load the pending icons now.
-    if (this.iconTask.isArmed) {
-      this.iconTask.disarm();
-      this.loadIcons();
+    if (this._iconLoader) {
+      this._iconLoader.onPageShow();
     }
   }
 
   onPageShow(event) {
-    let document = this.chromeGlobal.content.document;
-    if (event.target != document) {
+    if (event.target != this.content.document) {
       return;
     }
 
-    // Add the root icon if it hasn't already been added. We encounter this case
-    // for documents that do not have a <head> tag.
-    this.addRootIcon(document);
+    this.addRootIcon();
 
-    // If we've seen any additional icons since the start of the body element
-    // load them now.
-    if (this.iconTask.isArmed) {
-      this.iconTask.disarm();
-      this.loadIcons();
+    if (this._iconLoader) {
+      this._iconLoader.onPageShow();
     }
   }
 
   onPageHide(event) {
-    if (event.target != this.chromeGlobal.content.document) {
+    if (event.target != this.content.document) {
       return;
     }
 
-    this.richIconLoader.cancel();
-    this.tabIconLoader.cancel();
-
-    this.iconTask.disarm();
-    this.iconInfos = [];
-    this.seenTabIcon = false;
+    if (this._iconLoader) {
+      this._iconLoader.onPageHide();
+    }
   }
 
   onLinkEvent(event) {
     let link = event.target;
     // Ignore sub-frames (bugs 305472, 479408).
-    if (link.ownerGlobal != this.chromeGlobal.content) {
+    if (link.ownerGlobal != this.content) {
       return;
     }
 
     let rel = link.rel && link.rel.toLowerCase();
     if (!rel || !link.href)
       return;
 
     // Note: following booleans only work for the current link, not for the
@@ -575,83 +95,87 @@ class ContentLinkHandler {
     let feedAdded = false;
     let iconAdded = false;
     let searchAdded = false;
     let rels = {};
     for (let relString of rel.split(/\s+/))
       rels[relString] = true;
 
     for (let relVal in rels) {
-      let isRichIcon = true;
+      let isRichIcon = false;
 
       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)) {
-              this.chromeGlobal.sendAsyncMessage("Link:AddFeed", {
+              this.mm.sendAsyncMessage("Link:AddFeed", {
                 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":
+          isRichIcon = true;
+        case "icon":
           if (iconAdded || link.hasAttribute("mask")) { // Masked icons are not supported yet.
             break;
           }
 
-          let iconInfo = makeFaviconFromLink(link, isRichIcon);
+          if (!Services.prefs.getBoolPref("browser.chrome.site_icons", true)) {
+            return;
+          }
+
+          let iconInfo = FaviconLoader.makeFaviconFromLink(link, isRichIcon);
           if (iconInfo) {
-            iconAdded = this.addIcon(iconInfo);
+            iconAdded = true;
+            if (!isRichIcon) {
+              this.seenTabIcon = true;
+            }
+            this.iconLoader.addIcon(iconInfo);
           }
           break;
         case "search":
           if (Services.policies && !Services.policies.isAllowed("installSearchEngine")) {
             break;
           }
 
           if (!searchAdded && event.type == "DOMLinkAdded") {
             let type = link.type && link.type.toLowerCase();
             type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
 
             let re = /^(?:https?|ftp):/i;
             if (type == "application/opensearchdescription+xml" && link.title &&
                 re.test(link.href)) {
               let engine = { title: link.title, href: link.href };
-              this.chromeGlobal.sendAsyncMessage("Link:AddSearch", {
+              this.mm.sendAsyncMessage("Link:AddSearch", {
                 engine,
                 url: link.ownerDocument.documentURI,
               });
               searchAdded = true;
             }
           }
           break;
       }
     }
   }
 
   handleEvent(event) {
     switch (event.type) {
       case "pageshow":
-        this.onPageShow(event);
-        break;
+        return this.onPageShow(event);
       case "pagehide":
-        this.onPageHide(event);
-        break;
+        return this.onPageHide(event);
       case "DOMHeadElementParsed":
-        this.onHeadParsed(event);
-        break;
+        return this.onHeadParsed(event);
       default:
-        this.onLinkEvent(event);
+        return this.onLinkEvent(event);
     }
   }
 }
--- a/browser/actors/moz.build
+++ b/browser/actors/moz.build
@@ -26,16 +26,17 @@ FINAL_TARGET_FILES.actors += [
     'AboutReaderChild.jsm',
     'BlockedSiteChild.jsm',
     'BrowserTabChild.jsm',
     'ClickHandlerChild.jsm',
     'ContentSearchChild.jsm',
     'ContextMenuChild.jsm',
     'DOMFullscreenChild.jsm',
     'LightWeightThemeInstallChild.jsm',
+    'LinkHandlerChild.jsm',
     'NetErrorChild.jsm',
     'OfflineAppsChild.jsm',
     'PageInfoChild.jsm',
     'PageMetadataChild.jsm',
     'PageStyleChild.jsm',
     'PluginChild.jsm',
     'URIFixupChild.jsm',
     'WebRTCChild.jsm',
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -8,31 +8,29 @@
 
 /* eslint-env mozilla/frame-script */
 /* eslint no-unused-vars: ["error", {args: "none"}] */
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
-  ContentLinkHandler: "resource:///modules/ContentLinkHandler.jsm",
   ContentMetaHandler: "resource:///modules/ContentMetaHandler.jsm",
   FormSubmitObserver: "resource:///modules/FormSubmitObserver.jsm",
 });
 
 XPCOMUtils.defineLazyProxy(this, "formSubmitObserver", () => {
   return new FormSubmitObserver(content, this);
 }, {
   // stub QI
   QueryInterface: ChromeUtils.generateQI([Ci.nsIFormSubmitObserver, Ci.nsISupportsWeakReference])
 });
 
 Services.obs.addObserver(formSubmitObserver, "invalidformsubmit", true);
 
-new ContentLinkHandler(this);
 ContentMetaHandler.init(this);
 
 // This is a temporary hack to prevent regressions (bug 1471327).
 void content;
 
 addEventListener("DOMWindowFocus", function(event) {
   sendAsyncMessage("DOMWindowFocus", {});
 }, false);
--- a/browser/base/content/test/performance/browser_startup_content.js
+++ b/browser/base/content/test/performance/browser_startup_content.js
@@ -45,18 +45,18 @@ const whitelist = {
 
     // Forms and passwords
     "resource://formautofill/FormAutofill.jsm",
     "resource://formautofill/FormAutofillContent.jsm",
 
     // Browser front-end
     "resource:///actors/AboutReaderChild.jsm",
     "resource:///actors/BrowserTabChild.jsm",
-    "resource:///modules/ContentLinkHandler.jsm",
     "resource:///modules/ContentMetaHandler.jsm",
+    "resource:///modules/LinkHandlerChild.jsm",
     "resource:///actors/PageStyleChild.jsm",
     "resource://gre/modules/ActorChild.jsm",
     "resource://gre/modules/ActorManagerChild.jsm",
     "resource://gre/modules/E10SUtils.jsm",
     "resource://gre/modules/PrivateBrowsingUtils.jsm",
     "resource://gre/modules/ReaderMode.jsm",
     "resource://gre/modules/WebProgressChild.jsm",
 
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -126,16 +126,29 @@ let ACTORS = {
       events: {
         "InstallBrowserTheme": {wantUntrusted: true},
         "PreviewBrowserTheme": {wantUntrusted: true},
         "ResetBrowserThemePreview": {wantUntrusted: true},
       },
     },
   },
 
+  LinkHandler: {
+    child: {
+      module: "resource:///actors/LinkHandlerChild.jsm",
+      events: {
+        "DOMHeadElementParsed": {},
+        "DOMLinkAdded": {},
+        "DOMLinkChanged": {},
+        "pageshow": {},
+        "pagehide": {},
+      },
+    },
+  },
+
   NetError: {
     child: {
       module: "resource:///actors/NetErrorChild.jsm",
       events: {
         "AboutNetErrorLoad": {wantUntrusted: true},
         "AboutNetErrorOpenCaptivePortal": {wantUntrusted: true},
         "AboutNetErrorSetAutomatic": {wantUntrusted: true},
         "AboutNetErrorResetPreferences": {wantUntrusted: true},
copy from browser/modules/ContentLinkHandler.jsm
copy to browser/modules/FaviconLoader.jsm
--- a/browser/modules/ContentLinkHandler.jsm
+++ b/browser/modules/FaviconLoader.jsm
@@ -1,23 +1,21 @@
 /* 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";
 
-const EXPORTED_SYMBOLS = ["ContentLinkHandler"];
+const EXPORTED_SYMBOLS = ["FaviconLoader"];
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyGlobalGetters(this, ["Blob", "FileReader"]);
 
-ChromeUtils.defineModuleGetter(this, "Feeds",
-  "resource:///modules/Feeds.jsm");
 ChromeUtils.defineModuleGetter(this, "DeferredTask",
   "resource://gre/modules/DeferredTask.jsm");
 ChromeUtils.defineModuleGetter(this, "PromiseUtils",
   "resource://gre/modules/PromiseUtils.jsm");
 
 const BinaryInputStream = Components.Constructor("@mozilla.org/binaryinputstream;1",
                                                  "nsIBinaryInputStream", "setInputStream");
 
@@ -362,69 +360,52 @@ function selectIcons(iconInfos, preferre
   }
 
   return {
     richIcon: largestRichIcon,
     tabIcon
   };
 }
 
-function makeFaviconFromLink(aLink, aIsRichIcon) {
-  let iconUri = getLinkIconURI(aLink);
-  if (!iconUri)
-    return null;
-
-  // Extract the size type and width.
-  let width = extractIconSize(aLink.sizes);
-
-  return {
-    iconUri,
-    width,
-    isRichIcon: aIsRichIcon,
-    type: aLink.type,
-    node: aLink,
-  };
-}
-
 class IconLoader {
-  constructor(chromeGlobal) {
-    this.chromeGlobal = chromeGlobal;
+  constructor(mm) {
+    this.mm = mm;
   }
 
   async load(iconInfo) {
     if (this._loader) {
       this._loader.cancel();
     }
 
     if (LOCAL_FAVICON_SCHEMES.includes(iconInfo.iconUri.scheme)) {
-      this.chromeGlobal.sendAsyncMessage("Link:SetIcon", {
+      this.mm.sendAsyncMessage("Link:SetIcon", {
         originalURL: iconInfo.iconUri.spec,
         canUseForTab: !iconInfo.isRichIcon,
         expiration: undefined,
         iconURL: iconInfo.iconUri.spec,
       });
       return;
     }
 
     try {
       this._loader = new FaviconLoad(iconInfo);
       let { dataURL, expiration } = await this._loader.load();
 
-      this.chromeGlobal.sendAsyncMessage("Link:SetIcon", {
+      this.mm.sendAsyncMessage("Link:SetIcon", {
         originalURL: iconInfo.iconUri.spec,
         canUseForTab: !iconInfo.isRichIcon,
         expiration,
         iconURL: dataURL,
       });
     } catch (e) {
-      if (e.resultCode != Cr.NS_BINDING_ABORTED) {
+      if (e.result != Cr.NS_BINDING_ABORTED) {
         Cu.reportError(e);
 
         // Used mainly for tests currently.
-        this.chromeGlobal.sendAsyncMessage("Link:SetFailedIcon", {
+        this.mm.sendAsyncMessage("Link:SetFailedIcon", {
           originalURL: iconInfo.iconUri.spec,
           canUseForTab: !iconInfo.isRichIcon,
         });
       }
     } finally {
       this._loader = null;
     }
   }
@@ -434,224 +415,85 @@ class IconLoader {
       return;
     }
 
     this._loader.cancel();
     this._loader = null;
   }
 }
 
-class ContentLinkHandler {
-  constructor(chromeGlobal) {
-    this.chromeGlobal = chromeGlobal;
+class FaviconLoader {
+  constructor(mm) {
+    this.mm = mm;
     this.iconInfos = [];
-    this.seenTabIcon = false;
-
-    chromeGlobal.addEventListener("DOMLinkAdded", this);
-    chromeGlobal.addEventListener("DOMLinkChanged", this);
-    chromeGlobal.addEventListener("pageshow", this);
-    chromeGlobal.addEventListener("pagehide", this);
-    chromeGlobal.addEventListener("DOMHeadElementParsed", this);
 
     // For every page we attempt to find a rich icon and a tab icon. These
     // objects take care of the load process for each.
-    this.richIconLoader = new IconLoader(chromeGlobal);
-    this.tabIconLoader = new IconLoader(chromeGlobal);
+    this.richIconLoader = new IconLoader(mm);
+    this.tabIconLoader = new IconLoader(mm);
 
     this.iconTask = new DeferredTask(() => this.loadIcons(), FAVICON_PARSING_TIMEOUT);
   }
 
   loadIcons() {
-    let preferredWidth = PREFERRED_WIDTH * Math.ceil(this.chromeGlobal.content.devicePixelRatio);
+    let preferredWidth = PREFERRED_WIDTH * Math.ceil(this.mm.content.devicePixelRatio);
     let { richIcon, tabIcon } = selectIcons(this.iconInfos, preferredWidth);
     this.iconInfos = [];
 
     if (richIcon) {
       this.richIconLoader.load(richIcon);
     }
 
     if (tabIcon) {
       this.tabIconLoader.load(tabIcon);
     }
   }
 
   addIcon(iconInfo) {
-    if (!Services.prefs.getBoolPref("browser.chrome.site_icons", true)) {
-      return;
-    }
-
-    if (!iconInfo.isRichIcon) {
-      this.seenTabIcon = true;
-    }
     this.iconInfos.push(iconInfo);
     this.iconTask.arm();
   }
 
-  addRootIcon(document) {
-    // If we've already seen a tab icon or if root favicons are disabled then
-    // bail out.
-    if (this.seenTabIcon || !Services.prefs.getBoolPref("browser.chrome.guess_favicon", true)) {
-      return;
-    }
-
+  addDefaultIcon(baseURI) {
     // Currently ImageDocuments will just load the default favicon, see bug
     // 403651 for discussion.
-
-    // Inject the default icon. Use documentURIObject so that we do the right
-    // thing with about:-style error pages. See bug 453442
-    let baseURI = document.documentURIObject;
-    if (baseURI.schemeIs("http") || baseURI.schemeIs("https")) {
-      let iconUri = baseURI.mutate().setPathQueryRef("/favicon.ico").finalize();
-      this.addIcon({
-        iconUri,
-        width: -1,
-        isRichIcon: false,
-        type: TYPE_ICO,
-        node: document,
-      });
-    }
+    this.addIcon({
+      iconUri: baseURI.mutate().setPathQueryRef("/favicon.ico").finalize(),
+      width: -1,
+      isRichIcon: false,
+      type: TYPE_ICO,
+      node: this.mm.content.document,
+    });
   }
 
-  onHeadParsed(event) {
-    let document = this.chromeGlobal.content.document;
-    if (event.target.ownerDocument != document) {
-      return;
-    }
-
-    // Per spec icons are meant to be in the <head> tag so we should have seen
-    // all the icons now so add the root icon if no other tab icons have been
-    // seen.
-    this.addRootIcon(document);
-
+  onPageShow() {
     // We're likely done with icon parsing so load the pending icons now.
     if (this.iconTask.isArmed) {
       this.iconTask.disarm();
       this.loadIcons();
     }
   }
 
-  onPageShow(event) {
-    let document = this.chromeGlobal.content.document;
-    if (event.target != document) {
-      return;
-    }
-
-    // Add the root icon if it hasn't already been added. We encounter this case
-    // for documents that do not have a <head> tag.
-    this.addRootIcon(document);
-
-    // If we've seen any additional icons since the start of the body element
-    // load them now.
-    if (this.iconTask.isArmed) {
-      this.iconTask.disarm();
-      this.loadIcons();
-    }
-  }
-
-  onPageHide(event) {
-    if (event.target != this.chromeGlobal.content.document) {
-      return;
-    }
-
+  onPageHide() {
     this.richIconLoader.cancel();
     this.tabIconLoader.cancel();
 
     this.iconTask.disarm();
     this.iconInfos = [];
-    this.seenTabIcon = false;
   }
 
-  onLinkEvent(event) {
-    let link = event.target;
-    // Ignore sub-frames (bugs 305472, 479408).
-    if (link.ownerGlobal != this.chromeGlobal.content) {
-      return;
-    }
-
-    let rel = link.rel && link.rel.toLowerCase();
-    if (!rel || !link.href)
-      return;
+  static makeFaviconFromLink(aLink, aIsRichIcon) {
+    let iconUri = getLinkIconURI(aLink);
+    if (!iconUri)
+      return null;
 
-    // Note: following booleans only work for the current link, not for the
-    // whole content
-    let feedAdded = false;
-    let iconAdded = false;
-    let searchAdded = false;
-    let 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;
+    // Extract the size type and width.
+    let width = extractIconSize(aLink.sizes);
 
-            if (Feeds.isValidFeed(link, link.ownerDocument.nodePrincipal, "feed" in rels)) {
-              this.chromeGlobal.sendAsyncMessage("Link:AddFeed", {
-                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 || link.hasAttribute("mask")) { // Masked icons are not supported yet.
-            break;
-          }
-
-          let iconInfo = makeFaviconFromLink(link, isRichIcon);
-          if (iconInfo) {
-            iconAdded = this.addIcon(iconInfo);
-          }
-          break;
-        case "search":
-          if (Services.policies && !Services.policies.isAllowed("installSearchEngine")) {
-            break;
-          }
-
-          if (!searchAdded && event.type == "DOMLinkAdded") {
-            let type = link.type && link.type.toLowerCase();
-            type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
-
-            let re = /^(?:https?|ftp):/i;
-            if (type == "application/opensearchdescription+xml" && link.title &&
-                re.test(link.href)) {
-              let engine = { title: link.title, href: link.href };
-              this.chromeGlobal.sendAsyncMessage("Link:AddSearch", {
-                engine,
-                url: link.ownerDocument.documentURI,
-              });
-              searchAdded = true;
-            }
-          }
-          break;
-      }
-    }
-  }
-
-  handleEvent(event) {
-    switch (event.type) {
-      case "pageshow":
-        this.onPageShow(event);
-        break;
-      case "pagehide":
-        this.onPageHide(event);
-        break;
-      case "DOMHeadElementParsed":
-        this.onHeadParsed(event);
-        break;
-      default:
-        this.onLinkEvent(event);
-    }
+    return {
+      iconUri,
+      width,
+      isRichIcon: aIsRichIcon,
+      type: aLink.type,
+      node: aLink,
+    };
   }
 }
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -119,21 +119,21 @@ XPCSHELL_TESTS_MANIFESTS += ['test/unit/
 EXTRA_JS_MODULES += [
     'AboutNewTab.jsm',
     'AsyncTabSwitcher.jsm',
     'BrowserErrorReporter.jsm',
     'BrowserUsageTelemetry.jsm',
     'BrowserWindowTracker.jsm',
     'ContentClick.jsm',
     'ContentCrashHandlers.jsm',
-    'ContentLinkHandler.jsm',
     'ContentMetaHandler.jsm',
     'ContentObservers.js',
     'ContentSearch.jsm',
     'ExtensionsUI.jsm',
+    'FaviconLoader.jsm',
     'Feeds.jsm',
     'FormSubmitObserver.jsm',
     'FormValidationHandler.jsm',
     'HomePage.jsm',
     'LaterRun.jsm',
     'LightweightThemeChildHelper.jsm',
     'OpenInTabsUtils.jsm',
     'PageActions.jsm',