Merge inbound to mozilla-central. a=merge FIREFOX_NIGHTLY_68_END
authorMihai Alexandru Michis <malexandru@mozilla.com>
Mon, 20 May 2019 12:53:43 +0300
changeset 474459 97dae745c1b3ef2292127ba1c4e90b1345c8f576
parent 474452 17d1c1e26e31551fd7baf9b3ea80f8f2382385a0 (current diff)
parent 474458 72731b10931017b247d79f754145726e52672b71 (diff)
child 474460 eefb2904757e04b1e4b5f3ba7c480442ce01575e
child 474479 acb185a74efcf22bb8491f8eb95cedff393c6184
child 474721 d6e1e3c79dc608b6eafd8d021f77a712d1a475d7
push id36038
push usermalexandru@mozilla.com
push dateMon, 20 May 2019 09:54:18 +0000
treeherdermozilla-central@97dae745c1b3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone68.0a1
first release with
nightly linux32
97dae745c1b3 / 68.0a1 / 20190520095418 / files
nightly linux64
97dae745c1b3 / 68.0a1 / 20190520095418 / files
nightly mac
97dae745c1b3 / 68.0a1 / 20190520095418 / files
nightly win32
97dae745c1b3 / 68.0a1 / 20190520095418 / files
nightly win64
97dae745c1b3 / 68.0a1 / 20190520095418 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge inbound to mozilla-central. a=merge
--- a/devtools/client/debugger/test/mochitest/browser.ini
+++ b/devtools/client/debugger/test/mochitest/browser.ini
@@ -661,16 +661,17 @@ support-files =
   examples/doc-windowless-workers.html
   examples/doc-windowless-workers-early-breakpoint.html
   examples/simple-worker.js
   examples/doc-worker-scopes.html
   examples/scopes-worker.js
   examples/doc-event-handler.html
   examples/doc-eval-throw.html
   examples/doc-sourceURL-breakpoint.html
+  examples/doc-step-in-uninitialized.html
 
 [browser_dbg-asm.js]
 [browser_dbg-audiocontext.js]
 [browser_dbg-async-stepping.js]
 [browser_dbg-sourcemapped-breakpoint-console.js]
 skip-if = (os == "win" && ccov) # Bug 1453549
 [browser_dbg-xhr-breakpoints.js]
 [browser_dbg-xhr-run-to-completion.js]
@@ -774,16 +775,17 @@ skip-if = os == "win" || (verify) # Bug 
 skip-if = os == 'linux' && !asan # bug 1447118
 [browser_dbg-sources.js]
 [browser_dbg-sources-arrow-keys.js]
 [browser_dbg-sources-named-eval.js]
 [browser_dbg-sources-querystring.js]
 skip-if = true
 [browser_dbg-stepping.js]
 skip-if = debug || (verify && (os == 'win')) || (os == "win" && os_version == "6.1")
