Bug 1596939 Part 3 - Pause new service workers for the target's origin, r=jlast.
authorBrian Hackett <bhackett1024@gmail.com>
Sat, 07 Dec 2019 21:05:14 +0000
changeset 568128 12842404e9704042da29d2d86b7c1cec34d864a8
parent 568127 1bcf78aceaf6c4daf27c4e247403e983998d24bb
child 568129 053b0bb00fedc42ea54e7a6b8e32843d7d6b7849
push id12493
push userffxbld-merge
push dateMon, 06 Jan 2020 15:38:57 +0000
treeherdermozilla-beta@63ae456b848d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjlast
bugs1596939
milestone73.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1596939 Part 3 - Pause new service workers for the target's origin, r=jlast. Depends on D54293 Differential Revision: https://phabricator.services.mozilla.com/D54298
devtools/client/application/test/browser/browser_application_panel_list-domain-workers.js
devtools/client/debugger/src/client/firefox/events.js
devtools/client/debugger/src/client/firefox/targets.js
devtools/client/debugger/test/mochitest/browser_dbg-windowless-service-workers.js
devtools/client/debugger/test/mochitest/examples/service-worker.sjs
--- a/devtools/client/application/test/browser/browser_application_panel_list-domain-workers.js
+++ b/devtools/client/application/test/browser/browser_application_panel_list-domain-workers.js
@@ -10,16 +10,24 @@
 
 const SIMPLE_URL = URL_ROOT + "resources/service-workers/simple.html";
 const OTHER_URL = SIMPLE_URL.replace("example.com", "test1.example.com");
 const EMPTY_URL = (URL_ROOT + "resources/service-workers/empty.html").replace(
   "example.com",
   "test2.example.com"
 );
 
