Bug 1354820 - Idle unloader shim prototype. draft
authorAndrew McCreight <continuation@gmail.com>
Sat, 08 Apr 2017 08:17:50 -0700
changeset 559318 cf5fbee90c9464fe7dc3aa01b98c3228d5071aca
parent 558921 2129414210f15de2cb9cc39d73ede4afeb17ceb3
child 623359 b68244742edd0de3646757d44ae87329e4459544
push id53054
push userbmo:continuation@gmail.com
push dateSun, 09 Apr 2017 18:17:02 +0000
bugs1354820
milestone55.0a1
Bug 1354820 - Idle unloader shim prototype. MozReview-Commit-ID: H68wAplJgQo
browser/base/content/browser.js
js/xpconnect/loader/XPCOMUtils.jsm
toolkit/modules/Color.jsm
toolkit/modules/Finder.jsm
toolkit/modules/FinderHighlighter.jsm
toolkit/modules/FinderIterator.jsm
toolkit/modules/NLP.jsm
toolkit/modules/tests/xpcshell/test_Color.js
toolkit/modules/tests/xpcshell/test_FinderIterator.js
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -42,17 +42,16 @@ XPCOMUtils.defineLazyModuleGetter(this, 
  */
 [
   ["AboutHome", "resource:///modules/AboutHome.jsm"],
   ["BrowserUITelemetry", "resource:///modules/BrowserUITelemetry.jsm"],
   ["BrowserUsageTelemetry", "resource:///modules/BrowserUsageTelemetry.jsm"],
   ["BrowserUtils", "resource://gre/modules/BrowserUtils.jsm"],
   ["CastingApps", "resource:///modules/CastingApps.jsm"],
   ["CharsetMenu", "resource://gre/modules/CharsetMenu.jsm"],
-  ["Color", "resource://gre/modules/Color.jsm"],
   ["ContentSearch", "resource:///modules/ContentSearch.jsm"],
   ["Deprecated", "resource://gre/modules/Deprecated.jsm"],
   ["E10SUtils", "resource:///modules/E10SUtils.jsm"],
   ["ExtensionsUI", "resource:///modules/ExtensionsUI.jsm"],
   ["FormValidationHandler", "resource:///modules/FormValidationHandler.jsm"],
   ["FullZoomUI", "resource:///modules/FullZoomUI.jsm"],
   ["GMPInstallManager", "resource://gre/modules/GMPInstallManager.jsm"],
   ["LightweightThemeManager", "resource://gre/modules/LightweightThemeManager.jsm"],
@@ -81,16 +80,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   ["UpdateUtils", "resource://gre/modules/UpdateUtils.jsm"],
   ["Weave", "resource://services-sync/main.js"],
   ["fxAccounts", "resource://gre/modules/FxAccounts.jsm"],
   ["gDevTools", "resource://devtools/client/framework/gDevTools.jsm"],
   ["gDevToolsBrowser", "resource://devtools/client/framework/gDevTools.jsm"],
   ["webrtcUI", "resource:///modules/webrtcUI.jsm"],
 ].forEach(([name, resource]) => XPCOMUtils.defineLazyModuleGetter(this, name, resource));
 
+XPCOMUtils.defineIdleUnloadLazyModuleGetter(this, "Color",
+                                            "resource://gre/modules/Color.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "SafeBrowsing",
   "resource://gre/modules/SafeBrowsing.jsm");
 
 if (AppConstants.MOZ_CRASHREPORTER) {
   XPCOMUtils.defineLazyModuleGetter(this, "PluginCrashReporter",
     "resource:///modules/ContentCrashHandlers.jsm");
 }
 
--- a/js/xpconnect/loader/XPCOMUtils.jsm
+++ b/js/xpconnect/loader/XPCOMUtils.jsm
@@ -480,8 +480,114 @@ function makeQI(interfaceNames) {
     for (let i = 0; i < interfaceNames.length; i++) {
       if (Ci[interfaceNames[i]].equals(iid))
         return this;
     }
 
     throw Cr.NS_ERROR_NO_INTERFACE;
   };
 }
