diff --git a/browser/devtools/debugger/test/browser.ini b/browser/devtools/debugger/test/browser.ini index 0fa4d0beaeda..467e6f47d64f 100644 --- a/browser/devtools/debugger/test/browser.ini +++ b/browser/devtools/debugger/test/browser.ini @@ -43,6 +43,8 @@ support-files = code_ugly-7.js code_ugly-8 code_ugly-8^headers^ + code_WorkerActor.attach-worker1.js + code_WorkerActor.attach-worker2.js doc_auto-pretty-print-01.html doc_auto-pretty-print-02.html doc_binary_search.html @@ -103,6 +105,8 @@ support-files = doc_watch-expressions.html doc_watch-expression-button.html doc_with-frame.html + doc_WorkerActor.attach-tab1.html + doc_WorkerActor.attach-tab2.html head.js sjs_random-javascript.sjs testactors.js @@ -556,3 +560,5 @@ skip-if = e10s && debug skip-if = e10s && debug [browser_dbg_watch-expressions-02.js] skip-if = e10s && debug +[browser_dbg_WorkerActor.attach.js] +skip-if = e10s && debug diff --git a/browser/devtools/debugger/test/browser_dbg_WorkerActor.attach.js b/browser/devtools/debugger/test/browser_dbg_WorkerActor.attach.js new file mode 100644 index 000000000000..fe0d839518c5 --- /dev/null +++ b/browser/devtools/debugger/test/browser_dbg_WorkerActor.attach.js @@ -0,0 +1,63 @@ +let MAX_TOTAL_VIEWERS = "browser.sessionhistory.max_total_viewers"; + +let TAB1_URL = EXAMPLE_URL + "doc_WorkerActor.attach-tab1.html"; +let TAB2_URL = EXAMPLE_URL + "doc_WorkerActor.attach-tab2.html"; +let WORKER1_URL = "code_WorkerActor.attach-worker1.js"; +let WORKER2_URL = "code_WorkerActor.attach-worker2.js"; + +function test() { + Task.spawn(function* () { + let oldMaxTotalViewers = SpecialPowers.getIntPref(MAX_TOTAL_VIEWERS); + SpecialPowers.setIntPref(MAX_TOTAL_VIEWERS, 10); + + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + + let client = new DebuggerClient(DebuggerServer.connectPipe()); + yield connect(client); + + let tab = yield addTab(TAB1_URL); + let { tabs } = yield listTabs(client); + let [, tabClient] = yield attachTab(client, findTab(tabs, TAB1_URL)); + yield listWorkers(tabClient); + + // If a page still has pending network requests, it will not be moved into + // the bfcache. Consequently, we cannot use waitForWorkerListChanged here, + // because the worker is not guaranteed to have finished loading when it is + // registered. Instead, we have to wait for the promise returned by + // createWorker in the tab to be resolved. + yield createWorkerInTab(tab, WORKER1_URL); + + let { workers } = yield listWorkers(tabClient); + let [, workerClient1] = yield attachWorker(tabClient, + findWorker(workers, WORKER1_URL)); + is(workerClient1.isFrozen, false); + + executeSoon(() => { + tab.linkedBrowser.loadURI(TAB2_URL); + }); + yield waitForWorkerFreeze(workerClient1); + is(workerClient1.isFrozen, true, "worker should be frozen"); + + yield createWorkerInTab(tab, WORKER2_URL); + ({ workers } = yield listWorkers(tabClient)); + let [, workerClient2] = yield attachWorker(tabClient, + findWorker(workers, WORKER2_URL)); + is(workerClient2.isFrozen, false); + + executeSoon(() => { + tab.linkedBrowser.contentWindow.history.back(); + }); + yield Promise.all([ + waitForWorkerFreeze(workerClient2), + waitForWorkerThaw(workerClient1) + ]); + + terminateWorkerInTab(tab, WORKER1_URL); + yield waitForWorkerClose(workerClient1); + + yield close(client); + SpecialPowers.setIntPref(MAX_TOTAL_VIEWERS, oldMaxTotalViewers); + finish(); + }); +} diff --git a/browser/devtools/debugger/test/code_WorkerActor.attach-worker1.js b/browser/devtools/debugger/test/code_WorkerActor.attach-worker1.js new file mode 100644 index 000000000000..18f14864d6cf --- /dev/null +++ b/browser/devtools/debugger/test/code_WorkerActor.attach-worker1.js @@ -0,0 +1,5 @@ +"use strict"; + +self.onmessage = function () {}; + +postMessage("load"); diff --git a/browser/devtools/debugger/test/code_WorkerActor.attach-worker2.js b/browser/devtools/debugger/test/code_WorkerActor.attach-worker2.js new file mode 100644 index 000000000000..18f14864d6cf --- /dev/null +++ b/browser/devtools/debugger/test/code_WorkerActor.attach-worker2.js @@ -0,0 +1,5 @@ +"use strict"; + +self.onmessage = function () {}; + +postMessage("load"); diff --git a/browser/devtools/debugger/test/code_frame-script.js b/browser/devtools/debugger/test/code_frame-script.js index c42803b10dfd..0fdb4ac674fe 100644 --- a/browser/devtools/debugger/test/code_frame-script.js +++ b/browser/devtools/debugger/test/code_frame-script.js @@ -13,8 +13,14 @@ addMessageListener("test:call", function (message) { dump("Calling function with name " + message.data.name + ".\n"); let data = message.data; - XPCNativeWrapper.unwrap(content)[data.name].apply(undefined, data.args); - sendAsyncMessage("test:call"); + let result = XPCNativeWrapper.unwrap(content)[data.name].apply(undefined, data.args); + if (result && result.then) { + result.then(() => { + sendAsyncMessage("test:call"); + }); + } else { + sendAsyncMessage("test:call"); + } }); addMessageListener("test:click", function (message) { @@ -28,6 +34,34 @@ addMessageListener("test:click", function (message) { addMessageListener("test:eval", function (message) { dump("Evalling string " + message.data.string + ".\n"); - content.eval(message.data.string); - sendAsyncMessage("test:eval"); + let result = content.eval(message.data.string); + if (result.then) { + result.then(() => { + sendAsyncMessage("test:eval"); + }); + } else { + sendAsyncMessage("test:eval"); + } +}); + +let workers = {} + +addMessageListener("test:createWorker", function (message) { + dump("Creating worker with url '" + message.data.url + "'.\n"); + + let url = message.data.url; + let worker = new content.Worker(message.data.url); + worker.addEventListener("message", function listener() { + worker.removeEventListener("message", listener); + sendAsyncMessage("test:createWorker"); + }); + workers[url] = worker; +}); + +addMessageListener("test:terminateWorker", function (message) { + dump("Terminating worker with url '" + message.data.url + "'.\n"); + + let url = message.data.url; + workers[url].terminate(); + delete workers[url]; }); diff --git a/browser/devtools/debugger/test/code_workeractor-worker.js b/browser/devtools/debugger/test/code_workeractor-worker.js new file mode 100644 index 000000000000..18f14864d6cf --- /dev/null +++ b/browser/devtools/debugger/test/code_workeractor-worker.js @@ -0,0 +1,5 @@ +"use strict"; + +self.onmessage = function () {}; + +postMessage("load"); diff --git a/browser/devtools/debugger/test/doc_WorkerActor.attach-tab1.html b/browser/devtools/debugger/test/doc_WorkerActor.attach-tab1.html new file mode 100644 index 000000000000..62ab9be7d2e8 --- /dev/null +++ b/browser/devtools/debugger/test/doc_WorkerActor.attach-tab1.html @@ -0,0 +1,8 @@ + + +
+ + + + + diff --git a/browser/devtools/debugger/test/doc_WorkerActor.attach-tab2.html b/browser/devtools/debugger/test/doc_WorkerActor.attach-tab2.html new file mode 100644 index 000000000000..62ab9be7d2e8 --- /dev/null +++ b/browser/devtools/debugger/test/doc_WorkerActor.attach-tab2.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/browser/devtools/debugger/test/head.js b/browser/devtools/debugger/test/head.js index 643d349414cb..ecee083d2193 100644 --- a/browser/devtools/debugger/test/head.js +++ b/browser/devtools/debugger/test/head.js @@ -984,7 +984,7 @@ function callInTab(tab, name) { name: name, args: Array.prototype.slice.call(arguments, 2) }); - waitForMessageFromTab(tab, "test:call"); + return waitForMessageFromTab(tab, "test:call"); } function evalInTab(tab, string) { @@ -993,7 +993,7 @@ function evalInTab(tab, string) { sendMessageToTab(tab, "test:eval", { string: string, }); - waitForMessageFromTab(tab, "test:eval"); + return waitForMessageFromTab(tab, "test:eval"); } function sendMouseClickToTab(tab, target) { @@ -1026,6 +1026,24 @@ function getSourceForm(aSources, aURL) { return item.attachment.source; } +function createWorkerInTab(tab, url) { + info("Creating worker with url '" + url + "' in tab."); + + sendMessageToTab(tab, "test:createWorker", { + url: url + }); + return waitForMessageFromTab(tab, "test:createWorker"); +} + +function terminateWorkerInTab(tab, url) { + info("Terminating worker with url '" + url + "' in tab."); + + sendMessageToTab(tab, "test:terminateWorker", { + url: url + }); + return waitForMessageFromTab(tab, "test:terminateWorker"); +} + function connect(client) { info("Connecting client."); return new Promise(function (resolve) { @@ -1081,6 +1099,25 @@ function listWorkers(tabClient) { }); } +function findWorker(workers, url) { + info("Finding worker with url '" + url + "'."); + for (let worker of workers) { + if (worker.url === url) { + return worker; + } + } + return null; +} + +function attachWorker(tabClient, worker) { + info("Attaching to worker with url '" + worker.url + "'."); + return new Promise(function(resolve, reject) { + tabClient.attachWorker(worker.actor, function (response, workerClient) { + resolve([response, workerClient]); + }); + }); +} + function waitForWorkerListChanged(tabClient) { info("Waiting for worker list to change."); return new Promise(function (resolve) { @@ -1090,3 +1127,31 @@ function waitForWorkerListChanged(tabClient) { }); }); } + +function waitForWorkerClose(workerClient) { + info("Waiting for worker to close."); + return new Promise(function (resolve) { + workerClient.addOneTimeListener("close", function () { + info("Worker did close."); + resolve(); + }); + }); +} + +function waitForWorkerFreeze(workerClient) { + info("Waiting for worker to freeze."); + return new Promise(function (resolve) { + workerClient.addOneTimeListener("freeze", function () { + resolve(); + }); + }); +} + +function waitForWorkerThaw(workerClient) { + info("Waiting for worker to thaw."); + return new Promise(function (resolve) { + workerClient.addOneTimeListener("thaw", function () { + resolve(); + }); + }); +} diff --git a/toolkit/components/telemetry/Histograms.json b/toolkit/components/telemetry/Histograms.json index a0d7415142b9..62a8dacb4b8a 100644 --- a/toolkit/components/telemetry/Histograms.json +++ b/toolkit/components/telemetry/Histograms.json @@ -5541,6 +5541,20 @@ "n_buckets": "1000", "description": "The time (in milliseconds) that it took a 'detach' request to go round trip." }, + "DEVTOOLS_DEBUGGER_RDP_LOCAL_WORKERDETACH_MS": { + "expires_in_version": "never", + "kind": "exponential", + "high": "10000", + "n_buckets": "1000", + "description": "The time (in milliseconds) that it took a 'detach' request to go round trip." + }, + "DEVTOOLS_DEBUGGER_RDP_REMOTE_WORKERDETACH_MS": { + "expires_in_version": "never", + "kind": "exponential", + "high": "10000", + "n_buckets": "1000", + "description": "The time (in milliseconds) that it took a 'detach' request to go round trip." + }, "DEVTOOLS_DEBUGGER_DISPLAY_SOURCE_LOCAL_MS": { "expires_in_version": "never", "kind": "exponential", diff --git a/toolkit/devtools/client/dbg-client.jsm b/toolkit/devtools/client/dbg-client.jsm index 4d6d6b404812..43ebf59d3b74 100644 --- a/toolkit/devtools/client/dbg-client.jsm +++ b/toolkit/devtools/client/dbg-client.jsm @@ -51,6 +51,7 @@ Object.defineProperty(this, "WebConsoleClient", { }); Components.utils.import("resource://gre/modules/devtools/DevToolsUtils.jsm"); +this.executeSoon = DevToolsUtils.executeSoon; this.makeInfallible = DevToolsUtils.makeInfallible; this.values = DevToolsUtils.values; @@ -505,6 +506,29 @@ DebuggerClient.prototype = { }); }, + attachWorker: function DC_attachWorker(aWorkerActor, aOnResponse = noop) { + let workerClient = this._clients.get(aWorkerActor); + if (workerClient !== undefined) { + executeSoon(() => aOnResponse({ + from: workerClient.actor, + type: "attached", + isFrozen: workerClient.isFrozen + }, workerClient)); + return; + } + + this.request({ to: aWorkerActor, type: "attach" }, (aResponse) => { + if (aResponse.error) { + aOnResponse(aResponse, null); + return; + } + + let workerClient = new WorkerClient(this, aResponse); + this.registerClient(workerClient); + aOnResponse(aResponse, workerClient); + }); + }, + /** * Attach to an addon actor. * @@ -1326,11 +1350,82 @@ TabClient.prototype = { type: "listWorkers" }, { telemetry: "LISTWORKERS" - }) + }), + + attachWorker: function (aWorkerActor, aOnResponse) { + this.client.attachWorker(aWorkerActor, aOnResponse); + } }; eventSource(TabClient.prototype); +function WorkerClient(aClient, aForm) { + this._client = aClient; + this._actor = aForm.from; + this._isClosed = false; + this._isFrozen = aForm.isFrozen; + + this._onClose = this._onClose.bind(this); + this._onFreeze = this._onFreeze.bind(this); + this._onThaw = this._onThaw.bind(this); + + this.addListener("close", this._onClose); + this.addListener("freeze", this._onFreeze); + this.addListener("thaw", this._onThaw); +} + +WorkerClient.prototype = { + get _transport() { + return this._client._transport; + }, + + get request() { + return this._client.request; + }, + + get actor() { + return this._actor; + }, + + get isClosed() { + return this._isClosed; + }, + + get isFrozen() { + return this._isFrozen; + }, + + detach: DebuggerClient.requester({ type: "detach" }, { + after: function (aResponse) { + this._client.unregisterClient(this); + return aResponse; + }, + + telemetry: "WORKERDETACH" + }), + + _onClose: function () { + this.removeListener("close", this._onClose); + this.removeListener("freeze", this._onFreeze); + this.removeListener("thaw", this._onThaw); + + this._client.unregisterClient(this); + this._closed = true; + }, + + _onFreeze: function () { + this._isFrozen = true; + }, + + _onThaw: function () { + this._isFrozen = false; + }, + + events: ["close", "freeze", "thaw"] +}; + +eventSource(WorkerClient.prototype); + function AddonClient(aClient, aActor) { this._client = aClient; this._actor = aActor; diff --git a/toolkit/devtools/server/actors/worker.js b/toolkit/devtools/server/actors/worker.js index eb5f990e12f5..8f87fc79c721 100644 --- a/toolkit/devtools/server/actors/worker.js +++ b/toolkit/devtools/server/actors/worker.js @@ -27,6 +27,7 @@ function matchWorkerDebugger(dbg, options) { function WorkerActor(dbg) { this._dbg = dbg; + this._isAttached = false; } WorkerActor.prototype = { @@ -37,9 +38,61 @@ WorkerActor.prototype = { actor: this.actorID, url: this._dbg.url }; + }, + + onAttach: function () { + if (this._dbg.isClosed) { + return { error: "closed" }; + } + + if (!this._isAttached) { + this._dbg.addListener(this); + this._isAttached = true; + } + + return { + type: "attached", + isFrozen: this._dbg.isFrozen + }; + }, + + onDetach: function () { + if (!this._isAttached) { + return { error: "wrongState" }; + } + + this._detach(); + + return { type: "detached" }; + }, + + onClose: function () { + if (this._isAttached) { + this._detach(); + } + + this.conn.sendActorEvent(this.actorID, "close"); + }, + + onFreeze: function () { + this.conn.sendActorEvent(this.actorID, "freeze"); + }, + + onThaw: function () { + this.conn.sendActorEvent(this.actorID, "thaw"); + }, + + _detach: function () { + this._dbg.removeListener(this); + this._isAttached = false; } }; +WorkerActor.prototype.requestTypes = { + "attach": WorkerActor.prototype.onAttach, + "detach": WorkerActor.prototype.onDetach +}; + exports.WorkerActor = WorkerActor; function WorkerActorList(options) {