+// FIXME bug 1575427 this rejection is very common.
+const { PromiseTestUtils } = ChromeUtils.import(
+  "resource://testing-common/PromiseTestUtils.jsm"
+);
+PromiseTestUtils.whitelistRejectionsGlobally(
+  /this._frontCreationListeners is null/
+);
+
 add_task(async function() {
   await enableApplicationPanel();
 
   const { panel, target } = await openNewTabAndApplicationPanel(SIMPLE_URL);
   const doc = panel.panelWin.document;
 
   selectPage(panel, "service-workers");
 
--- a/devtools/client/debugger/src/client/firefox/events.js
+++ b/devtools/client/debugger/src/client/firefox/events.js
@@ -45,24 +45,22 @@ function attachAllTargets(currentTarget:
 }
 
 function setupEvents(dependencies: Dependencies) {
   const { tabTarget, threadFront, debuggerClient } = dependencies;
   actions = dependencies.actions;
   sourceQueue.initialize(actions);
 
   addThreadEventListeners(threadFront);
-  tabTarget.on("workerListChanged", () => threadListChanged("worker"));
-  debuggerClient.mainRoot.on("processListChanged", () =>
-    threadListChanged("contentProcess")
-  );
+  tabTarget.on("workerListChanged", () => threadListChanged());
+  debuggerClient.mainRoot.on("processListChanged", () => threadListChanged());
 
   if (features.windowlessServiceWorkers || attachAllTargets(tabTarget)) {
     const workersListener = new WorkersListener(debuggerClient.mainRoot);
-    workersListener.addListener(() => threadListChanged("worker"));
+    workersListener.addListener(() => threadListChanged());
   }
 }
 
 async function paused(threadFront: ThreadFront, packet: PausedPacket) {
   // When reloading we might get pauses from threads before they have been
   // added to the store. Ensure the pause won't be processed until we've
   // finished adding the thread.
   await actions.ensureHasThread(threadFront.actor);
@@ -109,18 +107,18 @@ function resumed(threadFront: ThreadFron
 
 function newSource(threadFront: ThreadFront, { source }: SourcePacket) {
   sourceQueue.queue({
     type: "generated",
     data: prepareSourcePayload(threadFront, source),
   });
 }
 
-function threadListChanged(type) {
-  actions.updateThreads(type);
+function threadListChanged() {
+  actions.updateThreads();
 }
 
 function replayFramePositions(
   threadFront: ThreadFront,
   { positions, frame, thread }: Object
 ) {
   actions.setFramePositions(positions, frame, thread);
 }
--- a/devtools/client/debugger/src/client/firefox/targets.js
+++ b/devtools/client/debugger/src/client/firefox/targets.js
@@ -85,67 +85,88 @@ async function listWorkerTargets(args: A
 
     const {
       registrations,
     } = await debuggerClient.mainRoot.listServiceWorkerRegistrations();
     serviceWorkerRegistrations = registrations;
   } else {
     workers = (await currentTarget.listWorkers()).workers;
     if (currentTarget.url && features.windowlessServiceWorkers) {
+      allWorkers = await debuggerClient.mainRoot.listAllWorkerTargets();
       const {
         registrations,
       } = await debuggerClient.mainRoot.listServiceWorkerRegistrations();
       serviceWorkerRegistrations = registrations.filter(front =>
         sameOrigin(front.url, currentTarget.url)
       );
     }
   }
 
   for (const front of serviceWorkerRegistrations) {
-    const { activeWorker, waitingWorker, installingWorker } = front;
+    const {
+      activeWorker,
+      waitingWorker,
+      installingWorker,
+      evaluatingWorker,
+    } = front;
     await maybeMarkServiceWorker(activeWorker, "active");
     await maybeMarkServiceWorker(waitingWorker, "waiting");
     await maybeMarkServiceWorker(installingWorker, "installing");
+    await maybeMarkServiceWorker(evaluatingWorker, "evaluating");
   }
 
   async function maybeMarkServiceWorker(info, status) {
     if (!info) {
       return;
     }
 
-    if (!allWorkers) {
-      allWorkers = await debuggerClient.mainRoot.listAllWorkerTargets();
-    }
     const worker = allWorkers.find(front => front && front.id == info.id);
     if (!worker) {
       return;
     }
 
     worker.debuggerServiceWorkerStatus = status;
     if (!workers.includes(worker)) {
       workers.push(worker);
     }
   }
 
   return workers;
 }
 
-async function listProcessTargets(args: Args) {
-  const { currentTarget, debuggerClient } = args;
-  if (!attachAllTargets(currentTarget)) {
-    return [];
-  }
-
+async function getAllProcessTargets(args) {
+  const { debuggerClient } = args;
   const { processes } = await debuggerClient.mainRoot.listProcesses();
-  const targets = await Promise.all(
+  return Promise.all(
     processes
       .filter(descriptor => !descriptor.isParent)
       .map(descriptor => descriptor.getTarget())
   );
+}
 
-  return targets;
+async function listProcessTargets(args: Args) {
+  const { currentTarget } = args;
+  if (!attachAllTargets(currentTarget)) {
+    if (currentTarget.url && features.windowlessServiceWorkers) {
+      // Service workers associated with our target's origin need to pause until
+      // we attach, regardless of which process they are running in.
+      const origin = new URL(currentTarget.url).origin;
+      const targets = await getAllProcessTargets(args);
+      try {
+        await Promise.all(
+          targets.map(t => t.pauseMatchingServiceWorkers({ origin }))
+        );
+      } catch (e) {
+        // Old servers without pauseMatchingServiceWorkers will throw.
+        // @backward-compatibility: remove in Firefox 75
+      }
+    }
+    return [];
+  }
+
+  return getAllProcessTargets(args);
 }
 
 export async function updateTargets(args: Args) {
   const workers = await listWorkerTargets(args);
   const processes = await listProcessTargets(args);
   await attachTargets([...workers, ...processes], args);
 }
--- a/devtools/client/debugger/test/mochitest/browser_dbg-windowless-service-workers.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg-windowless-service-workers.js
@@ -2,16 +2,24 @@
  * 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/>. */
 
 async function checkWorkerThreads(dbg, count) {
   await waitUntil(() => dbg.selectors.getThreads().length == count);
   ok(true, `Have ${count} threads`);
 }
 
+async function checkWorkerStatus(dbg, status) {
+  await waitUntil(() => {
+    const threads = dbg.selectors.getThreads();
+    return threads.some(t => t.serviceWorkerStatus == status);
+  });
+  ok(true, `Have thread with status ${status}`);
+}
+
 // Test that we can detect a new service worker and hit breakpoints that we've
 // set in it.
 add_task(async function() {
   info("Subtest #1");
 
   await pushPref("devtools.debugger.features.windowless-service-workers", true);
   await pushPref("devtools.debugger.workers-visible", true);
   await pushPref("dom.serviceWorkers.enabled", true);
@@ -26,18 +34,19 @@ add_task(async function() {
 
   await addBreakpoint(dbg, "service-worker.sjs", 13);
 
   invokeInTab("fetchFromWorker");
 
   await waitForPaused(dbg);
   assertPausedAtSourceAndLine(dbg, workerSource.id, 13);
 
-  // Leave the breakpoint in place for the next subtest.
+  // Leave the breakpoint and worker in place for the next subtest.
   await resume(dbg);
+  await waitForRequestsToSettle(dbg);
   await removeTab(gBrowser.selectedTab);
 });
 
 // Test that breakpoints can be immediately hit in service workers when reloading.
 add_task(async function() {
   info("Subtest #2");
 
   const toolbox = await openNewTabAndToolbox(EXAMPLE_URL + "doc-service-workers.html", "jsdebugger");
@@ -56,28 +65,30 @@ add_task(async function() {
   await checkWorkerThreads(dbg, 1);
 
   await resume(dbg);
   await dbg.actions.removeAllBreakpoints(getContext(dbg));
 
   invokeInTab("unregisterWorker");
 
   await checkWorkerThreads(dbg, 0);
+  await waitForRequestsToSettle(dbg);
   await removeTab(gBrowser.selectedTab);
 });
 
 // Test having a waiting and active service worker for the same registration.
 add_task(async function() {
   info("Subtest #3");
 
   const toolbox = await openNewTabAndToolbox(EXAMPLE_URL + "doc-service-workers.html", "jsdebugger");
   const dbg = createDebuggerContext(toolbox);
 
   invokeInTab("registerWorker");
   await checkWorkerThreads(dbg, 1);
+  await checkWorkerStatus(dbg, "active");
 
   const firstTab = gBrowser.selectedTab;
 
   await addTab(EXAMPLE_URL + "service-worker.sjs?setStatus=newServiceWorker");
   await removeTab(gBrowser.selectedTab);
 
   const secondTab = await addTab(EXAMPLE_URL + "doc-service-workers.html");
 
@@ -96,18 +107,60 @@ add_task(async function() {
 
   await selectSource(dbg, sources[1]);
   await waitForLoadedSource(dbg, sources[1]);
   const content1 = findSourceContent(dbg, sources[1]);
 
   ok(content0.value.includes("newServiceWorker") != content1.value.includes("newServiceWorker"),
      "Got two different sources for service worker");
 
+  // Add a breakpoint for the next subtest.
+  await addBreakpoint(dbg, "service-worker.sjs", 2);
+
   invokeInTab("unregisterWorker");
 
   await checkWorkerThreads(dbg, 0);
+  await waitForRequestsToSettle(dbg);
   await removeTab(firstTab);
   await removeTab(secondTab);
 
   // Reset the SJS in case we will be repeating the test.
   await addTab(EXAMPLE_URL + "service-worker.sjs?setStatus=");
   await removeTab(gBrowser.selectedTab);
 });
+
+// Test setting breakpoints while the service worker is starting up.
+add_task(async function() {
+  info("Subtest #4");
+
+  const toolbox = await openNewTabAndToolbox(EXAMPLE_URL + "doc-service-workers.html", "jsdebugger");
+  const dbg = createDebuggerContext(toolbox);
+
+  invokeInTab("registerWorker");
+  await checkWorkerThreads(dbg, 1);
+
+  await waitForSource(dbg, "service-worker.sjs");
+  const workerSource = findSource(dbg, "service-worker.sjs");
+
+  await waitForBreakpointCount(dbg, 1);
+  await waitForPaused(dbg);
+  assertPausedAtSourceAndLine(dbg, workerSource.id, 2);
+  await checkWorkerStatus(dbg, "evaluating");
+
+  await addBreakpoint(dbg, "service-worker.sjs", 19);
+  await resume(dbg);
+  await waitForPaused(dbg);
+  assertPausedAtSourceAndLine(dbg, workerSource.id, 19);
+  await checkWorkerStatus(dbg, "installing");
+
+  await addBreakpoint(dbg, "service-worker.sjs", 5);
+  await resume(dbg);
+  await waitForPaused(dbg);
+  assertPausedAtSourceAndLine(dbg, workerSource.id, 5);
+  await checkWorkerStatus(dbg, "active");
+
+  await resume(dbg);
+  invokeInTab("unregisterWorker");
+
+  await checkWorkerThreads(dbg, 0);
+  await waitForRequestsToSettle(dbg);
+  await removeTab(gBrowser.selectedTab);
+});
--- a/devtools/client/debugger/test/mochitest/examples/service-worker.sjs
+++ b/devtools/client/debugger/test/mochitest/examples/service-worker.sjs
@@ -14,33 +14,25 @@ self.addEventListener("activate", event 
 self.addEventListener("fetch", event => {
   const url = event.request.url;
   dump("Fetch: " + url + "\\n");
   if (url.includes("whatever")) {
     const response = new Response("Service worker response STATUS", { statusText: "OK" });
     event.respondWith(response);
   }
 });
+
+self.addEventListener("install", event => {
+  dump("Install\\n");
+});
 `;
 
 function handleRequest(request, response) {
   response.setHeader("Content-Type", "text/javascript");
 
   const arr = /setStatus=(.*)/.exec(request.queryString);
   if (arr) {
     setState("status", arr[1]);
   }
 
   const status = getState("status");
-
-  let newBody = body.replace("STATUS", status);
-
-  if (status == "stuckInstall") {
-newBody += `
-self.addEventListener("install", event => {
-  dump("Install\\n");
-  event.waitUntil(new Promise(resolve => {}));
-});
-`;
-  }
-
-  response.write(newBody);
+  response.write(body.replace("STATUS", status));
 }