+
+/**
+ * Unload modules that have been idle for some period.
+ */
+function myLog(aMsg)
+{
+  let proc = "[PARENT]";
+  if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT) {
+    proc = "[CHILDD]";
+  }
+  dump("ZZZ " + proc + " " + aMsg + "\n");
+}
+
+// XXX Is there some way we can detect that a jsm has been imported
+// normally and via the unloadable service? That will may cause weird
+// behavior.
+
+// XXX Make these fields of XPCOMUtils?
+var resourceIds = new Map();
+var resources = [];
+var importedValues = [];
+var actives = [];
+var unloaderTimerId = null;
+const idleUnloadTime = 5000;
+
+function unloaderTimerFunction()
+{
+  myLog(">> unloader timer fired");
+  let anyActive = false;
+
+  for (let currId = 0; currId < importedValues.length; currId++) {
+    if (actives[currId]) {
+      myLog("timer rejected " + resources[currId]);
+      actives[currId] = false;
+      anyActive = true;
+    } else if (importedValues[currId]) {
+      // XXX Ideally, throw an error or something nicer if this
+      // property is not defined.
+      if (importedValues[currId]["Reconstitutable"].is) {
+        myLog("unloading " + resources[currId]);
+        importedValues[currId] = null;
+        Cu.unload(resources[currId]);
+      } else {
+        myLog("can't unload " + resources[currId]);
+        // We can't reconstitute now, but maybe we can later.
+        anyActive = true;
+      }
+    } else {
+      myLog("sleep now " + resources[currId]);
+    }
+  }
+
+  if (!anyActive) {
+    myLog("stopping timer");
+    clearInterval(unloaderTimerId);
+    unloaderTimerId = null;
+  }
+}
+
+// XXX Move this up.
+Cu.import("resource://gre/modules/Timer.jsm");
+
+XPCOMUtils.defineIdleUnloadLazyModuleGetter = (aObject, aName, aResource, aSymbol) =>
+{
+  let resourceId = resourceIds.get(aResource);
+  if (typeof resourceId === "undefined") {
+    resourceId = importedValues.length;
+    resourceIds.set(aResource, resourceId);
+    resources.push(aResource);
+    importedValues.push(null);
+    actives.push(false);
+    myLog("init with new id " + resourceId + " for " + aResource);
+  } else {
+    myLog("found existing id " + resourceId + " for " + aResource);
+  }
+
+  Object.defineProperty(aObject, aName, {
+    get: function () {
+      if (importedValues[resourceId] == null) {
+        var values = {};
+        try {
+          Cu.import(aResource, values);
+        } catch (ex) {
+          Cu.reportError("Failed to load module " + aResource + ".");
+          throw ex;
+        }
+        importedValues[resourceId] = values;
+        myLog("import " + (aSymbol || aName));
+
+        if (!unloaderTimerId) {
+          myLog("starting timer");
+          unloaderTimerId = setInterval(unloaderTimerFunction,
+                                        idleUnloadTime);
+        }
+      }
+      if (!actives[resourceId]) {
+        myLog("activated " + aName);
+      }
+      actives[resourceId] = true;
+
+      return importedValues[resourceId][aSymbol || aName];
+    },
+    configurable: true,
+    enumerable: true
+  });
+}
--- a/toolkit/modules/Color.jsm
+++ b/toolkit/modules/Color.jsm
@@ -1,15 +1,15 @@
 /* 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";
 
-this.EXPORTED_SYMBOLS = ["Color"];
+this.EXPORTED_SYMBOLS = ["Color", "Reconstitutable"];
 
 /**
  * Color class, which describes a color.
  * In the future, this object may be extended to allow for conversions between
  * different color formats and notations, support transparency.
  *
  * @param {Number} r Red color component
  * @param {Number} g Green color component
@@ -78,8 +78,18 @@ Color.prototype = {
    * @return {Boolean}
    */
   isContrastRatioAcceptable(otherColor) {
     // Note: this is a high enough value to be considered as 'high contrast',
     //       but was decided upon empirically.
     return this.contrastRatio(otherColor) > 3;
   }
 };
