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 idunknown
push userunknown
push dateunknown
reviewersmerge
milestone68.0a1
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);