Bug 1539468 - Preview can initially take awhile to populate. r=loganfsmyth
authorJason Laster <jlaster@mozilla.com>
Wed, 15 May 2019 14:46:09 +0000
changeset 532791 f94cc66e79ecbf3a072858aaa5611ca4ec035b9c
parent 532790 03b1762fb0734dc1f58ce5453a8aef8770745538
child 532792 8b7a74cee6c4a11fb0dd980409288b4b3c299e6c
push id11272
push userapavel@mozilla.com
push dateThu, 16 May 2019 15:28:22 +0000
treeherdermozilla-beta@2265bfc5920d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersloganfsmyth
bugs1539468
milestone68.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 1539468 - Preview can initially take awhile to populate. r=loganfsmyth Differential Revision: https://phabricator.services.mozilla.com/D30952
devtools/client/debugger/dist/parser-worker.js
devtools/client/debugger/panel.js
devtools/client/debugger/src/actions/ast.js
devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js
devtools/client/debugger/src/actions/expressions.js
devtools/client/debugger/src/actions/navigation.js
devtools/client/debugger/src/actions/pause/commands.js
devtools/client/debugger/src/actions/pause/mapScopes.js
devtools/client/debugger/src/actions/sources/loadSourceText.js
devtools/client/debugger/src/actions/sources/symbols.js
devtools/client/debugger/src/actions/tests/__snapshots__/project-text-search.spec.js.snap
devtools/client/debugger/src/actions/types/index.js
devtools/client/debugger/src/actions/utils/middleware/log.js
devtools/client/debugger/src/client/index.js
devtools/client/debugger/src/main.js
devtools/client/debugger/src/test/tests-setup.js
devtools/client/debugger/src/utils/bootstrap.js
devtools/client/debugger/src/utils/pause/mapScopes/index.js
devtools/client/debugger/src/utils/test-head.js
devtools/client/debugger/src/workers/parser/index.js
devtools/client/debugger/src/workers/parser/worker.js
devtools/client/debugger/test/mochitest/helpers.js
devtools/client/framework/toolbox.js
devtools/client/webconsole/webconsole.js
--- a/devtools/client/debugger/dist/parser-worker.js
+++ b/devtools/client/debugger/dist/parser-worker.js
@@ -17940,28 +17940,32 @@ var _mapExpression2 = _interopRequireDef
 var _devtoolsUtils = __webpack_require__(7);
 
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
 const { workerHandler } = _devtoolsUtils.workerUtils; /* 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/>. */
 
