summaryrefslogtreecommitdiffstats
path: root/devtools/shared/commands/object
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/commands/object')
-rw-r--r--devtools/shared/commands/object/moz.build10
-rw-r--r--devtools/shared/commands/object/object-command.js63
-rw-r--r--devtools/shared/commands/object/tests/browser.toml9
-rw-r--r--devtools/shared/commands/object/tests/browser_object.js125
-rw-r--r--devtools/shared/commands/object/tests/head.js12
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
+);