diff options
Diffstat (limited to 'devtools/shared/commands/object')
-rw-r--r-- | devtools/shared/commands/object/moz.build | 10 | ||||
-rw-r--r-- | devtools/shared/commands/object/object-command.js | 63 | ||||
-rw-r--r-- | devtools/shared/commands/object/tests/browser.toml | 9 | ||||
-rw-r--r-- | devtools/shared/commands/object/tests/browser_object.js | 125 | ||||
-rw-r--r-- | devtools/shared/commands/object/tests/head.js | 12 |
5 files changed, 219 insertions, 0 deletions
diff --git a/devtools/shared/commands/object/moz.build b/devtools/shared/commands/object/moz.build new file mode 100644 index 0000000000..151750907c --- /dev/null +++ b/devtools/shared/commands/object/moz.build @@ -0,0 +1,10 @@ +# 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/. + +DevToolsModules( + "object-command.js", +) + +if CONFIG["MOZ_BUILD_APP"] != "mobile/android": + BROWSER_CHROME_MANIFESTS += ["tests/browser.toml"] diff --git a/devtools/shared/commands/object/object-command.js b/devtools/shared/commands/object/object-command.js new file mode 100644 index 0000000000..0396b6167a --- /dev/null +++ b/devtools/shared/commands/object/object-command.js @@ -0,0 +1,63 @@ +/* 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"; + +/** + * The ObjectCommand helps inspecting and managing lifecycle + * of all inspected JavaScript objects. + */ +class ObjectCommand { + constructor({ commands, descriptorFront, watcherFront }) { + this.#commands = commands; + } + #commands = null; + + /** + * Release a set of object actors all at once. + * + * @param {Array<ObjectFront>} frontsToRelease + * List of fronts for the object to release. + */ + async releaseObjects(frontsToRelease) { + // @backward-compat { version 123 } A new Objects Manager front has a new "releaseActors" method. + // Only supportsReleaseActors=true codepath can be kept once 123 is the release channel. + const { supportsReleaseActors } = this.#commands.client.mainRoot.traits; + + // First group all object fronts per target + const actorsPerTarget = new Map(); + const promises = []; + for (const frontToRelease of frontsToRelease) { + const { targetFront } = frontToRelease; + // If the front is already destroyed, its target front will be nullified. + if (!targetFront) { + continue; + } + + let actorIDsToRemove = actorsPerTarget.get(targetFront); + if (!actorIDsToRemove) { + actorIDsToRemove = []; + actorsPerTarget.set(targetFront, actorIDsToRemove); + } + if (supportsReleaseActors) { + actorIDsToRemove.push(frontToRelease.actorID); + frontToRelease.destroy(); + } else { + promises.push(frontToRelease.release()); + } + } + + if (supportsReleaseActors) { + // Then release all fronts by bulk per target + for (const [targetFront, actorIDs] of actorsPerTarget) { + const objectsManagerFront = await targetFront.getFront("objects-manager"); + promises.push(objectsManagerFront.releaseObjects(actorIDs)); + } + } + + await Promise.all(promises); + } +} + +module.exports = ObjectCommand; diff --git a/devtools/shared/commands/object/tests/browser.toml b/devtools/shared/commands/object/tests/browser.toml new file mode 100644 index 0000000000..4f1dbe830e --- /dev/null +++ b/devtools/shared/commands/object/tests/browser.toml @@ -0,0 +1,9 @@ +[DEFAULT] +tags = "devtools" +subsuite = "devtools" +support-files = [ + "!/devtools/client/shared/test/shared-head.js", + "head.js", +] + +["browser_object.js"] diff --git a/devtools/shared/commands/object/tests/browser_object.js b/devtools/shared/commands/object/tests/browser_object.js new file mode 100644 index 0000000000..9f6d5132d3 --- /dev/null +++ b/devtools/shared/commands/object/tests/browser_object.js @@ -0,0 +1,125 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the ObjectCommand + +add_task(async function testObjectRelease() { + const tab = await addTab("data:text/html;charset=utf-8,Test page<script>var foo = { bar: 42 };</script>"); + + const commands = await CommandsFactory.forTab(tab); + await commands.targetCommand.startListening(); + + const { objectCommand } = commands; + + const evaluationResponse = await commands.scriptCommand.execute( + "window.foo" + ); + + // Execute a second time so that the WebConsoleActor set this._lastConsoleInputEvaluation to another value + // and so we prevent freeing `window.foo` + await commands.scriptCommand.execute(""); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () { + is(content.wrappedJSObject.foo.bar, 42); + const weakRef = Cu.getWeakReference(content.wrappedJSObject.foo); + + // Hold off the weak reference on SpecialPowsers so that it can be accessed in the next SpecialPowers.spawn + SpecialPowers.weakRef = weakRef; + + // Nullify this variable so that it should be freed + // unless the DevTools inspection still hold it in memory + content.wrappedJSObject.foo = null; + + Cu.forceGC(); + Cu.forceCC(); + + ok(SpecialPowers.weakRef.get(), "The 'foo' object can't be freed because of DevTools keeping a reference on it"); + }); + + info("Release the server side actors which are keeping the object in memory"); + const objectFront = evaluationResponse.result; + await commands.objectCommand.releaseObjects([objectFront]); + + ok(objectFront.isDestroyed(), "The passed object front has been destroyed"); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () { + await ContentTaskUtils.waitForCondition(() => { + Cu.forceGC(); + Cu.forceCC(); + return !SpecialPowers.weakRef.get(); + }, "Wait for JS object to be freed", 500); + + ok(!SpecialPowers.weakRef.get(), "The 'foo' object has been freed"); + }); + + await commands.destroy(); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function testMultiTargetObjectRelease() { + // This test fails with EFT disabled + if (!isEveryFrameTargetEnabled()) { + return; + } + + const tab = await addTab(`data:text/html;charset=utf-8,Test page<iframe src="data:text/html,bar">/iframe>`); + + const commands = await CommandsFactory.forTab(tab); + await commands.targetCommand.startListening(); + + const [,iframeTarget] = commands.targetCommand.getAllTargets(commands.targetCommand.ALL_TYPES); + is(iframeTarget.url, "data:text/html,bar"); + + const { objectCommand } = commands; + + const evaluationResponse1 = await commands.scriptCommand.execute( + "window" + ); + const evaluationResponse2 = await commands.scriptCommand.execute( + "window", { + selectedTargetFront: iframeTarget, + } + ); + const object1 = evaluationResponse1.result; + const object2 = evaluationResponse2.result; + isnot(object1, object2, "The two window object fronts are different"); + isnot(object1.targetFront, object2.targetFront, "The two window object fronts relates to two distinct targets"); + is(object2.targetFront, iframeTarget, "The second object relates to the iframe target"); + + await commands.objectCommand.releaseObjects([object1, object2]); + ok(object1.isDestroyed(), "The first object front is destroyed"); + ok(object2.isDestroyed(), "The second object front is destroyed"); + + await commands.destroy(); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function testWorkerObjectRelease() { + const workerUrl = `data:text/javascript,const foo = {}`; + const tab = await addTab(`data:text/html;charset=utf-8,Test page<script>const worker = new Worker("${workerUrl}")</script>`); + + const commands = await CommandsFactory.forTab(tab); + commands.targetCommand.listenForWorkers = true; + await commands.targetCommand.startListening(); + + const [,workerTarget] = commands.targetCommand.getAllTargets(commands.targetCommand.ALL_TYPES); + is(workerTarget.url, workerUrl); + + const { objectCommand } = commands; + + const evaluationResponse = await commands.scriptCommand.execute( + "foo", { + selectedTargetFront: workerTarget, + } + ); + const object = evaluationResponse.result; + is(object.targetFront, workerTarget, "The 'foo' object relates to the worker target"); + + await commands.objectCommand.releaseObjects([object]); + ok(object.isDestroyed(), "The object front is destroyed"); + + await commands.destroy(); + BrowserTestUtils.removeTab(tab); +}); diff --git a/devtools/shared/commands/object/tests/head.js b/devtools/shared/commands/object/tests/head.js new file mode 100644 index 0000000000..ce65b3d827 --- /dev/null +++ b/devtools/shared/commands/object/tests/head.js @@ -0,0 +1,12 @@ +/* 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"; + +/* eslint no-unused-vars: [2, {"vars": "local"}] */ + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js", + this +); |