+function clearState() {
+  (0, _ast.clearASTs)();
+  (0, _getScopes.clearScopes)();
+  (0, _sources.clearSources)();
+  (0, _getSymbols.clearSymbols)();
+}
+
 self.onmessage = workerHandler({
   findOutOfScopeLocations: _findOutOfScopeLocations2.default,
   getSymbols: _getSymbols.getSymbols,
   getScopes: _getScopes2.default,
-  clearSymbols: _getSymbols.clearSymbols,
-  clearScopes: _getScopes.clearScopes,
-  clearASTs: _ast.clearASTs,
-  setSource: _sources.setSource,
-  clearSources: _sources.clearSources,
+  clearState,
   getNextStep: _steps.getNextStep,
   hasSyntaxError: _validate.hasSyntaxError,
-  mapExpression: _mapExpression2.default
+  mapExpression: _mapExpression2.default,
+  setSource: _sources.setSource
 });
 
 /***/ }),
 /* 200 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
--- a/devtools/client/debugger/panel.js
+++ b/devtools/client/debugger/panel.js
@@ -28,17 +28,20 @@ DebuggerPanel.prototype = {
       actions,
       store,
       selectors,
       client
     } = await this.panelWin.Debugger.bootstrap({
       threadClient: this.toolbox.threadClient,
       tabTarget: this.toolbox.target,
       debuggerClient: this.toolbox.target.client,
-      sourceMaps: this.toolbox.sourceMapService,
+      workers: {
+        sourceMaps: this.toolbox.sourceMapService,
+        evaluationsParser: this.toolbox.parserService
+      },
       panel: this
     });
 
     this._actions = actions;
     this._store = store;
     this._selectors = selectors;
     this._client = client;
     this.isReady = true;
--- a/devtools/client/debugger/src/actions/ast.js
+++ b/devtools/client/debugger/src/actions/ast.js
@@ -3,23 +3,21 @@
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 // @flow
 
 import { getSourceWithContent, getSelectedLocation } from "../selectors";
 
 import { setInScopeLines } from "./ast/setInScopeLines";
 
-import * as parser from "../workers/parser";
-
 import type { Context } from "../types";
 import type { ThunkArgs, Action } from "./types";
 
 export function setOutOfScopeLocations(cx: Context) {
-  return async ({ dispatch, getState }: ThunkArgs) => {
+  return async ({ dispatch, getState, parser }: ThunkArgs) => {
     const location = getSelectedLocation(getState());
     if (!location) {
       return;
     }
 
     const { source, content } = getSourceWithContent(
       getState(),
       location.sourceId
--- a/devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js
+++ b/devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js
@@ -39,16 +39,17 @@ async function mapLocations(
   generatedLocations: SourceLocation[],
   { sourceMaps }: ThunkArgs
 ) {
   if (generatedLocations.length == 0) {
     return [];
   }
 
   const { sourceId } = generatedLocations[0];
+
   const originalLocations = await sourceMaps.getOriginalLocations(
     sourceId,
     generatedLocations
   );
 
   return zip(originalLocations, generatedLocations).map(
     ([location, generatedLocation]) => ({ location, generatedLocation })
   );
--- a/devtools/client/debugger/src/actions/expressions.js
+++ b/devtools/client/debugger/src/actions/expressions.js
@@ -17,35 +17,34 @@ import {
   getIsPaused,
   isMapScopesEnabled
 } from "../selectors";
 import { PROMISE } from "./utils/middleware/promise";
 import { wrapExpression } from "../utils/expressions";
 import { features } from "../utils/prefs";
 import { isOriginal } from "../utils/source";
 
-import * as parser from "../workers/parser";
 import type { Expression, ThreadContext } from "../types";
 import type { ThunkArgs } from "./types";
 
 /**
  * Add expression for debugger to watch
  *
  * @param {object} expression
  * @param {number} expression.id
  * @memberof actions/pause
  * @static
  */
 export function addExpression(cx: ThreadContext, input: string) {
-  return async ({ dispatch, getState }: ThunkArgs) => {
+  return async ({ dispatch, getState, evaluationsParser }: ThunkArgs) => {
     if (!input) {
       return;
     }
 
-    const expressionError = await parser.hasSyntaxError(input);
+    const expressionError = await evaluationsParser.hasSyntaxError(input);
 
     const expression = getExpression(getState(), input);
     if (expression) {
       return dispatch(evaluateExpression(cx, expression));
     }
 
     dispatch({ type: "ADD_EXPRESSION", cx, input, expressionError });
 
@@ -75,17 +74,17 @@ export function clearExpressionError() {
   return { type: "CLEAR_EXPRESSION_ERROR" };
 }
 
 export function updateExpression(
   cx: ThreadContext,
   input: string,
   expression: Expression
 ) {
-  return async ({ dispatch, getState }: ThunkArgs) => {
+  return async ({ dispatch, getState, parser }: ThunkArgs) => {
     if (!input) {
       return;
     }
 
     const expressionError = await parser.hasSyntaxError(input);
     dispatch({
       type: "UPDATE_EXPRESSION",
       cx,
@@ -172,34 +171,40 @@ function evaluateExpression(cx: ThreadCo
   };
 }
 
 /**
  * Gets information about original variable names from the source map
  * and replaces all posible generated names.
  */
 export function getMappedExpression(expression: string) {
-  return async function({ dispatch, getState, client, sourceMaps }: ThunkArgs) {
+  return async function({
+    dispatch,
+    getState,
+    client,
+    sourceMaps,
+    evaluationsParser
+  }: ThunkArgs) {
     const thread = getCurrentThread(getState());
     const mappings = getSelectedScopeMappings(getState(), thread);
     const bindings = getSelectedFrameBindings(getState(), thread);
 
     // We bail early if we do not need to map the expression. This is important
-    // because mapping an expression can be slow if the parser worker is
-    // busy doing other work.
+    // because mapping an expression can be slow if the evaluationsParser
+    // worker is busy doing other work.
     //
     // 1. there are no mappings - we do not need to map original expressions
     // 2. does not contain `await` - we do not need to map top level awaits
     // 3. does not contain `=` - we do not need to map assignments
     const shouldMapScopes = isMapScopesEnabled(getState()) && mappings;
     if (!shouldMapScopes && !expression.match(/(await|=)/)) {
       return null;
     }
 
-    return parser.mapExpression(
+    return evaluationsParser.mapExpression(
       expression,
       mappings,
       bindings || [],
       features.mapExpressionBindings && getIsPaused(getState(), thread),
       features.mapAwaitExpression
     );
   };
 }
--- a/devtools/client/debugger/src/actions/navigation.js
+++ b/devtools/client/debugger/src/actions/navigation.js
@@ -7,46 +7,42 @@
 import { clearDocuments } from "../utils/editor";
 import sourceQueue from "../utils/source-queue";
 import { getSourceList } from "../reducers/sources";
 import { waitForMs } from "../utils/utils";
 
 import { newGeneratedSources } from "./sources";
 import { updateWorkers } from "./debuggee";
 
-import {
-  clearASTs,
-  clearSymbols,
-  clearScopes,
-  clearSources
-} from "../workers/parser";
-
 import { clearWasmStates } from "../utils/wasm";
 import { getMainThread } from "../selectors";
 import type { Action, ThunkArgs } from "./types";
 
 /**
  * Redux actions for the navigation state
  * @module actions/navigation
  */
 
 /**
  * @memberof actions/navigation
  * @static
  */
 export function willNavigate(event: Object) {
-  return function({ dispatch, getState, client, sourceMaps }: ThunkArgs) {
+  return async function({
+    dispatch,
+    getState,
+    client,
+    sourceMaps,
+    parser
+  }: ThunkArgs) {
     sourceQueue.clear();
     sourceMaps.clearSourceMaps();
     clearWasmStates();
     clearDocuments();
-    clearSymbols();
-    clearASTs();
-    clearScopes();
-    clearSources();
+    parser.clear();
     client.detachWorkers();
     const thread = getMainThread(getState());
 
     dispatch({
       type: "NAVIGATE",
       mainThread: { ...thread, url: event.url }
     });
   };
--- a/devtools/client/debugger/src/actions/pause/commands.js
+++ b/devtools/client/debugger/src/actions/pause/commands.js
@@ -8,17 +8,16 @@
 import {
   getSource,
   getSourceContent,
   getTopFrame,
   getSelectedFrame,
   getThreadContext
 } from "../../selectors";
 import { PROMISE } from "../utils/middleware/promise";
-import { getNextStep } from "../../workers/parser";
 import { addHiddenBreakpoint } from "../breakpoints";
 import { evaluateExpressions } from "../expressions";
 import { selectLocation } from "../sources";
 import { fetchScopes } from "./fetchScopes";
 import { features } from "../../utils/prefs";
 import { recordEvent } from "../../utils/telemetry";
 import assert from "../../utils/assert";
 import { isFulfilled, type AsyncValue } from "../../utils/async-value";
@@ -208,29 +207,32 @@ function hasAwait(content: AsyncValue<So
 
 /**
  * @memberOf actions/pause
  * @static
  * @param stepType
  * @returns {function(ThunkArgs)}
  */
 export function astCommand(cx: ThreadContext, stepType: Command) {
-  return async ({ dispatch, getState, sourceMaps }: ThunkArgs) => {
+  return async ({ dispatch, getState, sourceMaps, parser }: ThunkArgs) => {
     if (!features.asyncStepping) {
       return dispatch(command(cx, stepType));
     }
 
     if (stepType == "stepOver") {
       // This type definition is ambiguous:
       const frame: any = getTopFrame(getState(), cx.thread);
       const source = getSource(getState(), frame.location.sourceId);
       const content = source ? getSourceContent(getState(), source.id) : null;
 
       if (source && hasAwait(content, frame.location)) {
-        const nextLocation = await getNextStep(source.id, frame.location);
+        const nextLocation = await parser.getNextStep(
+          source.id,
+          frame.location
+        );
         if (nextLocation) {
           await dispatch(addHiddenBreakpoint(cx, nextLocation));
           return dispatch(command(cx, "resume"));
         }
       }
     }
 
     return dispatch(command(cx, stepType));
--- a/devtools/client/debugger/src/actions/pause/mapScopes.js
+++ b/devtools/client/debugger/src/actions/pause/mapScopes.js
@@ -50,17 +50,18 @@ export function toggleMapScopes() {
   };
 }
 
 export function mapScopes(
   cx: ThreadContext,
   scopes: Promise<Scope>,
   frame: Frame
 ) {
-  return async function({ dispatch, getState, client, sourceMaps }: ThunkArgs) {
+  return async function(thunkArgs: ThunkArgs) {
+    const { dispatch, getState } = thunkArgs;
     assert(cx.thread == frame.thread, "Thread mismatch");
 
     const generatedSource = getSource(
       getState(),
       frame.generatedLocation.sourceId
     );
 
     const source = getSource(getState(), frame.location.sourceId);
@@ -94,18 +95,17 @@ export function mapScopes(
 
           return await buildMappedScopes(
             source,
             content && isFulfilled(content)
               ? content.value
               : { type: "text", value: "", contentType: undefined },
             frame,
             await scopes,
-            sourceMaps,
-            client
+            thunkArgs
           );
         } catch (e) {
           log(e);
           return null;
         }
       })()
     });
   };
--- a/devtools/client/debugger/src/actions/sources/loadSourceText.js
+++ b/devtools/client/debugger/src/actions/sources/loadSourceText.js
@@ -16,17 +16,16 @@ import {
   getSourceActorsForSource
 } from "../../selectors";
 import { addBreakpoint } from "../breakpoints";
 
 import { prettyPrintSource } from "./prettyPrint";
 import { setBreakableLines } from "./breakableLines";
 import { isFulfilled } from "../../utils/async-value";
 
-import * as parser from "../../workers/parser";
 import { isOriginal, isPretty } from "../../utils/source";
 import {
   memoizeableAction,
   type MemoizedAction
 } from "../../utils/memoizableAction";
 
 import { Telemetry } from "devtools-modules";
 
@@ -88,17 +87,17 @@ async function loadSource(
     text: response.source,
     contentType: response.contentType || "text/javascript"
   };
 }
 
 async function loadSourceTextPromise(
   cx: Context,
   source: Source,
-  { dispatch, getState, client, sourceMaps }: ThunkArgs
+  { dispatch, getState, client, sourceMaps, parser }: ThunkArgs
 ): Promise<?Source> {
   const epoch = getSourcesEpoch(getState());
   await dispatch({
     type: "LOAD_SOURCE_TEXT",
     sourceId: source.id,
     epoch,
     [PROMISE]: loadSource(getState(), source, { sourceMaps, client, getState })
   });
--- a/devtools/client/debugger/src/actions/sources/symbols.js
+++ b/devtools/client/debugger/src/actions/sources/symbols.js
@@ -5,27 +5,25 @@
 // @flow
 
 import { hasSymbols, getSymbols } from "../../selectors";
 
 import { PROMISE } from "../utils/middleware/promise";
 import { updateTab } from "../tabs";
 import { loadSourceText } from "./loadSourceText";
 
-import * as parser from "../../workers/parser";
-
 import {
   memoizeableAction,
   type MemoizedAction
 } from "../../utils/memoizableAction";
 
 import type { Source, Context } from "../../types";
 import type { Symbols } from "../../reducers/types";
 
-async function doSetSymbols(cx, source, { dispatch, getState }) {
+async function doSetSymbols(cx, source, { dispatch, getState, parser }) {
   const sourceId = source.id;
 
   await dispatch(loadSourceText({ cx, source }));
 
   await dispatch({
     type: "SET_SYMBOLS",
     cx,
     sourceId,
--- a/devtools/client/debugger/src/actions/tests/__snapshots__/project-text-search.spec.js.snap
+++ b/devtools/client/debugger/src/actions/tests/__snapshots__/project-text-search.spec.js.snap
@@ -119,16 +119,37 @@ Array [
       },
     ],
     "sourceId": "bar",
     "type": "RESULT",
   },
 ]
 `;
 
+exports[`project text search should search a specific source 2`] = `
+Array [
+  Object {
+    "filepath": "http://localhost:8000/examples/bar",
+    "matches": Array [
+      Object {
+        "column": 9,
+        "line": 1,
+        "match": "bla",
+        "matchIndex": 9,
+        "sourceId": "bar",
+        "type": "MATCH",
+        "value": "function bla(x, y) {",
+      },
+    ],
+    "sourceId": "bar",
+    "type": "RESULT",
+  },
+]
+`;
+
 exports[`project text search should search all the loaded sources based on the query 1`] = `
 Array [
   Object {
     "filepath": "http://localhost:8000/examples/foo1",
     "matches": Array [
       Object {
         "column": 9,
         "line": 1,
--- a/devtools/client/debugger/src/actions/types/index.js
+++ b/devtools/client/debugger/src/actions/types/index.js
@@ -14,16 +14,17 @@ import type { SearchOperation } from "..
 import type { BreakpointAction } from "./BreakpointAction";
 import type { SourceAction } from "./SourceAction";
 import type { SourceActorAction } from "./SourceActorAction";
 import type { UIAction } from "./UIAction";
 import type { PauseAction } from "./PauseAction";
 import type { ASTAction } from "./ASTAction";
 import { clientCommands } from "../../client/firefox";
 import type { Panel } from "../../client/firefox/types";
+import type { ParserDispatcher } from "../../workers/parser";
 
 /**
  * Flow types
  * @module actions/types
  */
 
 /**
  * Argument parameters via Thunk middleware for {@link https://github.com/gaearon/redux-thunk|Redux Thunk}
@@ -33,16 +34,18 @@ import type { Panel } from "../../client
  * @typedef {Object} ThunkArgs
  */
 export type ThunkArgs = {
   dispatch: (action: any) => Promise<any>,
   forkedDispatch: (action: any) => Promise<any>,
   getState: () => State,
   client: typeof clientCommands,
   sourceMaps: SourceMaps,
+  parser: ParserDispatcher,
+  evaluationsParser: ParserDispatcher,
   panel: Panel
 };
 
 export type Thunk = ThunkArgs => any;
 
 export type ActionType = Object | Function;
 
 type ProjectTextSearchResult = {
--- a/devtools/client/debugger/src/actions/utils/middleware/log.js
+++ b/devtools/client/debugger/src/actions/utils/middleware/log.js
@@ -15,17 +15,18 @@ const blacklist = [
   "OUT_OF_SCOPE_LOCATIONS",
   "MAP_SCOPES",
   "MAP_FRAMES",
   "ADD_SCOPES",
   "IN_SCOPE_LINES",
   "REMOVE_BREAKPOINT",
   "NODE_PROPERTIES_LOADED",
   "SET_FOCUSED_SOURCE_ITEM",
-  "NODE_EXPAND"
+  "NODE_EXPAND",
+  "IN_SCOPE_LINES"
 ];
 
 function cloneAction(action: any) {
   action = action || {};
   action = { ...action };
 
   // ADD_TAB, ...
   if (action.source && action.source.text) {
--- a/devtools/client/debugger/src/client/index.js
+++ b/devtools/client/debugger/src/client/index.js
@@ -54,47 +54,47 @@ function getClient(connection: any) {
   const {
     tab: { clientType }
   } = connection;
   return clientType == "firefox" ? firefox : chrome;
 }
 
 export async function onConnect(
   connection: Object,
-  sourceMaps: Object,
+  panelWorkers: Object,
   panel: Panel
 ) {
   // NOTE: the landing page does not connect to a JS process
   if (!connection) {
     return;
   }
 
   verifyPrefSchema();
 
   const client = getClient(connection);
   const commands = client.clientCommands;
 
   const initialState = await loadInitialState();
+  const workers = bootstrapWorkers(panelWorkers);
 
   const { store, actions, selectors } = bootstrapStore(
     commands,
-    sourceMaps,
+    workers,
     panel,
     initialState
   );
 
-  const workers = bootstrapWorkers();
   await client.onConnect(connection, actions);
 
   await syncBreakpoints();
   syncXHRBreakpoints();
   setupHelper({
     store,
     actions,
     selectors,
-    workers: { ...workers, sourceMaps },
+    workers,
     connection,
     client: client.clientCommands
   });
 
   bootstrapApp(store);
   return { store, actions, selectors, client: commands };
 }
--- a/devtools/client/debugger/src/main.js
+++ b/devtools/client/debugger/src/main.js
@@ -14,29 +14,29 @@ function unmountRoot() {
   ReactDOM.unmountComponentAtNode(mount);
 }
 
 module.exports = {
   bootstrap: ({
     threadClient,
     tabTarget,
     debuggerClient,
-    sourceMaps,
+    workers,
     panel
   }: any) =>
     onConnect(
       {
         tab: { clientType: "firefox" },
         tabConnection: {
           tabTarget,
           threadClient,
           debuggerClient
         }
       },
-      sourceMaps,
+      workers,
       panel
     ),
   destroy: () => {
     unmountRoot();
     sourceQueue.clear();
     teardownWorkers();
   }
 };
--- a/devtools/client/debugger/src/test/tests-setup.js
+++ b/devtools/client/debugger/src/test/tests-setup.js
@@ -18,22 +18,17 @@ import { prefs } from "../utils/prefs";
 
 import { startSourceMapWorker, stopSourceMapWorker } from "devtools-source-map";
 
 import {
   start as startPrettyPrintWorker,
   stop as stopPrettyPrintWorker
 } from "../workers/pretty-print";
 
-import {
-  start as startParserWorker,
-  stop as stopParserWorker,
-  clearSymbols,
-  clearASTs
-} from "../workers/parser";
+import { ParserDispatcher } from "../workers/parser";
 import {
   start as startSearchWorker,
   stop as stopSearchWorker
 } from "../workers/search";
 import { clearDocuments } from "../utils/editor";
 import { clearHistory } from "./utils/history";
 
 import env from "devtools-environment/test-flag";
@@ -63,42 +58,43 @@ global.URL = URL;
 global.indexedDB = mockIndexeddDB();
 
 Enzyme.configure({ adapter: new Adapter() });
 
 function formatException(reason, p) {
   console && console.log("Unhandled Rejection at:", p, "reason:", reason);
 }
 
+export const parserWorker = new ParserDispatcher();
+
 beforeAll(() => {
   startSourceMapWorker(
     path.join(rootPath, "node_modules/devtools-source-map/src/worker.js"),
     ""
   );
   startPrettyPrintWorker(
     path.join(rootPath, "src/workers/pretty-print/worker.js")
   );
-  startParserWorker(path.join(rootPath, "src/workers/parser/worker.js"));
+  parserWorker.start(path.join(rootPath, "src/workers/parser/worker.js"));
   startSearchWorker(path.join(rootPath, "src/workers/search/worker.js"));
   process.on("unhandledRejection", formatException);
 });
 
 afterAll(() => {
   stopSourceMapWorker();
   stopPrettyPrintWorker();
-  stopParserWorker();
+  parserWorker.stop();
   stopSearchWorker();
   process.removeListener("unhandledRejection", formatException);
 });
 
 afterEach(() => {});
 
 beforeEach(async () => {
-  clearASTs();
-  await clearSymbols();
+  parserWorker.clear();
   clearHistory();
   clearDocuments();
   prefs.projectDirectoryRoot = "";
 
   // Ensures window.dbg is there to track telemetry
   setupHelper({ selectors: {} });
 });
 
--- a/devtools/client/debugger/src/utils/bootstrap.js
+++ b/devtools/client/debugger/src/utils/bootstrap.js
@@ -11,85 +11,94 @@ const { Provider } = require("react-redu
 
 import { isFirefoxPanel, isDevelopment, isTesting } from "devtools-environment";
 import SourceMaps, {
   startSourceMapWorker,
   stopSourceMapWorker
 } from "devtools-source-map";
 import * as search from "../workers/search";
 import * as prettyPrint from "../workers/pretty-print";
-import * as parser from "../workers/parser";
+import { ParserDispatcher } from "../workers/parser";
 
 import configureStore from "../actions/utils/create-store";
 import reducers from "../reducers";
 import * as selectors from "../selectors";
 import App from "../components/App";
 import { asyncStore, prefs } from "./prefs";
 
 import type { Panel } from "../client/firefox/types";
 
+let parser;
+
 function renderPanel(component, store) {
   const root = document.createElement("div");
   root.className = "launchpad-root theme-body";
   root.style.setProperty("flex", "1");
   const mount = document.querySelector("#mount");
   if (!mount) {
     return;
   }
   mount.appendChild(root);
 
   ReactDOM.render(
     React.createElement(Provider, { store }, React.createElement(component)),
     root
   );
 }
 
+type Workers = {
+  sourceMaps: typeof SourceMaps,
+  evaluationsParser: typeof ParserDispatcher
+};
+
 export function bootstrapStore(
   client: any,
-  sourceMaps: typeof SourceMaps,
+  workers: Workers,
   panel: Panel,
   initialState: Object
 ) {
   const createStore = configureStore({
     log: prefs.logging || isTesting(),
     timing: isDevelopment(),
     makeThunkArgs: (args, state) => {
-      return { ...args, client, sourceMaps, panel };
+      return { ...args, client, ...workers, panel };
     }
   });
 
   const store = createStore(combineReducers(reducers), initialState);
   store.subscribe(() => updatePrefs(store.getState()));
 
   const actions = bindActionCreators(
     require("../actions").default,
     store.dispatch
   );
 
   return { store, actions, selectors };
 }
 
-export function bootstrapWorkers() {
+export function bootstrapWorkers(panelWorkers: Workers) {
   const workerPath = isDevelopment()
     ? "assets/build"
     : "resource://devtools/client/debugger/dist";
 
   if (isDevelopment()) {
     // When used in Firefox, the toolbox manages the source map worker.
     startSourceMapWorker(
       `${workerPath}/source-map-worker.js`,
       // This is relative to the worker itself.
       "./source-map-worker-assets/"
     );
   }
 
   prettyPrint.start(`${workerPath}/pretty-print-worker.js`);
+  parser = new ParserDispatcher();
+
   parser.start(`${workerPath}/parser-worker.js`);
   search.start(`${workerPath}/search-worker.js`);
-  return { prettyPrint, parser, search };
+  return { ...panelWorkers, prettyPrint, parser, search };
 }
 
 export function teardownWorkers() {
   if (!isFirefoxPanel()) {
     // When used in Firefox, the toolbox manages the source map worker.
     stopSourceMapWorker();
   }
   prettyPrint.stop();
--- a/devtools/client/debugger/src/utils/pause/mapScopes/index.js
+++ b/devtools/client/debugger/src/utils/pause/mapScopes/index.js
@@ -2,17 +2,16 @@
  * 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/>. */
 
 // @flow
 
 import typeof SourceMaps from "devtools-source-map";
 
 import {
-  getScopes,
   type SourceScope,
   type BindingData,
   type BindingLocation
 } from "../../../workers/parser";
 import type { RenderableScope } from "../scopes/getScope";
 import { locColumn } from "./locColumn";
 import {
   loadRangeMetadata,
@@ -32,16 +31,18 @@ import {
   type GeneratedBindingLocation
 } from "./buildGeneratedBindingList";
 import {
   originalRangeStartsInside,
   getApplicableBindingsForOriginalPosition
 } from "./getApplicableBindingsForOriginalPosition";
 
 import { log } from "../../log";
+import type { ThunkArgs } from "../../../actions/types";
+
 import type {
   PartialPosition,
   Frame,
   Scope,
   Source,
   SourceContent,
   BindingContents,
   ScopeBindings
@@ -49,26 +50,25 @@ import type {
 
 export type OriginalScope = RenderableScope;
 
 export async function buildMappedScopes(
   source: Source,
   content: SourceContent,
   frame: Frame,
   scopes: Scope,
-  sourceMaps: any,
-  client: any
+  { client, parser, sourceMaps }: ThunkArgs
 ): Promise<?{
   mappings: {
     [string]: string
   },
   scope: OriginalScope
 }> {
-  const originalAstScopes = await getScopes(frame.location);
-  const generatedAstScopes = await getScopes(frame.generatedLocation);
+  const originalAstScopes = await parser.getScopes(frame.location);
+  const generatedAstScopes = await parser.getScopes(frame.generatedLocation);
 
   if (!originalAstScopes || !generatedAstScopes) {
     return null;
   }
 
   const originalRanges = await loadRangeMetadata(
     source,
     frame,
--- a/devtools/client/debugger/src/utils/test-head.js
+++ b/devtools/client/debugger/src/utils/test-head.js
@@ -10,16 +10,17 @@
  */
 
 import { combineReducers } from "redux";
 import sourceMaps from "devtools-source-map";
 import reducers from "../reducers";
 import actions from "../actions";
 import * as selectors from "../selectors";
 import { getHistory } from "../test/utils/history";
+import { parserWorker } from "../test/tests-setup";
 import configureStore from "../actions/utils/create-store";
 import sourceQueue from "../utils/source-queue";
 import type { Source, OriginalSourceData, GeneratedSourceData } from "../types";
 
 /**
  * This file contains older interfaces used by tests that have not been
  * converted to use test-mockup.js
  */
@@ -36,17 +37,18 @@ function createStore(client: any, initia
 
   const store = configureStore({
     log: false,
     history: getHistory(),
     makeThunkArgs: args => {
       return {
         ...args,
         client,
-        sourceMaps: sourceMapsMock !== undefined ? sourceMapsMock : sourceMaps
+        sourceMaps: sourceMapsMock !== undefined ? sourceMapsMock : sourceMaps,
+        parser: parserWorker
       };
     }
   })(combineReducers(reducers), initialState);
   sourceQueue.clear();
   sourceQueue.initialize({
     newQueuedSources: sources =>
       store.dispatch(actions.newQueuedSources(sources))
   });
--- a/devtools/client/debugger/src/workers/parser/index.js
+++ b/devtools/client/debugger/src/workers/parser/index.js
@@ -7,87 +7,81 @@
 import { workerUtils } from "devtools-utils";
 const { WorkerDispatcher } = workerUtils;
 
 import type { AstSource, AstLocation, AstPosition } from "./types";
 import type { SourceLocation, SourceId, SourceContent } from "../../types";
 import type { SourceScope } from "./getScopes/visitor";
 import type { SymbolDeclarations } from "./getSymbols";
 
-const dispatcher = new WorkerDispatcher();
-export const start = (url: string, win: any = window) =>
-  dispatcher.start(url, win);
-export const stop = () => dispatcher.stop();
-
-export const findOutOfScopeLocations = async (
-  sourceId: string,
-  position: AstPosition
-): Promise<AstLocation[]> =>
-  dispatcher.invoke("findOutOfScopeLocations", sourceId, position);
-
-export const getNextStep = async (
-  sourceId: SourceId,
-  pausedPosition: AstPosition
-): Promise<?SourceLocation> =>
-  dispatcher.invoke("getNextStep", sourceId, pausedPosition);
+export class ParserDispatcher extends WorkerDispatcher {
+  async findOutOfScopeLocations(
+    sourceId: string,
+    position: AstPosition
+  ): Promise<AstLocation[]> {
+    return this.invoke("findOutOfScopeLocations", sourceId, position);
+  }
 
-export const clearASTs = async (): Promise<void> =>
-  dispatcher.invoke("clearASTs");
-
-export const getScopes = async (
-  location: SourceLocation
-): Promise<SourceScope[]> => dispatcher.invoke("getScopes", location);
+  async getNextStep(
+    sourceId: SourceId,
+    pausedPosition: AstPosition
+  ): Promise<?SourceLocation> {
+    return this.invoke("getNextStep", sourceId, pausedPosition);
+  }
 
-export const clearScopes = async (): Promise<void> =>
-  dispatcher.invoke("clearScopes");
+  async clearState(): Promise<void> {
+    return this.invoke("clearState");
+  }
 
-export const clearSymbols = async (): Promise<void> =>
-  dispatcher.invoke("clearSymbols");
+  async getScopes(location: SourceLocation): Promise<SourceScope[]> {
+    return this.invoke("getScopes", location);
+  }
 
-export const getSymbols = async (
-  sourceId: string
-): Promise<SymbolDeclarations> => dispatcher.invoke("getSymbols", sourceId);
+  async getSymbols(sourceId: string): Promise<SymbolDeclarations> {
+    return this.invoke("getSymbols", sourceId);
+  }
 
-export const setSource = async (
-  sourceId: SourceId,
-  content: SourceContent
-): Promise<void> => {
-  const astSource: AstSource = {
-    id: sourceId,
-    text: content.type === "wasm" ? "" : content.value,
-    contentType: content.contentType || null,
-    isWasm: content.type === "wasm"
-  };
+  async setSource(sourceId: SourceId, content: SourceContent): Promise<void> {
+    const astSource: AstSource = {
+      id: sourceId,
+      text: content.type === "wasm" ? "" : content.value,
+      contentType: content.contentType || null,
+      isWasm: content.type === "wasm"
+    };
 
-  await dispatcher.invoke("setSource", astSource);
-};
+    return this.invoke("setSource", astSource);
+  }
 
-export const clearSources = async (): Promise<void> =>
-  dispatcher.invoke("clearSources");
+  async hasSyntaxError(input: string): Promise<string | false> {
+    return this.invoke("hasSyntaxError", input);
+  }
 
-export const hasSyntaxError = async (input: string): Promise<string | false> =>
-  dispatcher.invoke("hasSyntaxError", input);
+  async mapExpression(
+    expression: string,
+    mappings: {
+      [string]: string | null
+    } | null,
+    bindings: string[],
+    shouldMapBindings?: boolean,
+    shouldMapAwait?: boolean
+  ): Promise<{ expression: string }> {
+    return this.invoke(
+      "mapExpression",
+      expression,
+      mappings,
+      bindings,
+      shouldMapBindings,
+      shouldMapAwait
+    );
+  }
 
-export const mapExpression = async (
-  expression: string,
-  mappings: {
-    [string]: string | null
-  } | null,
-  bindings: string[],
-  shouldMapBindings?: boolean,
-  shouldMapAwait?: boolean
-): Promise<{ expression: string }> =>
-  dispatcher.invoke(
-    "mapExpression",
-    expression,
-    mappings,
-    bindings,
-    shouldMapBindings,
-    shouldMapAwait
-  );
+  async clear() {
+    await this.clearState();
+  }
+}
 
 export type {
   SourceScope,
   BindingData,
   BindingLocation,
   BindingLocationType,
   BindingDeclarationLocation,
   BindingMetaValue,
--- a/devtools/client/debugger/src/workers/parser/worker.js
+++ b/devtools/client/debugger/src/workers/parser/worker.js
@@ -11,21 +11,25 @@ import { setSource, clearSources } from 
 import findOutOfScopeLocations from "./findOutOfScopeLocations";
 import { getNextStep } from "./steps";
 import { hasSyntaxError } from "./validate";
 import mapExpression from "./mapExpression";
 
 import { workerUtils } from "devtools-utils";
 const { workerHandler } = workerUtils;
 
+function clearState() {
+  clearASTs();
+  clearScopes();
+  clearSources();
+  clearSymbols();
+}
+
 self.onmessage = workerHandler({
   findOutOfScopeLocations,
   getSymbols,
   getScopes,
-  clearSymbols,
-  clearScopes,
-  clearASTs,
-  setSource,
-  clearSources,
+  clearState,
   getNextStep,
   hasSyntaxError,
-  mapExpression
+  mapExpression,
+  setSource
 });
--- a/devtools/client/debugger/test/mochitest/helpers.js
+++ b/devtools/client/debugger/test/mochitest/helpers.js
@@ -1055,18 +1055,18 @@ const keyMappings = {
   quickOpen: { code: "p", modifiers: cmdOrCtrl },
   quickOpenFunc: { code: "o", modifiers: cmdShift },
   quickOpenLine: { code: ":", modifiers: cmdOrCtrl },
   fileSearch: { code: "f", modifiers: cmdOrCtrl },
   fileSearchNext: { code: "g", modifiers: cmdOrCtrl },
   fileSearchPrev: { code: "g", modifiers: cmdShift },
   Enter: { code: "VK_RETURN" },
   ShiftEnter: { code: "VK_RETURN", modifiers: shiftOrAlt },
-  AltEnter: { 
-    code: "VK_RETURN", 
+  AltEnter: {
+    code: "VK_RETURN",
     modifiers: { altKey: true }
   },
   Up: { code: "VK_UP" },
   Down: { code: "VK_DOWN" },
   Right: { code: "VK_RIGHT" },
   Left: { code: "VK_LEFT" },
   End: endKey,
   Start: startKey,
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -932,16 +932,34 @@ Toolbox.prototype = {
    * A common access point for the client-side mapping service for source maps that
    * any panel can use.  This is a "low-level" API that connects to
    * the source map worker.
    */
   get sourceMapService() {
     return this._createSourceMapService();
   },
 
+    /**
+   * A common access point for the client-side parser service that any panel can use.
+   */
+  get parserService() {
+    if (this._parserService) {
+      return this._parserService;
+    }
+
+    const { ParserDispatcher } =
+      require("devtools/client/debugger/src/workers/parser/index");
+
+    this._parserService = new ParserDispatcher();
+    this._parserService.start(
+      "resource://devtools/client/debugger/dist/parser-worker.js",
+      this.win);
+    return this._parserService;
+  },
+
   /**
    * Clients wishing to use source maps but that want the toolbox to
    * track the source and style sheet actor mapping can use this
    * source map service.  This is a higher-level service than the one
    * returned by |sourceMapService|, in that it automatically tracks
    * source and style sheet actor IDs.
    */
   get sourceMapURLService() {
@@ -3069,21 +3087,27 @@ Toolbox.prototype = {
     this.telemetry.toolClosed(this.currentToolId, this.sessionId, this);
 
     this._lastFocusedElement = null;
 
     if (this._sourceMapURLService) {
       this._sourceMapURLService.destroy();
       this._sourceMapURLService = null;
     }
+
     if (this._sourceMapService) {
       this._sourceMapService.stopSourceMapWorker();
       this._sourceMapService = null;
     }
 
+    if (this._parserService) {
+      this._parserService.stop();
+      this._parserService = null;
+    }
+
     if (this.webconsolePanel) {
       this._saveSplitConsoleHeight();
       this.webconsolePanel.removeEventListener("resize",
         this._saveSplitConsoleHeight);
       this.webconsolePanel = null;
     }
     if (this._componentMount) {
       this._componentMount.removeEventListener("keypress", this._onToolbarArrowKeypress);
--- a/devtools/client/webconsole/webconsole.js
+++ b/devtools/client/webconsole/webconsole.js
@@ -296,27 +296,25 @@ class WebConsole {
       const res = this.parserService.mapExpression(
         expression, null, null, shouldMapBindings, shouldMapAwait);
       return res;
     }
 
     return null;
   }
 
-  /**
-   * A common access point for the client-side parser service that any panel can use.
-   */
   get parserService() {
     if (this._parserService) {
       return this._parserService;
     }
 
-    this._parserService =
+    const { ParserDispatcher } =
       require("devtools/client/debugger/src/workers/parser/index");
 
+    this._parserService = new ParserDispatcher();
     this._parserService.start(
       "resource://devtools/client/debugger/dist/parser-worker.js",
       this.chromeUtilsWindow);
     return this._parserService;
   }
 
   /**
    * Retrieves the current selection from the Inspector, if such a selection