Bug 932880 - Fix window leaks in App Manager. r=past

CLOSED TREE
This commit is contained in:
J. Ryan Stinnett 2013-11-01 11:17:07 -05:00
parent b53c481fcc
commit 9ad29bd326
10 changed files with 152 additions and 41 deletions

View File

@ -19,11 +19,11 @@ module.exports = ConnectionStore = function(connection) {
ObservableObject.call(this, {status:null,host:null,port:null});
this._destroy = this._destroy.bind(this);
this.destroy = this.destroy.bind(this);
this._feedStore = this._feedStore.bind(this);
this._connection = connection;
this._connection.once(Connection.Events.DESTROYED, this._destroy);
this._connection.once(Connection.Events.DESTROYED, this.destroy);
this._connection.on(Connection.Events.STATUS_CHANGED, this._feedStore);
this._connection.on(Connection.Events.PORT_CHANGED, this._feedStore);
this._connection.on(Connection.Events.HOST_CHANGED, this._feedStore);
@ -32,12 +32,18 @@ module.exports = ConnectionStore = function(connection) {
}
ConnectionStore.prototype = {
_destroy: function() {
this._connection.off(Connection.Events.STATUS_CHANGED, this._feedStore);
this._connection.off(Connection.Events.PORT_CHANGED, this._feedStore);
this._connection.off(Connection.Events.HOST_CHANGED, this._feedStore);
_knownConnectionStores.delete(this._connection);
this._connection = null;
destroy: function() {
if (this._connection) {
// While this.destroy is bound using .once() above, that event may not
// have occurred when the ConnectionStore client calls destroy, so we
// manually remove it here.
this._connection.off(Connection.Events.DESTROYED, this.destroy);
this._connection.off(Connection.Events.STATUS_CHANGED, this._feedStore);
this._connection.off(Connection.Events.PORT_CHANGED, this._feedStore);
this._connection.off(Connection.Events.HOST_CHANGED, this._feedStore);
_knownConnectionStores.delete(this._connection);
this._connection = null;
}
},
_feedStore: function() {

View File

@ -18,6 +18,11 @@ const DeviceStore = require("devtools/app-manager/device-store");
const simulatorsStore = require("devtools/app-manager/simulators-store");
const adbStore = require("devtools/app-manager/builtin-adb-store");
window.addEventListener("unload", function onUnload() {
window.removeEventListener("unload", onUnload);
UI.destroy();
});
let UI = {
init: function() {
this.useFloatingScrollbarsIfNeeded();
@ -54,10 +59,7 @@ let UI = {
let pre = document.querySelector("#logs > pre");
pre.textContent = this.connection.logs;
pre.scrollTop = pre.scrollTopMax;
this.connection.on(Connection.Events.NEW_LOG, (event, str) => {
pre.textContent += "\n" + str;
pre.scrollTop = pre.scrollTopMax;
});
this.connection.on(Connection.Events.NEW_LOG, this._onNewLog);
this.template = new Template(document.body, this.store, Utils.l10n);
this.template.start();
@ -66,6 +68,18 @@ let UI = {
this._onSimulatorDisconnected = this._onSimulatorDisconnected.bind(this);
},
destroy: function() {
this.store.destroy();
this.connection.off(Connection.Events.NEW_LOG, this._onNewLog);
this.template.destroy();
},
_onNewLog: function(event, str) {
let pre = document.querySelector("#logs > pre");
pre.textContent += "\n" + str;
pre.scrollTop = pre.scrollTopMax;
},
useFloatingScrollbarsIfNeeded: function() {
if (Services.appinfo.OS == "Darwin") {
return;

View File

@ -32,12 +32,16 @@ window.addEventListener("message", function(event) {
} catch(e) {
Cu.reportError(e);
}
}, false);
});
window.addEventListener("unload", function onUnload() {
window.removeEventListener("unload", onUnload);
UI.destroy();
});
let UI = {
init: function() {
this.showFooterIfNeeded();
this._onConnectionStatusChange = this._onConnectionStatusChange.bind(this);
this.setTab("apps");
if (this.connection) {
this.onNewConnection();
@ -46,6 +50,18 @@ let UI = {
}
},
destroy: function() {
if (this.connection) {
this.connection.off(Connection.Events.STATUS_CHANGED, this._onConnectionStatusChange);
}
if (this.store) {
this.store.destroy();
}
if (this.template) {
this.template.destroy();
}
},
showFooterIfNeeded: function() {
let footer = document.querySelector("#connection-footer");
if (window.parent == window) {
@ -73,6 +89,9 @@ let UI = {
"apps": new WebappsStore(this.connection),
});
if (this.template) {
this.template.destroy();
}
this.template = new Template(document.body, this.store, Utils.l10n);
this.template.start();
@ -189,3 +208,7 @@ let UI = {
return deferred.promise;
},
}
// This must be bound immediately, as it might be used via the message listener
// before UI.init() has been called.
UI._onConnectionStatusChange = UI._onConnectionStatusChange.bind(UI);

View File

@ -40,13 +40,25 @@ window.addEventListener("message", function(event) {
}
}, false);
window.addEventListener("unload", function onUnload() {
window.removeEventListener("unload", onUnload);
if (connection) {
connection.off(Connection.Status.CONNECTED, onConnected);
connection.off(Connection.Status.DISCONNECTED, onDisconnected);
}
});
function onNewConnection() {
connection.on(Connection.Status.CONNECTED, () => {
document.querySelector("#content").classList.add("connected");
});
connection.on(Connection.Status.DISCONNECTED, () => {
document.querySelector("#content").classList.remove("connected");
});
connection.on(Connection.Status.CONNECTED, onConnected);
connection.on(Connection.Status.DISCONNECTED, onDisconnected);
}
function onConnected() {
document.querySelector("#content").classList.add("connected");
}
function onDisconnected() {
document.querySelector("#content").classList.remove("connected");
}
function selectTab(id) {

View File

@ -35,7 +35,12 @@ window.addEventListener("message", function(event) {
}
}
} catch(e) {}
}, false);
});
window.addEventListener("unload", function onUnload() {
window.removeEventListener("unload", onUnload);
UI.destroy();
});
let UI = {
isReady: false,
@ -55,8 +60,15 @@ let UI = {
});
},
destroy: function() {
if (this.connection) {
this.connection.off(Connection.Events.STATUS_CHANGED, this._onConnectionStatusChange);
}
this.template.destroy();
},
onNewConnection: function() {
this.connection.on(Connection.Events.STATUS_CHANGED, () => this._onConnectionStatusChange());
this.connection.on(Connection.Events.STATUS_CHANGED, this._onConnectionStatusChange);
this._onConnectionStatusChange();
},
@ -427,4 +439,8 @@ let UI = {
}
};
// This must be bound immediately, as it might be used via the message listener
// before UI.onload() has been called.
UI._onConnectionStatusChange = UI._onConnectionStatusChange.bind(UI);
EventEmitter.decorate(UI);

View File

@ -69,7 +69,8 @@ function Template(root, store, l10nResolver) {
this._root = root;
this._doc = this._root.ownerDocument;
this._store.on("set", (event,path,value) => this._storeChanged(path,value));
this._storeChanged = this._storeChanged.bind(this);
this._store.on("set", this._storeChanged);
}
Template.prototype = {
@ -77,6 +78,12 @@ Template.prototype = {
this._processTree(this._root);
},
destroy: function() {
this._store.off("set", this._storeChanged);
this._root = null;
this._doc = null;
},
_resolvePath: function(path, defaultValue=null) {
// From the store, get the value of an object located
@ -110,7 +117,7 @@ Template.prototype = {
return obj;
},
_storeChanged: function(path, value) {
_storeChanged: function(event, path, value) {
// The store has changed (a "set" event has been emitted).
// We need to invalidate and rebuild the affected elements.

View File

@ -19,19 +19,36 @@ let Utils = (function() {
const EventEmitter = require("devtools/shared/event-emitter");
function _forwardSetEvent(key, store, finalStore) {
store.on("set", function(event, path, value) {
function _createSetEventForwarder(key, finalStore) {
return function(event, path, value) {
finalStore.emit("set", [key].concat(path), value);
});
};
}
function mergeStores(stores) {
let finalStore = {object:{}};
EventEmitter.decorate(finalStore);
let setEventForwarders = {};
for (let key in stores) {
finalStore.object[key] = stores[key].object,
_forwardSetEvent(key, stores[key], finalStore);
let store = stores[key];
finalStore.object[key] = store.object;
setEventForwarders[key] = _createSetEventForwarder(key, finalStore);
store.on("set", setEventForwarders[key]);
}
finalStore.destroy = () => {
for (let key in stores) {
let store = stores[key];
store.off("set", setEventForwarders[key]);
if (store.destroy) {
store.destroy();
}
}
};
return finalStore;
}

View File

@ -25,21 +25,27 @@ module.exports = DeviceStore = function(connection) {
this._resetStore();
this._destroy = this._destroy.bind(this);
this.destroy = this.destroy.bind(this);
this._onStatusChanged = this._onStatusChanged.bind(this);
this._connection = connection;
this._connection.once(Connection.Events.DESTROYED, this._destroy);
this._connection.once(Connection.Events.DESTROYED, this.destroy);
this._connection.on(Connection.Events.STATUS_CHANGED, this._onStatusChanged);
this._onStatusChanged();
return this;
}
DeviceStore.prototype = {
_destroy: function() {
this._connection.off(Connection.Events.STATUS_CHANGED, this._onStatusChanged);
_knownDeviceStores.delete(this._connection);
this._connection = null;
destroy: function() {
if (this._connection) {
// While this.destroy is bound using .once() above, that event may not
// have occurred when the DeviceStore client calls destroy, so we
// manually remove it here.
this._connection.off(Connection.Events.DESTROYED, this.destroy);
this._connection.off(Connection.Events.STATUS_CHANGED, this._onStatusChanged);
_knownDeviceStores.delete(this._connection);
this._connection = null;
}
},
_resetStore: function() {

View File

@ -101,7 +101,9 @@ function waitForProjectsPanel(deferred = promise.defer()) {
let projectsWindow = getProjectsWindow();
let projectsUI = projectsWindow.UI;
if (!projectsUI) {
info("projectsUI false");
projectsWindow.addEventListener("load", function onLoad() {
info("got load event");
projectsWindow.removeEventListener("load", onLoad);
waitForProjectsPanel(deferred);
});
@ -109,10 +111,12 @@ function waitForProjectsPanel(deferred = promise.defer()) {
}
if (projectsUI.isReady) {
info("projectsUI ready");
deferred.resolve();
return deferred.promise;
}
info("projectsUI not ready");
projectsUI.once("ready", deferred.resolve);
return deferred.promise;
}

View File

@ -25,21 +25,27 @@ module.exports = WebappsStore = function(connection) {
this._resetStore();
this._destroy = this._destroy.bind(this);
this.destroy = this.destroy.bind(this);
this._onStatusChanged = this._onStatusChanged.bind(this);
this._connection = connection;
this._connection.once(Connection.Events.DESTROYED, this._destroy);
this._connection.once(Connection.Events.DESTROYED, this.destroy);
this._connection.on(Connection.Events.STATUS_CHANGED, this._onStatusChanged);
this._onStatusChanged();
return this;
}
WebappsStore.prototype = {
_destroy: function() {
this._connection.off(Connection.Events.STATUS_CHANGED, this._onStatusChanged);
_knownWebappsStores.delete(this._connection);
this._connection = null;
destroy: function() {
if (this._connection) {
// While this.destroy is bound using .once() above, that event may not
// have occurred when the WebappsStore client calls destroy, so we
// manually remove it here.
this._connection.off(Connection.Events.DESTROYED, this.destroy);
this._connection.off(Connection.Events.STATUS_CHANGED, this._onStatusChanged);
_knownWebappsStores.delete(this._connection);
this._connection = null;
}
},
_resetStore: function() {