Bug 1374382 - [WIP] Defer DOMLinkAdded and DOMLinkChanged reactions until an idlecallback. draft
authorMike Conley <mconley@mozilla.com>
Wed, 31 Jan 2018 14:35:35 -0500
changeset 750493 a716c50c9ca0ba61abf8bd9835f6ff6fab9770b3
parent 748372 c0f08b020685f67a7ea08658731adb410f70b7e6
child 750869 41963105685e64c313c265270d750d3158f7ddf1
child 751527 0e4d35286689a33ec8ada4990344cb350386aabc
child 751530 e8c3d666e8ed7c669896fa68891db2e0faff5d02
child 751796 75674d67b5a54f51724596a1d24006c61c5e62c9
child 752271 67174953035222209bd1200aca2b8ca38bd8e91f
push id97689
push usermconley@mozilla.com
push dateFri, 02 Feb 2018 14:53:25 +0000
bugs1374382
milestone60.0a1
Bug 1374382 - [WIP] Defer DOMLinkAdded and DOMLinkChanged reactions until an idlecallback. MozReview-Commit-ID: FqI6PsUrGsB
browser/base/content/content.js
browser/modules/ContentLinkHandler.jsm
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -663,17 +663,22 @@ var ClickEventHandler = {
     // callers expect <a>-like elements.
     // Note: makeURI() will throw if aUri is not a valid URI.
     return [href ? Services.io.newURI(href, null, baseURI).spec : null, null,
             node && node.ownerDocument.nodePrincipal];
   }
 };
 ClickEventHandler.init();
 
-ContentLinkHandler.init(this);
+var contentLinkHandler;
+
+if (content === content.top) {
+ contentLinkHandler = new ContentLinkHandler(this);
+}
+//ContentLinkHandler.init(this);
 ContentMetaHandler.init(this);
 
 // TODO: Load this lazily so the JSM is run only if a relevant event/message fires.
 var pluginContent = new PluginContent(global);
 
 addEventListener("DOMWindowFocus", function(event) {
   sendAsyncMessage("DOMWindowFocus", {});
 }, false);
--- a/browser/modules/ContentLinkHandler.jsm
+++ b/browser/modules/ContentLinkHandler.jsm
@@ -263,36 +263,139 @@ function handleFaviconLink(aLink, aIsRic
     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) {
-    const faviconLoads = new Map();
-    chromeGlobal.addEventListener("DOMLinkAdded", event => {
-      this.onLinkEvent(event, chromeGlobal, faviconLoads);
-    });
-    chromeGlobal.addEventListener("DOMLinkChanged", event => {
-      this.onLinkEvent(event, chromeGlobal, faviconLoads);
-    });
-    chromeGlobal.addEventListener("unload", event => {
-      for (const [pageUrl, load] of faviconLoads) {
+this.ContentLinkHandler = function(global) {
+  this.init(global);
+};
+
+ContentLinkHandler.prototype = {
+  init(global) {
+    this.global = global;
+    this.content = this.global.content;
+    this.reset();
+
+    global.addEventListener("DOMLinkAdded", this);
+    global.addEventListener("DOMLinkChanged", this);
+    global.addEventListener("pagehide", this);
+    global.addEventListener("unload", this);
+    global.addEventListener("load", this, true);
+  },
+
+  uninit() {
+    let global = this.global;
+
+    global.removeEventListener("DOMLinkAdded", this);
+    global.removeEventListener("DOMLinkChanged", this);
+    global.removeEventListener("pagehide", this);
+    global.removeEventListener("unload", this);
+    global.removeEventListener("load", this, true);
+
+    delete this.eventQueue;
+    delete this.faviconLoads;
+    delete this.idleTimerId;
+    delete this.content;
+    delete this.global;
+  },
+
+  reset() {
+    this.cancelDeferredFlush();
+
+    if (this.faviconLoads) {
+      for (const [pageUrl, load] of this.faviconLoads) {
         load.timer.cancel();
         load.timer = null;
-        faviconLoads.delete(pageUrl);
+        this.faviconLoads.delete(pageUrl);
+      }
+    }
+
+    this.waitingForLoad = true;
+    this.eventQueue = [];
+    this.faviconLoads = new Map();
+    this.idleTimerId = null;
+  },
+
+  handleEvent(event) {
+    let window = event.originalTarget.ownerGlobal;
+    if (window && window.top !== window) {
+      return;
+    }
+
+    switch (event.type) {
+      case "DOMLinkChanged":
+        // fall-through
+      case "DOMLinkAdded": {
+        this.queueEvent(event);
+        break;
+      }
+
+      case "load": {
+        if (this.waitingForLoad) {
+          this.waitingForLoad = false;
+          this.setDeferredFlush();
+        }
+        break;
+      }
+
+      case "pagehide": {
+        this.reset();
+        break;
       }
+      case "unload": {
+        this.uninit();
+        break;
+      }
+    }
+  },
+
+  cancelDeferredFlush() {
+    if (this.idleTimerId) {
+      this.content.cancelIdleCallback(this.idleTimerId);
+      this.idleTimerId = null;
+    }
+  },
+
+  setDeferredFlush() {
+    if (this.waitingForLoad) {
+      return;
+    }
+
+    if (this.idleTimerId) {
+      this.cancelDeferredFlush();
+    }
+
+    this.idleTimerId = this.content.requestIdleCallback(idleDeadline => {
+      this.flush(idleDeadline);
     });
   },
 
+  queueEvent(event) {
+    this.eventQueue.push(event);
+    this.setDeferredFlush();
+  },
+
+  flush(idleDeadline) {
+    while (idleDeadline.timeRemaining() && this.eventQueue.length > 0) {
+      let event = this.eventQueue.shift();
+      this.onLinkEvent(event, this.global, this.faviconLoads);
+    }
+
+    if (this.eventQueue.length > 0) {
+      this.setDeferredFlush();
+    }
+  },
+
   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;