Bug 881120 - Allow clients to specify nodes that shouldn't be released. r=jwalker

This commit is contained in:
Dave Camp 2013-06-11 20:27:23 -07:00
parent 6b355d8fa4
commit 3cc23fe0ec
5 changed files with 477 additions and 94 deletions

View File

@ -209,6 +209,11 @@ let NodeFront = protocol.FrontClass(NodeActor, {
protocol.Front.prototype.initialize.call(this, conn, form, detail, ctx);
},
/**
* Destroy a node front. The node must have been removed from the
* ownership tree before this is called, unless the whole walker front
* is being destroyed.
*/
destroy: function() {
// If an observer was added on this node, shut it down.
if (this.observer) {
@ -216,12 +221,6 @@ let NodeFront = protocol.FrontClass(NodeActor, {
this._observer = null;
}
// Disconnect this item and from the ownership tree and destroy
// all of its children.
this.reparent(null);
for (let child of this.treeChildren()) {
child.destroy();
}
protocol.Front.prototype.destroy.call(this);
},
@ -645,6 +644,11 @@ var WalkerActor = protocol.ActorClass({
// this set.
this._orphaned = new Set();
// The client can tell the walker that it is interested in a node
// even when it is orphaned with the `retainNode` method. This
// list contains orphaned nodes that were so retained.
this._retainedOrphans = new Set();
this.onMutations = this.onMutations.bind(this);
this.onFrameLoad = this.onFrameLoad.bind(this);
this.onFrameUnload = this.onFrameUnload.bind(this);
@ -790,24 +794,76 @@ var WalkerActor = protocol.ActorClass({
return null;
},
/**
* Mark a node as 'retained'.
*
* A retained node is not released when `releaseNode` is called on its
* parent, or when a parent is released with the `cleanup` option to
* `getMutations`.
*
* When a retained node's parent is released, a retained mode is added to
* the walker's "retained orphans" list.
*
* Retained nodes can be deleted by providing the `force` option to
* `releaseNode`. They will also be released when their document
* has been destroyed.
*
* Retaining a node makes no promise about its children; They can
* still be removed by normal means.
*/
retainNode: method(function(node) {
node.retained = true;
}, {
request: { node: Arg(0, "domnode") },
response: {}
}),
/**
* Remove the 'retained' mark from a node. If the node was a
* retained orphan, release it.
*/
unretainNode: method(function(node) {
node.retained = false;
if (this._retainedOrphans.has(node)) {
this._retainedOrphans.delete(node);
this.releaseNode(node);
}
}, {
request: { node: Arg(0, "domnode") },
response: {},
}),
/**
* Release actors for a node and all child nodes.
*/
releaseNode: method(function(node) {
releaseNode: method(function(node, options={}) {
if (node.retained && !options.force) {
this._retainedOrphans.add(node);
return;
}
if (node.retained) {
// Forcing a retained node to go away.
this._retainedOrphans.delete(node);
}
let walker = documentWalker(node.rawNode);
let child = walker.firstChild();
while (child) {
let childActor = this._refMap.get(child);
if (childActor) {
this.releaseNode(childActor);
this.releaseNode(childActor, options);
}
child = walker.nextSibling();
}
node.destroy();
}, {
request: { node: Arg(0, "domnode") }
request: {
node: Arg(0, "domnode"),
force: Option(1)
}
}),
/**
@ -1124,6 +1180,8 @@ var WalkerActor = protocol.ActorClass({
if (options.cleanup) {
for (let node of this._orphaned) {
// Release the orphaned node. Nodes or children that have been
// retained will be moved to this._retainedOrphans.
this.releaseNode(node);
}
this._orphaned = new Set();
@ -1139,6 +1197,22 @@ var WalkerActor = protocol.ActorClass({
}
}),
queueMutation: function(mutation) {
if (!this.actorID) {
// We've been destroyed, don't bother queueing this mutation.
return;
}
// We only send the `new-mutations` notification once, until the client
// fetches mutations with the `getMutations` packet.
let needEvent = this._pendingMutations.length === 0;
this._pendingMutations.push(mutation);
if (needEvent) {
events.emit(this, "new-mutations");
}
},
/**
* Handles mutations from the DOM mutation observer API.
*
@ -1146,10 +1220,6 @@ var WalkerActor = protocol.ActorClass({
* See https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver#MutationRecord
*/
onMutations: function(mutations) {
// We only send the `new-mutations` notification once, until the client
// fetches mutations with the `getMutations` packet.
let needEvent = this._pendingMutations.length === 0;
for (let change of mutations) {
let targetActor = this._refMap.get(change.target);
if (!targetActor) {
@ -1206,10 +1276,7 @@ var WalkerActor = protocol.ActorClass({
mutation.removed = removedActors;
mutation.added = addedActors;
}
this._pendingMutations.push(mutation);
}
if (needEvent) {
events.emit(this, "new-mutations");
this.queueMutation(mutation);
}
},
@ -1219,35 +1286,64 @@ var WalkerActor = protocol.ActorClass({
if (!frameActor) {
return;
}
let needEvent = this._pendingMutations.length === 0;
this._pendingMutations.push({
this.queueMutation({
type: "frameLoad",
target: frameActor.actorID,
added: [],
removed: []
});
},
if (needEvent) {
events.emit(this, "new-mutations");
// Returns true if domNode is in window or a subframe.
_childOfWindow: function(window, domNode) {
let win = nodeDocument(domNode).defaultView;
while (win) {
if (win === window) {
return true;
}
win = win.frameElement;
}
return false;
},
onFrameUnload: function(window) {
// Any retained orphans that belong to this document
// or its children need to be released, and a mutation sent
// to notify of that.
let releasedOrphans = [];
for (let retained of this._retainedOrphans) {
if (Cu.isDeadWrapper(retained.rawNode) ||
this._childOfWindow(window, retained.rawNode)) {
this._retainedOrphans.delete(retained);
releasedOrphans.push(retained.actorID);
this.releaseNode(retained, { force: true });
}
}
if (releasedOrphans.length > 0) {
this.queueMutation({
target: this.rootNode.actorID,
type: "unretained",
nodes: releasedOrphans
});
}
let doc = window.document;
let documentActor = this._refMap.get(doc);
if (!documentActor) {
return;
}
let needEvent = this._pendingMutations.length === 0;
this._pendingMutations.push({
this.queueMutation({
type: "documentUnload",
target: documentActor.actorID
});
this.releaseNode(documentActor);
if (needEvent) {
events.emit(this, "new-mutations");
}
// Need to force a release of this node, because those nodes can't
// be accessed anymore.
this.releaseNode(documentActor, { force: true });
}
});
@ -1261,6 +1357,7 @@ var WalkerFront = exports.WalkerFront = protocol.FrontClass(WalkerActor, {
initialize: function(client, form) {
protocol.Front.prototype.initialize.call(this, client, form);
this._orphaned = new Set();
this._retainedOrphans = new Set();
},
destroy: function() {
@ -1290,11 +1387,51 @@ var WalkerFront = exports.WalkerFront = protocol.FrontClass(WalkerActor, {
return types.getType("domnode").read({ actor: id }, this, "standin");
},
releaseNode: protocol.custom(function(node) {
/**
* See the documentation for WalkerActor.prototype.retainNode for
* information on retained nodes.
*
* From the client's perspective, `retainNode` can fail if the node in
* question is removed from the ownership tree before the `retainNode`
* request reaches the server. This can only happen if the client has
* asked the server to release nodes but hasn't gotten a response
* yet: Either a `releaseNode` request or a `getMutations` with `cleanup`
* set is outstanding.
*
* If either of those requests is outstanding AND releases the retained
* node, this request will fail with noSuchActor, but the ownership tree
* will stay in a consistent state.
*
* Because the protocol guarantees that requests will be processed and
* responses received in the order they were sent, we get the right
* semantics by setting our local retained flag on the node only AFTER
* a SUCCESSFUL retainNode call.
*/
retainNode: protocol.custom(function(node) {
return this._retainNode(node).then(() => {
node.retained = true;
});
}, {
impl: "_retainNode",
}),
unretainNode: protocol.custom(function(node) {
return this._unretainNode(node).then(() => {
node.retained = false;
if (this._retainedOrphans.has(node)) {
this._retainedOrphans.delete(node);
this._releaseFront(node);
}
});
}, {
impl: "_unretainNode"
}),
releaseNode: protocol.custom(function(node, options={}) {
// NodeFront.destroy will destroy children in the ownership tree too,
// mimicking what the server will do here.
let actorID = node.actorID;
node.destroy();
this._releaseFront(node, !!options.force);
return this._releaseNode({ actorID: actorID });
}, {
impl: "_releaseNode"
@ -1308,6 +1445,28 @@ var WalkerFront = exports.WalkerFront = protocol.FrontClass(WalkerActor, {
impl: "_querySelector"
}),
_releaseFront: function(node, force) {
if (node.retained && !force) {
node.reparent(null);
this._retainedOrphans.add(node);
return;
}
if (node.retained) {
// Forcing a removal.
this._retainedOrphans.delete(node);
}
// Release any children
for (let child of node.treeChildren()) {
this._releaseFront(child, force);
}
// All children will have been removed from the node by this point.
node.reparent(null);
node.destroy();
},
/**
* Get any unprocessed mutation records and process them.
*/
@ -1374,7 +1533,17 @@ var WalkerFront = exports.WalkerFront = protocol.FrontClass(WalkerActor, {
// We try to give fronts instead of actorIDs, but these fronts need
// to be destroyed now.
emittedMutation.target = targetFront.actorID;
targetFront.destroy();
// Release the document node and all of its children, even retained.
this._releaseFront(targetFront, true);
} else if (change.type === "unretained") {
// Retained orphans were force-released without the intervention of
// client (probably a navigated frame).
for (let released of change.nodes) {
let releasedFront = this.get(released);
this._retainedOrphans.delete(released);
this._releaseFront(releasedFront, true);
}
} else {
targetFront.updateMutation(change);
}
@ -1384,7 +1553,8 @@ var WalkerFront = exports.WalkerFront = protocol.FrontClass(WalkerActor, {
if (options.cleanup) {
for (let node of this._orphaned) {
node.destroy();
// This will move retained nodes to this._retainedOrphans.
this._releaseFront(node);
}
this._orphaned = new Set();
}

View File

@ -19,6 +19,7 @@ MOCHITEST_CHROME_FILES = \
test_inspector-mutations-frameload.html \
test_inspector-mutations-value.html \
test_inspector-release.html \
test_inspector-retain.html \
test_inspector-traversal.html \
test_unsafeDereference.html \
nonchrome_unsafeDereference.html \

View File

@ -110,7 +110,8 @@ function serverOwnershipTree(walker) {
return {
root: serverOwnershipSubtree(serverWalker, serverWalker.rootDoc ),
orphaned: [serverOwnershipSubtree(serverWalker, o.rawNode) for (o of serverWalker._orphaned)]
orphaned: [serverOwnershipSubtree(serverWalker, o.rawNode) for (o of serverWalker._orphaned)],
retained: [serverOwnershipSubtree(serverWalker, o.rawNode) for (o of serverWalker._retainedOrphans)]
};
}
@ -124,7 +125,8 @@ function clientOwnershipSubtree(node) {
function clientOwnershipTree(walker) {
return {
root: clientOwnershipSubtree(walker.rootNode),
orphaned: [clientOwnershipSubtree(o) for (o of walker._orphaned)]
orphaned: [clientOwnershipSubtree(o) for (o of walker._orphaned)],
retained: [clientOwnershipSubtree(o) for (o of walker._retainedOrphans)]
}
}
@ -161,6 +163,23 @@ function checkMissing(client, actorID) {
return deferred.promise;
}
// Verify that an actorID is accessible both from the client library and the server.
function checkAvailable(client, actorID) {
let deferred = Promise.defer();
let front = client.getActor(actorID);
ok(front, "Front should be accessible from the client for actorID: " + actorID);
let deferred = Promise.defer();
client.request({
to: actorID,
type: "garbageAvailableTest",
}, response => {
is(response.error, "unrecognizedPacketType", "node list actor should be contactable.");
deferred.resolve(undefined);
});
return deferred.promise;
}
function promiseDone(promise) {
promise.then(null, err => {
ok(false, "Promise failed: " + err);
@ -171,6 +190,77 @@ function promiseDone(promise) {
});
}
// Mutation list testing
function isSrcChange(change) {
return (change.type === "attributes" && change.attributeName === "src");
}
function assertAndStrip(mutations, message, test) {
let size = mutations.length;
mutations = mutations.filter(test);
ok((mutations.size != size), message);
return mutations;
}
function isSrcChange(change) {
return change.type === "attributes" && change.attributeName === "src";
}
function isUnload(change) {
return change.type === "documentUnload";
}
function isFrameLoad(change) {
return change.type === "frameLoad";
}
function isUnretained(change) {
return change.type === "unretained";
}
function isChildList(change) {
return change.type === "childList";
}
// Make sure an iframe's src attribute changed and then
// strip that mutation out of the list.
function assertSrcChange(mutations) {
return assertAndStrip(mutations, "Should have had an iframe source change.", isSrcChange);
}
// Make sure there's an unload in the mutation list and strip
// that mutation out of the list
function assertUnload(mutations) {
return assertAndStrip(mutations, "Should have had a document unload change.", isUnload);
}
// Make sure there's a frame load in the mutation list and strip
// that mutation out of the list
function assertFrameLoad(mutations) {
return assertAndStrip(mutations, "Should have had a frame load change.", isFrameLoad);
}
// Load mutations aren't predictable, so keep accumulating mutations until
// the one we're looking for shows up.
function waitForMutation(walker, test, mutations=[]) {
let deferred = Promise.defer();
for (let change of mutations) {
if (test(change)) {
deferred.resolve(mutations);
}
}
walker.once("mutations", newMutations => {
waitForMutation(walker, test, mutations.concat(newMutations)).then(finalMutations => {
deferred.resolve(finalMutations);
})
});
return deferred.promise;
}
var _tests = [];
function addTest(test) {
_tests.push(test);

View File

@ -67,67 +67,6 @@ function loadChildSelector(selector) {
});
}
function isSrcChange(change) {
return (change.type === "attributes" && change.attributeName === "src");
}
function assertAndStrip(mutations, message, test) {
let size = mutations.length;
mutations = mutations.filter(test);
ok((mutations.size != size), message);
return mutations;
}
function isSrcChange(change) {
return change.type === "attributes" && change.attributeName === "src";
}
function isUnload(change) {
return change.type === "documentUnload";
}
function isFrameLoad(change) {
return change.type === "frameLoad";
}
// Make sure an iframe's src attribute changed and then
// strip that mutation out of the list.
function assertSrcChange(mutations) {
return assertAndStrip(mutations, "Should have had an iframe source change.", isSrcChange);
}
// Make sure there's an unload in the mutation list and strip
// that mutation out of the list
function assertUnload(mutations) {
return assertAndStrip(mutations, "Should have had a document unload change.", isUnload);
}
// Make sure there's a frame load in the mutation list and strip
// that mutation out of the list
function assertFrameLoad(mutations) {
return assertAndStrip(mutations, "Should have had a frame load change.", isFrameLoad);
}
// Load mutations aren't predictable, so keep accumulating mutations until
// the one we're looking for shows up.
function waitForMutation(walker, test, mutations=[]) {
let deferred = Promise.defer();
for (let change of mutations) {
if (test(change)) {
deferred.resolve(mutations);
}
}
walker.once("mutations", newMutations => {
waitForMutation(walker, test, mutations.concat(newMutations)).then(finalMutations => {
deferred.resolve(finalMutations);
})
});
return deferred.promise;
}
function getUnloadedDoc(mutations) {
for (let change of mutations) {
if (isUnload(change)) {

View File

@ -0,0 +1,183 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=
-->
<head>
<meta charset="utf-8">
<title>Test for Bug </title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript;version=1.8" src="inspector-helpers.js"></script>
<script type="application/javascript;version=1.8">
Components.utils.import("resource://gre/modules/devtools/Loader.jsm");
const Promise = devtools.require("sdk/core/promise");
const inspector = devtools.require("devtools/server/actors/inspector");
window.onload = function() {
SimpleTest.waitForExplicitFinish();
runNextTest();
}
var gWalker = null;
var gClient = null;
var gInspectee = null;
function assertOwnership() {
return assertOwnershipTrees(gWalker);
}
addTest(function setup() {
let url = document.getElementById("inspectorContent").href;
attachURL(url, function(err, client, tab, doc) {
gInspectee = doc;
let {InspectorFront} = devtools.require("devtools/server/actors/inspector");
let inspector = InspectorFront(client, tab);
promiseDone(inspector.getWalker().then(walker => {
ok(walker, "getWalker() should return an actor.");
gClient = client;
gWalker = walker;
}).then(runNextTest));
});
});
// Retain a node, and a second-order child (in another document, for kicks)
// Release the parent of the top item, which should cause one retained orphan.
// Then unretain the top node, which should retain the orphan.
// Then change the source of the iframe, which should kill that orphan.
addTest(function testRetain() {
let originalOwnershipSize = 0;
let bodyFront = null;
let frameFront = null;
let childListFront = null;
// Get the toplevel body element and retain it.
promiseDone(gWalker.querySelector(gWalker.rootNode, "body").then(front => {
bodyFront = front;
return gWalker.retainNode(bodyFront);
}).then(() => {
// Get an element in the child frame and retain it.
return gWalker.querySelector(gWalker.rootNode, "#childFrame");
}).then(frame => {
frameFront = frame;
return gWalker.children(frame, { maxNodes: 1 }).then(children => {
return children.nodes[0];
});
}).then(childDoc => {
return gWalker.querySelector(childDoc, "#longlist");
}).then(list => {
childListFront = list;
originalOwnershipSize = assertOwnership();
// and rtain it.
return gWalker.retainNode(childListFront);
}).then(() => {
// OK, try releasing the parent of the first retained.
return gWalker.releaseNode(bodyFront.parentNode());
}).then(() => {
let size = assertOwnership();
let clientTree = clientOwnershipTree(gWalker);
// That request should have freed the parent of the first retained
// but moved the rest into the retained orphaned tree.
is(ownershipTreeSize(clientTree.root) + ownershipTreeSize(clientTree.retained[0]) + 1,
originalOwnershipSize,
"Should have only lost one item overall.");
is(gWalker._retainedOrphans.size, 1, "Should have retained one orphan");
ok(gWalker._retainedOrphans.has(bodyFront), "Should have retained the expected node.");
}).then(() => {
// Unretain the body, which should promote the childListFront to a retained orphan.
return gWalker.unretainNode(bodyFront);
}).then(() => {
assertOwnership();
let clientTree = clientOwnershipTree(gWalker);
is(gWalker._retainedOrphans.size, 1, "Should still only have one retained orphan.");
ok(!gWalker._retainedOrphans.has(bodyFront), "Should have dropped the body node.")
ok(gWalker._retainedOrphans.has(childListFront), "Should have retained the child node.")
}).then(() => {
// Change the source of the iframe, which should kill the retained orphan.
gInspectee.querySelector("#childFrame").src = "data:text/html,<html>new child</html>";
return waitForMutation(gWalker, isUnretained);
}).then(mutations => {
assertOwnership();
let clientTree = clientOwnershipTree(gWalker);
is(gWalker._retainedOrphans.size, 0, "Should have no more retained orphans.");
}).then(runNextTest));
});
// Get a hold of a node, remove it from the doc and retain it at the same time.
// We should always win that race (even though the mutation happens before the
// retain request), because we haven't issued `getMutations` yet.
addTest(function testWinRace() {
let front = null;
promiseDone(gWalker.querySelector(gWalker.rootNode, "#a").then(node => {
front = node;
let contentNode = gInspectee.querySelector("#a");
contentNode.parentNode.removeChild(contentNode);
// Now wait for that mutation and retain response to come in.
return Promise.all([
gWalker.retainNode(front),
waitForMutation(gWalker, isChildList)
]);
}).then(() => {
assertOwnership();
let clientTree = clientOwnershipTree(gWalker);
is(gWalker._retainedOrphans.size, 1, "Should have a retained orphan.");
ok(gWalker._retainedOrphans.has(front), "Should have retained our expected node.");
return gWalker.unretainNode(front);
}).then(() => {
// Make sure we're clear for the next test.
assertOwnership();
let clientTree = clientOwnershipTree(gWalker);
is(gWalker._retainedOrphans.size, 0, "Should have no more retained orphans.");
}).then(runNextTest));
});
// Same as above, but issue the request right after the 'new-mutations' event, so that
// we *lose* the race.
addTest(function testLoseRace() {
let front = null;
promiseDone(gWalker.querySelector(gWalker.rootNode, "#z").then(node => {
front = node;
gInspectee.querySelector("#z").parentNode = null;
let contentNode = gInspectee.querySelector("#a");
contentNode.parentNode.removeChild(contentNode);
return promiseOnce(gWalker, "new-mutations");
}).then(() => {
// Verify that we have an outstanding request (no good way to tell that it's a
// getMutations request, but there's nothing else it would be).
is(gWalker._requests.length, 1, "Should have an outstanding request.");
return gWalker.retainNode(front)
}).then(() => { ok(false, "Request should not have succeeded!"); },
(err) => {
ok(err, "noSuchActor", "Should have lost the race.");
let clientTree = clientOwnershipTree(gWalker);
is(gWalker._retainedOrphans.size, 0, "Should have no more retained orphans.");
// Don't re-throw the error.
}).then(runNextTest));
});
addTest(function cleanup() {
delete gWalker;
delete gClient;
runNextTest();
});
</script>
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a>
<a id="inspectorContent" target="_blank" href="inspector-traversal-data.html">Test Document</a>
<p id="display"></p>
<div id="content" style="display: none">
</div>
<pre id="test">
</pre>
</body>
</html>