+
+// XXX What happens if we unload and a color object exists? Do we not
+// really clear it up?  Is the Color object something in the Color.jsm
+// compartment? Maybe it would just keep the compartment alive? But
+// what if we then import it again? Maybe unload wouldn't really drop
+// the mapping until the compartment goes away? Wouldn't be too hard
+// to test.
+this.Reconstitutable = {
+  is : true,
+};
--- a/toolkit/modules/Finder.jsm
+++ b/toolkit/modules/Finder.jsm
@@ -20,16 +20,19 @@ XPCOMUtils.defineLazyServiceGetter(this,
                                          "nsITextToSubURI");
 XPCOMUtils.defineLazyServiceGetter(this, "Clipboard",
                                          "@mozilla.org/widget/clipboard;1",
                                          "nsIClipboard");
 XPCOMUtils.defineLazyServiceGetter(this, "ClipboardHelper",
                                          "@mozilla.org/widget/clipboardhelper;1",
                                          "nsIClipboardHelper");
 
+XPCOMUtils.defineIdleUnloadLazyModuleGetter(this, "FinderHighlighter",
+                                            "resource://gre/modules/FinderHighlighter.jsm");
+
 const kSelectionMaxLen = 150;
 const kMatchesCountLimitPref = "accessibility.typeaheadfind.matchesCountLimit";
 
 function Finder(docShell) {
   this._fastFind = Cc["@mozilla.org/typeaheadfind;1"].createInstance(Ci.nsITypeAheadFind);
   this._fastFind.init(docShell);
 
   this._currentFoundRange = null;
@@ -45,17 +48,19 @@ function Finder(docShell) {
   BrowserUtils.getRootWindow(this._docShell).addEventListener("unload",
     this.onLocationChange.bind(this, { isTopLevel: true }));
 }
 
 Finder.prototype = {
   get iterator() {
     if (this._iterator)
       return this._iterator;
-    this._iterator = Cu.import("resource://gre/modules/FinderIterator.jsm", null).FinderIterator;
+    XPCOMUtils.defineIdleUnloadLazyModuleGetter(this, "_iterator",
+                                                "resource://gre/modules/FinderIterator.jsm",
+                                                "FinderIterator");
     return this._iterator;
   },
 
   destroy() {
     if (this._iterator)
       this._iterator.reset();
     let window = this._getWindow();
     if (this._highlighter && window) {
@@ -161,18 +166,16 @@ Finder.prototype = {
       return;
     this._fastFind.entireWord = aEntireWord;
     this.iterator.reset();
   },
 
   get highlighter() {
     if (this._highlighter)
       return this._highlighter;
-
-    const {FinderHighlighter} = Cu.import("resource://gre/modules/FinderHighlighter.jsm", {});
     return this._highlighter = new FinderHighlighter(this);
   },
 
   get matchesCountLimit() {
     if (typeof this._matchesCountLimit == "number")
       return this._matchesCountLimit;
 
     this._matchesCountLimit = Services.prefs.getIntPref(kMatchesCountLimitPref) || 0;
--- a/toolkit/modules/FinderHighlighter.jsm
+++ b/toolkit/modules/FinderHighlighter.jsm
@@ -1,23 +1,24 @@
 /* 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";
 
-this.EXPORTED_SYMBOLS = ["FinderHighlighter"];
+this.EXPORTED_SYMBOLS = ["FinderHighlighter", "Reconstitutable"];
 
 const { interfaces: Ci, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "Color", "resource://gre/modules/Color.jsm");
+XPCOMUtils.defineIdleUnloadLazyModuleGetter(this, "Color",
+                                            "resource://gre/modules/Color.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Rect", "resource://gre/modules/Geometry.jsm");
 XPCOMUtils.defineLazyGetter(this, "kDebug", () => {
   const kDebugPref = "findbar.modalHighlight.debug";
   return Services.prefs.getPrefType(kDebugPref) && Services.prefs.getBoolPref(kDebugPref);
 });
 
 const kContentChangeThresholdPx = 5;
 const kBrightTextSampleSize = 5;
@@ -144,17 +145,19 @@ function FinderHighlighter(finder) {
   this._modal = Services.prefs.getBoolPref(kModalHighlightPref);
   this.finder = finder;
 }
 
 FinderHighlighter.prototype = {
   get iterator() {
     if (this._iterator)
       return this._iterator;
-    this._iterator = Cu.import("resource://gre/modules/FinderIterator.jsm", null).FinderIterator;
+    XPCOMUtils.defineIdleUnloadLazyModuleGetter(this, "_iterator",
+                                                "resource://gre/modules/FinderIterator.jsm",
+                                                "FinderIterator");
     return this._iterator;
   },
 
   /**
    * Each window is unique, globally, and the relation between an active
    * highlighting session and a window is 1:1.
    * For each window we track a number of properties which _at least_ consist of
    *  - {Boolean} detectedGeometryChange Whether the geometry of the found ranges'
@@ -1618,8 +1621,17 @@ FinderHighlighter.prototype = {
       },
 
       // Unimplemented
       notifyDocumentCreated() {},
       notifyDocumentStateChanged(aDirty) {}
     };
   }
 };
+
+// XXX This doesn't really work, because the map doesn't seem to get
+// cleared, even if we're not currently highlighting anything. Not
+// sure how to deal with that.
+this.Reconstitutable = {
+  get is() {
+    return ThreadSafeChromeUtils.nondeterministicGetWeakMapKeys(gWindows).length == 0;
+  }
+};
--- a/toolkit/modules/FinderIterator.jsm
+++ b/toolkit/modules/FinderIterator.jsm
@@ -1,24 +1,24 @@
 /* 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";
 
-this.EXPORTED_SYMBOLS = ["FinderIterator"];
+this.EXPORTED_SYMBOLS = ["FinderIterator", "Reconstitutable"];
 
 const { interfaces: Ci, classes: Cc, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "NLP", "resource://gre/modules/NLP.jsm");
+XPCOMUtils.defineIdleUnloadLazyModuleGetter(this, "NLP", "resource://gre/modules/NLP.jsm");
 
 const kDebug = false;
 const kIterationSizeMax = 100;
 const kTimeoutPref = "findbar.iteratorTimeout";
 
 /**
  * FinderIterator singleton. See the documentation for the `start()` method to
  * learn more.
@@ -650,8 +650,14 @@ this.FinderIterator = {
       }
 
       node = node.parentNode;
     } while (node);
 
     return isInsideLink;
   }
 };
+
+this.Reconstitutable = {
+  get is() {
+    return !FinderIterator.running;
+  }
+};
--- a/toolkit/modules/NLP.jsm
+++ b/toolkit/modules/NLP.jsm
@@ -1,15 +1,15 @@
 /* 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";
 
-this.EXPORTED_SYMBOLS = ["NLP"];
+this.EXPORTED_SYMBOLS = ["NLP", "Reconstitutable"];
 
 /**
  * NLP, which stands for Natural Language Processing, is a module that provides
  * an entry point to various methods to interface with human language.
  *
  * At least, that's the goal. Eventually. Right now, the find toolbar only really
  * needs the Levenshtein distance algorithm.
  */
@@ -69,8 +69,12 @@ this.NLP = {
       p2 = tmp;
     }
 
     c0 = p1[l2];
 
     return c0;
   }
 };
+
+this.Reconstitutable = {
+  is : true,
+};
--- a/toolkit/modules/tests/xpcshell/test_Color.js
+++ b/toolkit/modules/tests/xpcshell/test_Color.js
@@ -1,11 +1,12 @@
 "use strict";
 
-Components.utils.import("resource://gre/modules/Color.jsm");
+XPCOMUtils.defineIdleUnloadLazyModuleGetter(this, "Color",
+                                            "resource://gre/modules/Color.jsm");
 
 function run_test() {
   testRelativeLuminance();
   testIsBright();
   testContrastRatio();
   testIsContrastRatioAcceptable();
 }
 
--- a/toolkit/modules/tests/xpcshell/test_FinderIterator.js
+++ b/toolkit/modules/tests/xpcshell/test_FinderIterator.js
@@ -1,10 +1,13 @@
 const { interfaces: Ci, classes: Cc, utils: Cu } = Components;
-const { FinderIterator } = Cu.import("resource://gre/modules/FinderIterator.jsm", {});
+
+XPCOMUtils.defineIdleUnloadLazyModuleGetter(this, "FinderIterator",
+                                            "resource://gre/modules/FinderIterator.jsm");
+
 Cu.import("resource://gre/modules/Promise.jsm");
 
 var gFindResults = [];
 // Stub the method that instantiates nsIFind and does all the interaction with
 // the docShell to be searched through.
 FinderIterator._iterateDocument = function* (word, window, finder) {
   for (let range of gFindResults)
     yield range;