+[browser_dbg-step-in-uninitialized.js]
 [browser_dbg-tabs.js]
 [browser_dbg-tabs-keyboard.js]
 skip-if = os == "win"
 [browser_dbg-tabs-pretty-print.js]
 [browser_dbg-tabs-without-urls.js]
 [browser_dbg-toggling-tools.js]
 [browser_dbg-react-app.js]
 skip-if = os == "win"
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg-step-in-uninitialized.js
@@ -0,0 +1,38 @@
+/* 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/>. */
+
+// When stepping into a function, 'let' variables should show as uninitialized
+// instead of undefined.
+
+function findNodeValue(dbg, text) {
+  for (let index = 0;; index++) {
+    var elem = findElement(dbg, "scopeNode", index);
+    if (elem && elem.innerText == text) {
+      return findElement(dbg, "scopeValue", index).innerText;
+    }
+  }
+}
+
+add_task(async function test() {
+  const dbg = await initDebugger("doc-step-in-uninitialized.html");
+  invokeInTab("main");
+  await waitForPaused(dbg, "doc-step-in-uninitialized.html");
+
+  await stepOver(dbg);
+  await stepIn(dbg);
+
+  assertDebugLine(dbg, 8);
+  assertPausedLocation(dbg);
+
+  // We step past the 'let x' at the start of the function because it is not
+  // a breakpoint position.
+  ok(findNodeValue(dbg, "x") == "undefined", "x undefined");
+  ok(findNodeValue(dbg, "y") == "(uninitialized)", "y uninitialized");
+
+  await stepOver(dbg);
+
+  assertDebugLine(dbg, 9);
+
+  ok(findNodeValue(dbg, "y") == "3", "y initialized");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/examples/doc-step-in-uninitialized.html
@@ -0,0 +1,11 @@
+<script>
+function main() {
+  debugger;
+  foo();
+}
+function foo() {
+  let x;
+  let y = 3;
+  return 0;
+}
+</script>
--- a/devtools/client/netmonitor/test/browser.ini
+++ b/devtools/client/netmonitor/test/browser.ini
@@ -40,18 +40,20 @@ support-files =
   html_statistics-test-page.html
   html_status-codes-test-page.html
   html_tracking-protection.html
   html_api-calls-test-page.html
   html_copy-as-curl.html
   html_curl-utils.html
   html_open-request-in-tab.html
   html_worker-test-page.html
+  html_websocket-test-page.html
   js_worker-test.js
   js_worker-test2.js
+  js_websocket-worker-test.js
   sjs_content-type-test-server.sjs
   sjs_cors-test-server.sjs
   sjs_https-redirect-test-server.sjs
   sjs_hsts-test-server.sjs
   sjs_json-test-server.sjs
   sjs_method-test-server.sjs
   sjs_set-cookie-same-site.sjs
   sjs_simple-test-server.sjs
@@ -219,9 +221,10 @@ skip-if = true # Bug 1373558
 [browser_net_timeline_ticks.js]
 skip-if = true # TODO: fix the test
 [browser_net_timing-division.js]
 [browser_net_tracking-resources.js]
 [browser_net_truncate-post-data.js]
 [browser_net_truncate.js]
 [browser_net_view-source-debugger.js]
 [browser_net_waterfall-click.js]
+[browser_net_websocket_stacks.js]
 [browser_net_worker_stacks.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/browser_net_websocket_stacks.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that we get stack traces for the network requests made when creating
+// web sockets on the main or worker threads.
+
+const TOP_FILE_NAME = "html_websocket-test-page.html";
+const TOP_URL = EXAMPLE_URL + TOP_FILE_NAME;
+const WORKER_FILE_NAME = "js_websocket-worker-test.js";
+
+const EXPECTED_REQUESTS = [
+  {
+    method: "GET",
+    url: TOP_URL,
+    causeType: "document",
+    causeUri: null,
+    stack: true,
+  },
+  {
+    method: "GET",
+    url: "http://localhost:8080/",
+    causeType: "websocket",
+    causeUri: TOP_URL,
+    stack: [
+      { fn: "openSocket", file: TOP_FILE_NAME, line: 6 },
+      { file: TOP_FILE_NAME, line: 3 },
+    ],
+  },
+  {
+    method: "GET",
+    url: EXAMPLE_URL + WORKER_FILE_NAME,
+    causeType: "script",
+    causeUri: TOP_URL,
+    stack: [ { file: TOP_FILE_NAME, line: 9 } ],
+  },
+  {
+    method: "GET",
+    url: "https://localhost:8081/",
+    causeType: "websocket",
+    causeUri: TOP_URL,
+    stack: [
+      { fn: "openWorkerSocket", file: WORKER_FILE_NAME, line: 5 },
+      { file: WORKER_FILE_NAME, line: 2 },
+    ],
+  },
+];
+
+add_task(async function() {
+  // Load a different URL first to instantiate the network monitor before we
+  // load the page we're really interested in.
+  const { tab, monitor } = await initNetMonitor(SIMPLE_URL);
+
+  const { store, windowRequire, connector } = monitor.panelWin;
+  const {
+    getSortedRequests,
+  } = windowRequire("devtools/client/netmonitor/src/selectors/index");
+
+  BrowserTestUtils.loadURI(tab.linkedBrowser, TOP_URL);
+
+  await waitForNetworkEvents(monitor, EXPECTED_REQUESTS.length);
+
+  is(store.getState().requests.requests.size, EXPECTED_REQUESTS.length,
+    "All the page events should be recorded.");
+
+  // Wait for stack traces from all requests.
+  const requests = getSortedRequests(store.getState());
+  await Promise.all(requests.map(requestItem =>
+    connector.requestData(requestItem.id, "stackTrace")));
+
+  validateRequests(EXPECTED_REQUESTS, monitor);
+
+  await teardown(monitor);
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/html_websocket-test-page.html
@@ -0,0 +1,10 @@
+<script>
+"use strict";
+openSocket();
+
+function openSocket() {
+  new WebSocket("ws://localhost:8080/");
+}
+
+new Worker("js_websocket-worker-test.js");
+</script>
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/test/js_websocket-worker-test.js
@@ -0,0 +1,6 @@
+"use strict";
+openWorkerSocket();
+
+function openWorkerSocket() {
+  new WebSocket("wss://localhost:8081");
+}
--- a/devtools/server/actors/network-event.js
+++ b/devtools/server/actors/network-event.js
@@ -257,28 +257,36 @@ const NetworkEventActor = protocol.Actor
    *         The response packet - stack trace.
    */
   async getStackTrace() {
     let stacktrace = this._stackTrace;
     // If _stackTrace was "true", it means we are in parent process
     // and the stack is available from the content process.
     // Fetch it lazily from here via the message manager.
     if (stacktrace && typeof stacktrace == "boolean") {
+      let id;
+      if (this._cause.type == "websocket") {
+        // Convert to a websocket URL, as in onNetworkEvent.
+        id = this._request.url.replace(/^http/, "ws");
+      } else {
+        id = this._channelId;
+      }
+
       const messageManager = this.netMonitorActor.messageManager;
       stacktrace = await new Promise(resolve => {
         const onMessage = ({ data }) => {
           const { channelId, stack } = data;
-          if (channelId == this._channelId) {
+          if (channelId == id) {
             messageManager.removeMessageListener("debug:request-stack:response",
               onMessage);
             resolve(stack);
           }
         };
         messageManager.addMessageListener("debug:request-stack:response", onMessage);
-        messageManager.sendAsyncMessage("debug:request-stack:request", this._channelId);
+        messageManager.sendAsyncMessage("debug:request-stack:request", id);
       });
       this._stackTrace = stacktrace;
     }
 
     return {
       stacktrace,
     };
   },
--- a/devtools/server/actors/network-monitor.js
+++ b/devtools/server/actors/network-monitor.js
@@ -211,24 +211,35 @@ const NetworkMonitorActor = ActorClassWi
 
     // map channel to actor so we can associate future events with it
     this._netEvents.set(channelId, actor);
     return actor;
   },
 
   // This method is called by NetworkMonitor instance when a new request is fired
   onNetworkEvent(event) {
-    const { channelId } = event;
+    const { channelId, cause, url } = event;
 
     const actor = this.getNetworkEventActor(channelId);
     this._netEvents.set(channelId, actor);
 
-    event.cause.stacktrace = this.stackTraces.has(channelId);
+    // Find the ID which the stack trace collector will use to save this
+    // channel's stack trace.
+    let id;
+    if (cause.type == "websocket") {
+      // Use the URL, but convert from the http URL which this channel uses to
+      // the original websocket URL which triggered this channel's construction.
+      id = url.replace(/^http/, "ws");
+    } else {
+      id = channelId;
+    }
+
+    event.cause.stacktrace = this.stackTraces.has(id);
     if (event.cause.stacktrace) {
-      this.stackTraces.delete(channelId);
+      this.stackTraces.delete(id);
     }
     actor.init(event);
 
     this._networkEventActorsByURL.set(actor._request.url, actor);
 
     this.conn.sendActorEvent(this.parentID, "networkEvent", { eventActor: actor.form() });
     return actor;
   },
--- a/devtools/server/actors/network-monitor/stack-trace-collector.js
+++ b/devtools/server/actors/network-monitor/stack-trace-collector.js
@@ -38,34 +38,46 @@ StackTraceCollector.prototype = {
     Services.obs.removeObserver(this, "network-monitor-alternate-stack");
     ChannelEventSinkFactory.getService().unregisterCollector(this);
     for (const { messageManager } of this.netmonitors) {
       messageManager.removeMessageListener("debug:request-stack:request",
         this.onGetStack);
     }
   },
 
-  _saveStackTrace(channel, stacktrace) {
-    if (this.stacktracesById.has(channel.channelId)) {
+  _saveStackTrace(id, stacktrace) {
+    if (this.stacktracesById.has(id)) {
       // We can get up to two stack traces for the same channel: one each from
       // the two observer topics we are listening to. Use the first stack trace
       // which is specified, and ignore any later one.
       return;
     }
     for (const { messageManager } of this.netmonitors) {
       messageManager.sendAsyncMessage("debug:request-stack-available", {
-        channelId: channel.channelId,
+        channelId: id,
         stacktrace: stacktrace && stacktrace.length > 0,
       });
     }
-    this.stacktracesById.set(channel.channelId, stacktrace);
+    this.stacktracesById.set(id, stacktrace);
   },
 
   observe(subject, topic, data) {
-    const channel = subject.QueryInterface(Ci.nsIHttpChannel);
+    let channel, id;
+    try {
+      channel = subject.QueryInterface(Ci.nsIHttpChannel);
+      id = channel.channelId;
+    } catch (e) {
+      // WebSocketChannels do not have IDs, so use the URL. When a WebSocket is
+      // opened in a content process, a channel is created locally but the HTTP
+      // channel for the connection lives entirely in the parent process. When
+      // the server code running in the parent sees that HTTP channel, it will
+      // look for the creation stack using the websocket's URL.
+      channel = subject.QueryInterface(Ci.nsIWebSocketChannel);
+      id = channel.URI.spec;
+    }
 
     if (!matchRequest(channel, this.filters)) {
       return;
     }
 
     const stacktrace = [];
     switch (topic) {
       case "http-on-opening-request": {
@@ -116,33 +128,33 @@ StackTraceCollector.prototype = {
           frame = frame.parent || frame.asyncParent;
         }
         break;
       }
       default:
         throw new Error("Unexpected observe() topic");
     }
 
-    this._saveStackTrace(channel, stacktrace);
+    this._saveStackTrace(id, stacktrace);
   },
 
   // eslint-disable-next-line no-shadow
   onChannelRedirect(oldChannel, newChannel, flags) {
     // We can be called with any nsIChannel, but are interested only in HTTP channels
     try {
       oldChannel.QueryInterface(Ci.nsIHttpChannel);
       newChannel.QueryInterface(Ci.nsIHttpChannel);
     } catch (ex) {
       return;
     }
 
     const oldId = oldChannel.channelId;
     const stacktrace = this.stacktracesById.get(oldId);
     if (stacktrace) {
-      this._saveStackTrace(newChannel, stacktrace);
+      this._saveStackTrace(newChannel.channelId, stacktrace);
     }
   },
 
   getStackTrace(channelId) {
     const trace = this.stacktracesById.get(channelId);
     this.stacktracesById.delete(channelId);
     return trace;
   },
--- a/devtools/server/actors/thread.js
+++ b/devtools/server/actors/thread.js
@@ -552,26 +552,46 @@ const ThreadActor = ActorClassWithSpec(t
     }
 
     // If the parent actor has been closed, terminate the debuggee script
     // instead of continuing. Executing JS after the content window is gone is
     // a bad idea.
     return this._parentClosed ? null : undefined;
   },
 
-  _makeOnEnterFrame: function({ pauseAndRespond }) {
+  _makeOnEnterFrame: function({ thread, pauseAndRespond }) {
     return frame => {
       const { generatedSourceActor } = this.sources.getFrameLocation(frame);
 
       const url = generatedSourceActor.url;
       if (this.sources.isBlackBoxed(url)) {
         return undefined;
       }
 
-      return pauseAndRespond(frame);
+      // If the initial frame offset is a step target, we are done.
+      if (frame.script.getOffsetMetadata(frame.offset).isStepStart) {
+        return pauseAndRespond(frame);
+      }
+
+      // Continue forward until we get to a valid step target.
+      const { onStep, onPop } = thread._makeSteppingHooks(
+        null, "next", false, null
+      );
+
+      if (thread.dbg.replaying) {
+        const offsets =
+          thread._findReplayingStepOffsets(null, frame,
+                                           /* rewinding = */ false);
+        frame.setReplayingOnStep(onStep, offsets);
+      } else {
+        frame.onStep = onStep;
+      }
+
+      frame.onPop = onPop;
+      return undefined;
     };
   },
 
   _makeOnPop: function({ thread, pauseAndRespond, startLocation, steppingType }) {
     const result = function(completion) {
       // onPop is called with 'this' set to the current frame.
       const generatedLocation = thread.sources.getFrameLocation(this);
 
@@ -641,17 +661,17 @@ const ThreadActor = ActorClassWithSpec(t
   _intraFrameLocationIsStepTarget: function(startLocation, script, offset) {
     // Only allow stepping stops at entry points for the line.
     if (!script.getOffsetMetadata(offset).isBreakpoint) {
       return false;
     }
 
     const generatedLocation = this.sources.getScriptOffsetLocation(script, offset);
 
-    if (startLocation.generatedUrl !== generatedLocation.generatedUrl) {
+    if (!startLocation || startLocation.generatedUrl !== generatedLocation.generatedUrl) {
       return true;
     }
 
     // TODO(logan): When we remove points points, this can be removed too as
     // we assert that we're at a different frame offset from the last time
     // we paused.
     const lineChanged = startLocation.generatedLine !== generatedLocation.generatedLine;
     const columnChanged =
@@ -670,22 +690,16 @@ const ThreadActor = ActorClassWithSpec(t
       return pausePoint.step;
     }
 
     return script.getOffsetMetadata(offset).isStepStart;
   },
 
   _makeOnStep: function({ thread, pauseAndRespond, startFrame,
                           startLocation, steppingType, completion, rewinding }) {
-    // Breaking in place: we should always pause.
-    if (steppingType === "break") {
-      return () => pauseAndRespond(this);
-    }
-
-    // Otherwise take what a "step" means into consideration.
     return function() {
       // onStep is called with 'this' set to the current frame.
 
       const generatedLocation = thread.sources.getFrameLocation(this);
 
       // Always continue execution if either:
       //
       // 1. We are in a source mapped region, but inside a null mapping
--- a/dom/websocket/WebSocket.cpp
+++ b/dom/websocket/WebSocket.cpp
@@ -13,16 +13,17 @@
 #include "mozilla/DOMEventTargetHelper.h"
 #include "mozilla/net/WebSocketChannel.h"
 #include "mozilla/dom/File.h"
 #include "mozilla/dom/MessageEvent.h"
 #include "mozilla/dom/MessageEventBinding.h"
 #include "mozilla/dom/nsCSPContext.h"
 #include "mozilla/dom/nsCSPUtils.h"
 #include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/dom/SerializedStackHolder.h"
 #include "mozilla/dom/WorkerPrivate.h"
 #include "mozilla/dom/WorkerRef.h"
 #include "mozilla/dom/WorkerRunnable.h"
 #include "mozilla/dom/WorkerScope.h"
 #include "nsAutoPtr.h"
 #include "mozilla/LoadInfo.h"
 #include "nsGlobalWindow.h"
 #include "nsIScriptGlobalObject.h"
@@ -119,17 +120,18 @@ class WebSocketImpl final : public nsIIn
   nsresult Init(JSContext* aCx, nsIPrincipal* aLoadingPrincipal,
                 nsIPrincipal* aPrincipal, bool aIsServerSide,
                 const nsAString& aURL, nsTArray<nsString>& aProtocolArray,
                 const nsACString& aScriptFile, uint32_t aScriptLine,
                 uint32_t aScriptColumn);
 
   nsresult AsyncOpen(nsIPrincipal* aPrincipal, uint64_t aInnerWindowID,
                      nsITransportProvider* aTransportProvider,
-                     const nsACString& aNegotiatedExtensions);
+                     const nsACString& aNegotiatedExtensions,
+                     UniquePtr<SerializedStackHolder> aOriginStack);
 
   nsresult ParseURL(const nsAString& aURL);
   nsresult InitializeConnection(nsIPrincipal* aPrincipal,
                                 nsICookieSettings* aCookieSettings);
 
   // These methods when called can release the WebSocket object
   void FailConnection(uint16_t reasonCode,
                       const nsACString& aReasonString = EmptyCString());
@@ -1114,21 +1116,23 @@ class ConnectRunnable final : public Web
   // Raw pointer. This worker runnable runs synchronously.
   WebSocketImpl* mImpl;
 
   bool mConnectionFailed;
 };
 
 class AsyncOpenRunnable final : public WebSocketMainThreadRunnable {
  public:
-  explicit AsyncOpenRunnable(WebSocketImpl* aImpl)
+  explicit AsyncOpenRunnable(WebSocketImpl* aImpl,
+                             UniquePtr<SerializedStackHolder> aOriginStack)
       : WebSocketMainThreadRunnable(
             aImpl->mWorkerRef->Private(),
             NS_LITERAL_CSTRING("WebSocket :: AsyncOpen")),
         mImpl(aImpl),
+        mOriginStack(std::move(aOriginStack)),
         mErrorCode(NS_OK) {
     MOZ_ASSERT(mWorkerPrivate);
     mWorkerPrivate->AssertIsOnWorkerThread();
   }
 
   nsresult ErrorCode() const { return mErrorCode; }
 
  protected:
@@ -1154,33 +1158,36 @@ class AsyncOpenRunnable final : public W
     if (topWindow) {
       topInner = topWindow->GetCurrentInnerWindow();
     }
 
     if (topInner) {
       windowID = topInner->WindowID();
     }
 
-    mErrorCode = mImpl->AsyncOpen(principal, windowID, nullptr, EmptyCString());
+    mErrorCode = mImpl->AsyncOpen(principal, windowID, nullptr, EmptyCString(),
+                                  std::move(mOriginStack));
     return true;
   }
 
   virtual bool InitWindowless(WorkerPrivate* aTopLevelWorkerPrivate) override {
     MOZ_ASSERT(NS_IsMainThread());
     MOZ_ASSERT(aTopLevelWorkerPrivate && !aTopLevelWorkerPrivate->GetWindow());
 
     mErrorCode = mImpl->AsyncOpen(aTopLevelWorkerPrivate->GetPrincipal(), 0,
-                                  nullptr, EmptyCString());
+                                  nullptr, EmptyCString(), nullptr);
     return true;
   }
 
  private:
   // Raw pointer. This worker runs synchronously.
   WebSocketImpl* mImpl;
 
+  UniquePtr<SerializedStackHolder> mOriginStack;
+
   nsresult mErrorCode;
 };
 
 }  // namespace
 
 already_AddRefed<WebSocket> WebSocket::ConstructorCommon(
     const GlobalObject& aGlobal, const nsAString& aUrl,
     const Sequence<nsString>& aProtocols,
@@ -1356,34 +1363,47 @@ already_AddRefed<WebSocket> WebSocket::C
   }
 
   if (NS_IsMainThread()) {
     MOZ_ASSERT(principal);
 
     nsCOMPtr<nsPIDOMWindowInner> ownerWindow = do_QueryInterface(global);
     nsPIDOMWindowOuter* outerWindow = ownerWindow->GetOuterWindow();
 
+    UniquePtr<SerializedStackHolder> stack;
+    nsIDocShell* docShell = outerWindow->GetDocShell();
+    if (docShell && docShell->GetWatchedByDevtools()) {
+      stack = GetCurrentStackForNetMonitor(aGlobal.Context());
+    }
+
     uint64_t windowID = 0;
     nsCOMPtr<nsPIDOMWindowOuter> topWindow = outerWindow->GetScriptableTop();
     nsCOMPtr<nsPIDOMWindowInner> topInner;
     if (topWindow) {
       topInner = topWindow->GetCurrentInnerWindow();
     }
 
     if (topInner) {
       windowID = topInner->WindowID();
     }
 
     aRv = webSocket->mImpl->AsyncOpen(principal, windowID, aTransportProvider,
-                                      aNegotiatedExtensions);
+                                      aNegotiatedExtensions, std::move(stack));
   } else {
     MOZ_ASSERT(!aTransportProvider && aNegotiatedExtensions.IsEmpty(),
                "not yet implemented");
+
+    UniquePtr<SerializedStackHolder> stack;
+    WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
+    if (workerPrivate->IsWatchedByDevtools()) {
+      stack = GetCurrentStackForNetMonitor(aGlobal.Context());
+    }
+
     RefPtr<AsyncOpenRunnable> runnable =
-        new AsyncOpenRunnable(webSocket->mImpl);
+        new AsyncOpenRunnable(webSocket->mImpl, std::move(stack));
     runnable->Dispatch(Canceling, aRv);
     if (NS_WARN_IF(aRv.Failed())) {
       return nullptr;
     }
 
     aRv = runnable->ErrorCode();
   }
 
@@ -1627,17 +1647,18 @@ nsresult WebSocketImpl::Init(JSContext* 
   }
 
   return NS_OK;
 }
 
 nsresult WebSocketImpl::AsyncOpen(nsIPrincipal* aPrincipal,
                                   uint64_t aInnerWindowID,
                                   nsITransportProvider* aTransportProvider,
-                                  const nsACString& aNegotiatedExtensions) {
+                                  const nsACString& aNegotiatedExtensions,
+                                  UniquePtr<SerializedStackHolder> aOriginStack) {
   MOZ_ASSERT(NS_IsMainThread(), "Not running on main thread");
   MOZ_ASSERT_IF(!aTransportProvider, aNegotiatedExtensions.IsEmpty());
 
   nsCString asciiOrigin;
   nsresult rv = nsContentUtils::GetASCIIOrigin(aPrincipal, asciiOrigin);
   NS_ENSURE_SUCCESS(rv, rv);
 
   if (aTransportProvider) {
@@ -1654,16 +1675,18 @@ nsresult WebSocketImpl::AsyncOpen(nsIPri
     MOZ_ASSERT(NS_SUCCEEDED(rv));
   }
 
   rv = mChannel->AsyncOpen(uri, asciiOrigin, aInnerWindowID, this, nullptr);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return NS_ERROR_CONTENT_BLOCKED;
   }
 
+  NotifyNetworkMonitorAlternateStack(mChannel, std::move(aOriginStack));
+
   mInnerWindowID = aInnerWindowID;
 
   return NS_OK;
 }
 
 //-----------------------------------------------------------------------------
 // WebSocketImpl methods:
 //-----------------------------------------------------------------------------
--- a/js/src/builtin/ModuleObject.cpp
+++ b/js/src/builtin/ModuleObject.cpp
@@ -1038,18 +1038,21 @@ bool ModuleObject::execute(JSContext* cx
   if (!AssertFrozen(cx, self)) {
     return false;
   }
 #endif
 
   RootedScript script(cx, self->script());
 
   // The top-level script if a module is only ever executed once. Clear the
-  // reference to prevent us keeping this alive unnecessarily.
-  self->setReservedSlot(ScriptSlot, UndefinedValue());
+  // reference at exit to prevent us keeping this alive unnecessarily. This is
+  // kept while executing so it is available to the debugger.
+  auto guardA = mozilla::MakeScopeExit([&] {
+      self->setReservedSlot(ScriptSlot, UndefinedValue());
+    });
 
   RootedModuleEnvironmentObject scope(cx, self->environment());
   if (!scope) {
     JS_ReportErrorASCII(cx,
                         "Module declarations have not yet been instantiated");
     return false;
   }
 
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Environment-module-01.js
@@ -0,0 +1,26 @@
+// Debug environments for module environments should include variables that are
+// are not closed over or exported.
+
+var g = newGlobal({newCompartment: true});
+var dbg = Debugger(g);
+dbg.onEnterFrame = function (frame) {
+  if (!frame.older) {
+    return;
+  }
+  const env = frame.older.environment;
+  assertEq(env.names().join(','), "foo,y,x,z");
+  assertEq(env.getVariable('x'), 0);
+  assertEq(env.getVariable('y'), 1);
+  assertEq(env.getVariable('z'), 2);
+  env.setVariable('x', 3);
+  assertEq(env.getVariable('x'), 3);
+};
+const m = g.parseModule(`
+  var x = 0;
+  export var y = 1;
+  const z = 2;
+  foo();
+  function foo() {}
+`);
+m.declarationInstantiation();
+m.evaluation();
--- a/js/src/vm/EnvironmentObject.cpp
+++ b/js/src/vm/EnvironmentObject.cpp
@@ -410,21 +410,21 @@ ModuleEnvironmentObject* ModuleEnvironme
   }
   MOZ_ASSERT(env->lastProperty()->getObjectFlags() & BaseShape::NOT_EXTENSIBLE);
   MOZ_ASSERT(!env->inDictionaryMode());
 #endif
 
   return env;
 }
 
-ModuleObject& ModuleEnvironmentObject::module() {
+ModuleObject& ModuleEnvironmentObject::module() const {
   return getReservedSlot(MODULE_SLOT).toObject().as<ModuleObject>();
 }
 
-IndirectBindingMap& ModuleEnvironmentObject::importBindings() {
+IndirectBindingMap& ModuleEnvironmentObject::importBindings() const {
   return module().importBindings();
 }
 
 bool ModuleEnvironmentObject::createImportBinding(JSContext* cx,
                                                   HandleAtom importName,
                                                   HandleModuleObject module,
                                                   HandleAtom localName) {
   RootedId importNameId(cx, AtomToId(importName));
@@ -1476,38 +1476,47 @@ class DebugEnvironmentProxyHandler : pub
                              Action action, MutableHandleValue vp,
                              AccessResult* accessResult) const {
     MOZ_ASSERT(&debugEnv->environment() == env);
     MOZ_ASSERT_IF(action == SET, !debugEnv->isOptimizedOut());
     *accessResult = ACCESS_GENERIC;
     LiveEnvironmentVal* maybeLiveEnv =
         DebugEnvironments::hasLiveEnvironment(*env);
 
-    if (env->is<ModuleEnvironmentObject>()) {
-      /* Everything is aliased and stored in the environment object. */
-      return true;
-    }
-
-    /* Handle unaliased formals, vars, lets, and consts at function scope. */
-    if (env->is<CallObject>()) {
-      CallObject& callobj = env->as<CallObject>();
-      RootedFunction fun(cx, &callobj.callee());
-      RootedScript script(cx, JSFunction::getOrCreateScript(cx, fun));
-      if (!script->ensureHasAnalyzedArgsUsage(cx)) {
-        return false;
+    // Handle unaliased formals, vars, lets, and consts at function or module
+    // scope.
+    if (env->is<CallObject>() || env->is<ModuleEnvironmentObject>()) {
+      RootedScript script(cx);
+      if (env->is<CallObject>()) {
+        CallObject& callobj = env->as<CallObject>();
+        RootedFunction fun(cx, &callobj.callee());
+        script = JSFunction::getOrCreateScript(cx, fun);
+        if (!script->ensureHasAnalyzedArgsUsage(cx)) {
+          return false;
+        }
+      } else {
+        script = env->as<ModuleEnvironmentObject>().module().maybeScript();
+        if (!script) {
+          *accessResult = ACCESS_LOST;
+          return true;
+        }
       }
 
       BindingIter bi(script);
       while (bi && NameToId(bi.name()->asPropertyName()) != id) {
         bi++;
       }
       if (!bi) {
         return true;
       }
 
+      if (bi.location().kind() == BindingLocation::Kind::Import) {
+        return true;
+      }
+
       if (!bi.hasArgumentSlot()) {
         if (bi.closedOver()) {
           return true;
         }
 
         uint32_t i = bi.location().slot();
         if (maybeLiveEnv) {
           AbstractFramePtr frame = maybeLiveEnv->frame();
@@ -1772,16 +1781,20 @@ class DebugEnvironmentProxyHandler : pub
     return env.is<LexicalEnvironmentObject>() &&
            !env.as<LexicalEnvironmentObject>().isExtensible();
   }
 
   static Scope* getEnvironmentScope(const JSObject& env) {
     if (isFunctionEnvironment(env)) {
       return env.as<CallObject>().callee().nonLazyScript()->bodyScope();
     }
+    if (env.is<ModuleEnvironmentObject>()) {
+      JSScript* script = env.as<ModuleEnvironmentObject>().module().maybeScript();
+      return script ? script->bodyScope() : nullptr;
+    }
     if (isNonExtensibleLexicalEnvironment(env)) {
       return &env.as<LexicalEnvironmentObject>().scope();
     }
     if (env.is<VarEnvironmentObject>()) {
       return &env.as<VarEnvironmentObject>().scope();
     }
     if (env.is<WasmInstanceEnvironmentObject>()) {
       return &env.as<WasmInstanceEnvironmentObject>().scope();
--- a/js/src/vm/EnvironmentObject.h
+++ b/js/src/vm/EnvironmentObject.h
@@ -403,18 +403,18 @@ class ModuleEnvironmentObject : public E
 
  public:
   static const Class class_;
 
   static const uint32_t RESERVED_SLOTS = 2;
 
   static ModuleEnvironmentObject* create(JSContext* cx,
                                          HandleModuleObject module);
-  ModuleObject& module();
-  IndirectBindingMap& importBindings();
+  ModuleObject& module() const;
+  IndirectBindingMap& importBindings() const;
 
   bool createImportBinding(JSContext* cx, HandleAtom importName,
                            HandleModuleObject module, HandleAtom exportName);
 
   bool hasImportBinding(HandlePropertyName name);
 
   bool lookupImport(jsid name, ModuleEnvironmentObject** envOut,
                     Shape** shapeOut);