--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -39,16 +39,18 @@ XPCOMUtils.defineLazyModuleGetter(this,
"resource://gre/modules/MessageChannel.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
"resource://gre/modules/PromiseUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
"resource://gre/modules/Schemas.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames",
"resource://gre/modules/WebNavigationFrames.jsm");
+const Timer = Components.Constructor("@mozilla.org/timer;1", "nsITimer", "initWithCallback");
+
Cu.import("resource://gre/modules/ExtensionChild.jsm");
Cu.import("resource://gre/modules/ExtensionCommon.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
const {
DefaultMap,
EventEmitter,
LocaleData,
@@ -98,19 +100,57 @@ var apiManager = new class extends Schem
registerSchemaAPI(namespace, envType, getAPI) {
if (envType == "content_child") {
super.registerSchemaAPI(namespace, envType, getAPI);
}
}
}();
+const SCRIPT_EXPIRY_TIMEOUT_MS = 300000;
+const SCRIPT_CLEAR_TIMEOUT_MS = 5000;
+
+const scriptCaches = new WeakSet();
+
class ScriptCache extends DefaultMap {
constructor(options) {
super(url => ChromeUtils.compileScript(url, options));
+
+ scriptCaches.add(this);
+ }
+
+ get(url) {
+ let script = super.get(url);
+
+ script.lastUsed = Date.now();
+ if (script.timer) {
+ script.timer.cancel();
+ }
+ script.timer = Timer(this.delete.bind(this, url),
+ SCRIPT_EXPIRY_TIMEOUT_MS,
+ Ci.nsITimer.TYPE_ONE_SHOT);
+
+ return script;
+ }
+
+ delete(url) {
+ if (this.has(url)) {
+ super.get(url).timer.cancel();
+ }
+
+ super.delete(url);
+ }
+
+ clear(timeout = SCRIPT_CLEAR_TIMEOUT_MS) {
+ let now = Date.now();
+ for (let [url, script] of this.entries()) {
+ if (now - script.lastUsed >= timeout) {
+ this.delete(url);
+ }
+ }
}
}
// Represents a content script.
function Script(extension, options, deferred = PromiseUtils.defer()) {
this.extension = extension;
this.options = options;
this.run_at = this.options.run_at;
@@ -532,25 +572,27 @@ DocumentManager = {
init() {
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
Services.obs.addObserver(this, "http-on-opening-request", false);
}
Services.obs.addObserver(this, "content-document-global-created", false);
Services.obs.addObserver(this, "document-element-inserted", false);
Services.obs.addObserver(this, "inner-window-destroyed", false);
+ Services.obs.addObserver(this, "memory-pressure", false);
},
uninit() {
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
Services.obs.removeObserver(this, "http-on-opening-request");
}
Services.obs.removeObserver(this, "content-document-global-created");
Services.obs.removeObserver(this, "document-element-inserted");
Services.obs.removeObserver(this, "inner-window-destroyed");
+ Services.obs.removeObserver(this, "memory-pressure");
},
getWindowState(contentWindow) {
let readyState = contentWindow.document.readyState;
if (readyState == "complete") {
return "document_idle";
}
if (readyState == "interactive") {
@@ -643,16 +685,22 @@ DocumentManager = {
let {loadInfo} = subject.QueryInterface(Ci.nsIChannel);
if (loadInfo) {
let {externalContentPolicyType: type} = loadInfo;
if (type === Ci.nsIContentPolicy.TYPE_DOCUMENT ||
type === Ci.nsIContentPolicy.TYPE_SUBDOCUMENT) {
this.preloadScripts(subject.URI, loadInfo);
}
}
+ } else if (topic === "memory-pressure") {
+ let timeout = data === "heap-minimize" ? 0 : undefined;
+
+ for (let cache of ChromeUtils.nondeterministicGetWeakSetKeys(scriptCaches)) {
+ cache.clear(timeout);
+ }
}
},
handleEvent(event) {
let window = event.currentTarget;
if (event.target != window.document) {
// We use capturing listeners so we have precedence over content script
// listeners, but only care about events targeted to the element we're