Bug 1539493 - memoize setSymbols call. r=loganfsmyth
authorJason Laster <jlaster@mozilla.com>
Tue, 02 Apr 2019 16:02:34 +0000
changeset 467631 196f7c24339ea15a3129ce2197ee7d6bbbbc10ad
parent 467630 09b6d709b2da40d15d257ce70bc63b0e6943a178
child 467632 d980e312439f112b41af18b4365b5e253fc90c39
push id35806
push userrgurzau@mozilla.com
push dateWed, 03 Apr 2019 04:07:39 +0000
treeherdermozilla-central@45808ab18609 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersloganfsmyth
bugs1539493
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 1539493 - memoize setSymbols call. r=loganfsmyth Differential Revision: https://phabricator.services.mozilla.com/D25307
devtools/client/debugger/new/bin/module-manifest.json
devtools/client/debugger/new/packages/devtools-source-map/src/utils/index.js
devtools/client/debugger/new/src/actions/breakpoints/breakpointPositions.js
devtools/client/debugger/new/src/actions/breakpoints/modify.js
devtools/client/debugger/new/src/actions/breakpoints/syncBreakpoint.js
devtools/client/debugger/new/src/actions/breakpoints/tests/breakpointPositions.spec.js
devtools/client/debugger/new/src/actions/breakpoints/tests/breakpoints.spec.js
devtools/client/debugger/new/src/actions/pause/mapFrames.js
devtools/client/debugger/new/src/actions/pause/mapScopes.js
devtools/client/debugger/new/src/actions/pause/tests/pause.spec.js
devtools/client/debugger/new/src/actions/project-text-search.js
devtools/client/debugger/new/src/actions/sources/loadSourceText.js
devtools/client/debugger/new/src/actions/sources/newSources.js
devtools/client/debugger/new/src/actions/sources/prettyPrint.js
devtools/client/debugger/new/src/actions/sources/select.js
devtools/client/debugger/new/src/actions/sources/symbols.js
devtools/client/debugger/new/src/actions/sources/tests/loadSource.spec.js
devtools/client/debugger/new/src/actions/tests/ast.spec.js
devtools/client/debugger/new/src/actions/tests/expressions.spec.js
devtools/client/debugger/new/src/actions/tests/pending-breakpoints.spec.js
devtools/client/debugger/new/src/actions/tests/project-text-search.spec.js
devtools/client/debugger/new/src/components/Editor/DebugLine.js
devtools/client/debugger/new/src/components/Editor/index.js
devtools/client/debugger/new/src/reducers/ast.js
devtools/client/debugger/new/src/reducers/sources.js
devtools/client/debugger/new/src/reducers/types.js
devtools/client/debugger/new/src/selectors/visibleColumnBreakpoints.js
devtools/client/debugger/new/src/utils/breakpoint/astBreakpointLocation.js
devtools/client/debugger/new/src/utils/memoizableAction.js
devtools/client/debugger/new/src/utils/moz.build
devtools/client/debugger/new/test/mochitest/browser_dbg-debugger-buttons.js
devtools/client/debugger/new/test/mochitest/browser_dbg-pretty-print-paused.js
devtools/client/debugger/new/test/mochitest/browser_dbg-xhr-run-to-completion.js
devtools/client/debugger/new/test/mochitest/helpers.js
devtools/client/shared/source-map/index.js
devtools/client/shared/source-map/worker.js
--- a/devtools/client/debugger/new/bin/module-manifest.json
+++ b/devtools/client/debugger/new/bin/module-manifest.json
@@ -10,17 +10,17 @@
           "0": 0,
           "1": 1
         }
       },
       "chunks": {
         "byName": {},
         "byBlocks": {},
         "usedIds": {
-          "1": 1
+          "0": 0
         }
       }
     }
   ],
   "extract-text-webpack-plugin ../../extract-text-webpack-plugin/dist ../../css-loader/index.js??ref--3-1!../../postcss-loader/lib/index.js!../../react-aria-components/src/tabs/tab.css": [
     {
       "modules": {
         "byIdentifier": {
@@ -31,17 +31,17 @@
           "0": 0,
           "1": 1
         }
       },
       "chunks": {
         "byName": {},
         "byBlocks": {},
         "usedIds": {
-          "1": 1
+          "0": 0
         }
       }
     }
   ],
   "extract-text-webpack-plugin ../../extract-text-webpack-plugin/dist ../../css-loader/index.js??ref--3-1!../../postcss-loader/lib/index.js!../../react-aria-components/src/tabs/tab-list.css": [
     {
       "modules": {
         "byIdentifier": {
@@ -52,17 +52,17 @@
           "0": 0,
           "1": 1
         }
       },
       "chunks": {
         "byName": {},
         "byBlocks": {},
         "usedIds": {
-          "1": 1
+          "0": 0
         }
       }
     }
   ],
   "extract-text-webpack-plugin ../../extract-text-webpack-plugin/dist ../../css-loader/index.js??ref--3-1!../../postcss-loader/lib/index.js!../../devtools-contextmenu/menu.css": [
     {
       "modules": {
         "byIdentifier": {
@@ -73,17 +73,17 @@
           "0": 0,
           "1": 1
         }
       },
       "chunks": {
         "byName": {},
         "byBlocks": {},
         "usedIds": {
-          "1": 1
+          "0": 0
         }
       }
     }
   ],
   "extract-text-webpack-plugin ../../extract-text-webpack-plugin/dist ../../css-loader/index.js??ref--3-1!../../postcss-loader/lib/index.js!../../../packages/devtools-components/src/tree.css": [
     {
       "modules": {
         "byIdentifier": {
@@ -94,17 +94,17 @@
           "0": 0,
           "1": 1
         }
       },
       "chunks": {
         "byName": {},
         "byBlocks": {},
         "usedIds": {
-          "1": 1
+          "0": 0
         }
       }
     }
   ],
   "extract-text-webpack-plugin ../../extract-text-webpack-plugin/dist ../../css-loader/index.js??ref--3-1!../../postcss-loader/lib/index.js!../../../packages/devtools-reps/src/object-inspector/components/ObjectInspector.css": [
     {
       "modules": {
         "byIdentifier": {
@@ -115,17 +115,17 @@
           "0": 0,
           "1": 1
         }
       },
       "chunks": {
         "byName": {},
         "byBlocks": {},
         "usedIds": {
-          "1": 1
+          "0": 0
         }
       }
     }
   ],
   "extract-text-webpack-plugin ../../extract-text-webpack-plugin/dist ../../css-loader/index.js??ref--3-1!../../postcss-loader/lib/index.js!../../../packages/devtools-reps/src/reps/reps.css": [
     {
       "modules": {
         "byIdentifier": {
@@ -136,17 +136,17 @@
           "0": 0,
           "1": 1
         }
       },
       "chunks": {
         "byName": {},
         "byBlocks": {},
         "usedIds": {
-          "1": 1
+          "0": 0
         }
       }
     }
   ],
   "modules": {
     "byIdentifier": {
       "external \"devtools/client/shared/vendor/react-prop-types\"": 0,
       "external \"devtools/client/shared/vendor/react-dom-factories\"": 1,
--- a/devtools/client/debugger/new/packages/devtools-source-map/src/utils/index.js
+++ b/devtools/client/debugger/new/packages/devtools-source-map/src/utils/index.js
@@ -1,18 +1,22 @@
 /* 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/>. */
 
 // @flow
 
 const md5 = require("md5");
 
-function originalToGeneratedId(originalId: string) {
-  const match = originalId.match(/(.*)\/originalSource/);
+function originalToGeneratedId(sourceId: string) {
+  if (isGeneratedId(sourceId)) {
+    return sourceId;
+  }
+
+  const match = sourceId.match(/(.*)\/originalSource/);
   return match ? match[1] : "";
 }
 
 function generatedToOriginalId(generatedId: string, url: string) {
   return `${generatedId}/originalSource-${md5(url)}`;
 }
 
 function isOriginalId(id: string) {
--- a/devtools/client/debugger/new/src/actions/breakpoints/breakpointPositions.js
+++ b/devtools/client/debugger/new/src/actions/breakpoints/breakpointPositions.js
@@ -5,26 +5,35 @@
 // @flow
 
 import { isOriginalId, originalToGeneratedId } from "devtools-source-map";
 import { uniqBy, zip } from "lodash";
 
 import {
   getSource,
   getSourceFromId,
+  getGeneratedSourceById,
   hasBreakpointPositions,
   getBreakpointPositionsForSource
 } from "../../selectors";
 
-import type { MappedLocation, SourceLocation } from "../../types";
-import type { ThunkArgs } from "../../actions/types";
+import type {
+  MappedLocation,
+  SourceLocation,
+  BreakpointPositions
+} from "../../types";
 import { makeBreakpointId } from "../../utils/breakpoint";
+import {
+  memoizeableAction,
+  type MemoizedAction
+} from "../../utils/memoizableAction";
+
 import typeof SourceMaps from "../../../packages/devtools-source-map/src";
 
-const requests = new Map();
+// const requests = new Map();
 
 async function mapLocations(
   generatedLocations: SourceLocation[],
   { sourceMaps }: { sourceMaps: SourceMaps }
 ) {
   const originalLocations = await sourceMaps.getOriginalLocations(
     generatedLocations
   );
@@ -108,60 +117,34 @@ async function _setBreakpointPositions(s
   positions = filterBySource(positions, sourceId);
   positions = filterByUniqLocation(positions);
 
   const source = getSource(getState(), sourceId);
   // NOTE: it's possible that the source was removed during a navigate
   if (!source) {
     return;
   }
+
   dispatch({
     type: "ADD_BREAKPOINT_POSITIONS",
     source: source,
     positions
   });
-}
 
-function buildCacheKey(sourceId: string, thunkArgs: ThunkArgs): string {
-  const generatedSource = getSource(
-    thunkArgs.getState(),
-    isOriginalId(sourceId) ? originalToGeneratedId(sourceId) : sourceId
-  );
-
-  let key = sourceId;
-
-  if (generatedSource) {
-    for (const actor of generatedSource.actors) {
-      key += `:${actor.actor}`;
-    }
-  }
-  return key;
+  return positions;
 }
 
-export function setBreakpointPositions(sourceId: string) {
-  return async (thunkArgs: ThunkArgs) => {
-    const { getState } = thunkArgs;
-    if (hasBreakpointPositions(getState(), sourceId)) {
-      return getBreakpointPositionsForSource(getState(), sourceId);
-    }
-
-    const cacheKey = buildCacheKey(sourceId, thunkArgs);
-
-    if (!requests.has(cacheKey)) {
-      requests.set(
-        cacheKey,
-        (async () => {
-          try {
-            await _setBreakpointPositions(sourceId, thunkArgs);
-          } catch (e) {
-            // TODO: Address exceptions originating from 1536618
-            // `Debugger.Source belongs to a different Debugger`
-          } finally {
-            requests.delete(cacheKey);
-          }
-        })()
-      );
-    }
-
-    await requests.get(cacheKey);
-    return getBreakpointPositionsForSource(getState(), sourceId);
-  };
-}
+export const setBreakpointPositions: MemoizedAction<
+  { sourceId: string },
+  ?BreakpointPositions
+> = memoizeableAction("setBreakpointPositions", {
+  hasValue: ({ sourceId }, { getState }) =>
+    hasBreakpointPositions(getState(), sourceId),
+  getValue: ({ sourceId }, { getState }) =>
+    getBreakpointPositionsForSource(getState(), sourceId),
+  createKey({ sourceId }, { getState }) {
+    const generatedSource = getGeneratedSourceById(getState(), sourceId);
+    const actors = generatedSource.actors.map(({ actor }) => actor);
+    return [sourceId, ...actors].join(":");
+  },
+  action: ({ sourceId }, thunkArgs) =>
+    _setBreakpointPositions(sourceId, thunkArgs)
+});
--- a/devtools/client/debugger/new/src/actions/breakpoints/modify.js
+++ b/devtools/client/debugger/new/src/actions/breakpoints/modify.js
@@ -1,42 +1,42 @@
 /* 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/>. */
 
 // @flow
 
-import type { ThunkArgs } from "../types";
-import type {
-  Breakpoint,
-  BreakpointOptions,
-  SourceLocation
-} from "../../types";
-
 import {
   makeBreakpointLocation,
   makeBreakpointId,
   getASTLocation
 } from "../../utils/breakpoint";
 
 import { getTextAtPosition } from "../../utils/source";
 
 import {
   getBreakpoint,
   getBreakpointPositionsForLocation,
   getFirstBreakpointPosition,
-  getSourceFromId,
   getSymbols
 } from "../../selectors";
 
-import { loadSourceText } from "../sources/loadSourceText";
+import { loadSourceById } from "../sources/loadSourceText";
 import { setBreakpointPositions } from "./breakpointPositions";
 
 import { recordEvent } from "../../utils/telemetry";
 
+import type { ThunkArgs } from "../types";
+import type {
+  Breakpoint,
+  BreakpointOptions,
+  BreakpointPosition,
+  SourceLocation
+} from "../../types";
+
 // This file has the primitive operations used to modify individual breakpoints
 // and keep them in sync with the breakpoints installed on server threads. These
 // are collected here to make it easier to preserve the following invariant:
 //
 // Breakpoints are included in reducer state iff they are disabled or requests
 // have been dispatched to set them in all server threads.
 //
 // To maintain this property, updates to the reducer and installed breakpoints
@@ -97,41 +97,33 @@ export function addBreakpoint(
   options: BreakpointOptions = {},
   disabled: boolean = false
 ) {
   return async ({ dispatch, getState, sourceMaps, client }: ThunkArgs) => {
     recordEvent("add_breakpoint");
 
     const { sourceId, column } = initialLocation;
 
-    await dispatch(setBreakpointPositions(sourceId));
+    await dispatch(setBreakpointPositions({ sourceId }));
 
-    const position = column
+    const position: ?BreakpointPosition = column
       ? getBreakpointPositionsForLocation(getState(), initialLocation)
       : getFirstBreakpointPosition(getState(), initialLocation);
 
     if (!position) {
       return;
     }
 
     const { location, generatedLocation } = position;
-
     // Both the original and generated sources must be loaded to get the
     // breakpoint's text.
-    await dispatch(
-      loadSourceText(getSourceFromId(getState(), location.sourceId))
-    );
-    await dispatch(
-      loadSourceText(getSourceFromId(getState(), generatedLocation.sourceId))
-    );
 
-    const source = getSourceFromId(getState(), location.sourceId);
-    const generatedSource = getSourceFromId(
-      getState(),
-      generatedLocation.sourceId
+    const source = await dispatch(loadSourceById(sourceId));
+    const generatedSource = await dispatch(
+      loadSourceById(generatedLocation.sourceId)
     );
 
     const symbols = getSymbols(getState(), source);
     const astLocation = await getASTLocation(source, symbols, location);
 
     const originalText = getTextAtPosition(source, location);
     const text = getTextAtPosition(generatedSource, generatedLocation);
 
@@ -151,26 +143,20 @@ export function addBreakpoint(
     // Because a generated location cannot map to multiple original locations,
     // the only breakpoints that can map to this generated location have the
     // new breakpoint's |location| or |generatedLocation| as their own
     // |location|. We will overwrite any breakpoint at |location| with the
     // SET_BREAKPOINT action below, but need to manually remove any breakpoint
     // at |generatedLocation|.
     const generatedId = makeBreakpointId(breakpoint.generatedLocation);
     if (id != generatedId && getBreakpoint(getState(), generatedLocation)) {
-      dispatch({
-        type: "REMOVE_BREAKPOINT",
-        location: generatedLocation
-      });
+      dispatch({ type: "REMOVE_BREAKPOINT", location: generatedLocation });
     }
 
-    dispatch({
-      type: "SET_BREAKPOINT",
-      breakpoint
-    });
+    dispatch({ type: "SET_BREAKPOINT", breakpoint });
 
     if (disabled) {
       // If we just clobbered an enabled breakpoint with a disabled one, we need
       // to remove any installed breakpoint in the server.
       return dispatch(clientRemoveBreakpoint(breakpoint));
     }
 
     return dispatch(clientSetBreakpoint(breakpoint));
--- a/devtools/client/debugger/new/src/actions/breakpoints/syncBreakpoint.js
+++ b/devtools/client/debugger/new/src/actions/breakpoints/syncBreakpoint.js
@@ -1,52 +1,62 @@
 /* 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/>. */
 
 // @flow
 
 import { setBreakpointPositions } from "./breakpointPositions";
+import { setSymbols } from "../sources/symbols";
 import {
   assertPendingBreakpoint,
   findFunctionByName,
   findPosition,
   makeBreakpointLocation
 } from "../../utils/breakpoint";
 
 import { comparePosition, createLocation } from "../../utils/location";
 
 import { originalToGeneratedId, isOriginalId } from "devtools-source-map";
 import { getSource, getBreakpoint } from "../../selectors";
 import { removeBreakpoint, addBreakpoint } from ".";
 
 import type { ThunkArgs } from "../types";
+import type { LoadedSymbols } from "../../reducers/types";
 
 import type {
   SourceLocation,
   ASTLocation,
   PendingBreakpoint,
-  SourceId
+  SourceId,
+  BreakpointPositions
 } from "../../types";
 
 async function findBreakpointPosition(
   { getState, dispatch },
   location: SourceLocation
 ) {
-  const positions = await dispatch(setBreakpointPositions(location.sourceId));
+  const positions: BreakpointPositions = await dispatch(
+    setBreakpointPositions({ sourceId: location.sourceId })
+  );
+
   const position = findPosition(positions, location);
   return position && position.generatedLocation;
 }
 
 async function findNewLocation(
   { name, offset, index }: ASTLocation,
   location: SourceLocation,
-  source
+  source,
+  thunkArgs
 ) {
-  const func = await findFunctionByName(source, name, index);
+  const symbols: LoadedSymbols = await thunkArgs.dispatch(
+    setSymbols({ source })
+  );
+  const func = findFunctionByName(symbols, name, index);
 
   // Fallback onto the location line, if we do not find a function is not found
   let line = location.line;
   if (func) {
     line = func.location.start.line + offset.line;
   }
 
   return {
@@ -128,17 +138,18 @@ export function syncBreakpoint(
       );
     }
 
     const previousLocation = { ...location, sourceId };
 
     const newLocation = await findNewLocation(
       astLocation,
       previousLocation,
-      source
+      source,
+      thunkArgs
     );
 
     const newGeneratedLocation = await findBreakpointPosition(
       thunkArgs,
       newLocation
     );
 
     if (!newGeneratedLocation) {
--- a/devtools/client/debugger/new/src/actions/breakpoints/tests/breakpointPositions.spec.js
+++ b/devtools/client/debugger/new/src/actions/breakpoints/tests/breakpointPositions.spec.js
@@ -16,17 +16,17 @@ describe("breakpointPositions", () => {
   it("fetches positions", async () => {
     const store = createStore({
       getBreakpointPositions: async () => ({ "9": [1] })
     });
 
     const { dispatch, getState } = store;
     await dispatch(actions.newSource(makeSource("foo")));
 
-    dispatch(actions.setBreakpointPositions("foo"));
+    dispatch(actions.setBreakpointPositions({ sourceId: "foo" }));
 
     await waitForState(store, state =>
       selectors.hasBreakpointPositions(state, "foo")
     );
 
     expect(
       selectors.getBreakpointPositionsForSource(getState(), "foo")
     ).toEqual([
@@ -56,18 +56,18 @@ describe("breakpointPositions", () => {
           count++;
           resolve = r;
         })
     });
 
     const { dispatch, getState } = store;
     await dispatch(actions.newSource(makeSource("foo")));
 
-    dispatch(actions.setBreakpointPositions("foo"));
-    dispatch(actions.setBreakpointPositions("foo"));
+    dispatch(actions.setBreakpointPositions({ sourceId: "foo" }));
+    dispatch(actions.setBreakpointPositions({ sourceId: "foo" }));
 
     resolve({ "9": [1] });
     await waitForState(store, state =>
       selectors.hasBreakpointPositions(state, "foo")
     );
 
     expect(
       selectors.getBreakpointPositionsForSource(getState(), "foo")
--- a/devtools/client/debugger/new/src/actions/breakpoints/tests/breakpoints.spec.js
+++ b/devtools/client/debugger/new/src/actions/breakpoints/tests/breakpoints.spec.js
@@ -28,17 +28,17 @@ describe("breakpoints", () => {
       sourceId: "a",
       line: 2,
       column: 1,
       sourceUrl: "http://localhost:8000/examples/a"
     };
 
     const source = makeSource("a");
     await dispatch(actions.newSource(source));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
     await dispatch(
       actions.setSelectedLocation(source, {
         line: 1,
         column: 1,
         sourceId: source.id
       })
     );
 
@@ -58,17 +58,17 @@ describe("breakpoints", () => {
     const loc1 = {
       sourceId: "a",
       line: 5,
       column: 1,
       sourceUrl: "http://localhost:8000/examples/a"
     };
     const source = makeSource("a");
     await dispatch(actions.newSource(source));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
     await dispatch(
       actions.setSelectedLocation(source, {
         line: 1,
         column: 1,
         sourceId: source.id
       })
     );
 
@@ -85,17 +85,17 @@ describe("breakpoints", () => {
     const loc1 = {
       sourceId: "a",
       line: 5,
       column: 1,
       sourceUrl: "http://localhost:8000/examples/a"
     };
     const source = makeSource("a");
     await dispatch(actions.newSource(source));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
     await dispatch(
       actions.setSelectedLocation(source, {
         line: 1,
         column: 1,
         sourceId: source.id
       })
     );
 
@@ -119,17 +119,17 @@ describe("breakpoints", () => {
       sourceId: "a",
       line: 5,
       column: 1,
       sourceUrl: "http://localhost:8000/examples/a"
     };
 
     const source = makeSource("a");
     await dispatch(actions.newSource(source));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
     await dispatch(
       actions.setSelectedLocation(source, {
         line: 1,
         column: 1,
         sourceId: source.id
       })
     );
 
@@ -158,21 +158,21 @@ describe("breakpoints", () => {
       sourceId: "b",
       line: 6,
       column: 2,
       sourceUrl: "http://localhost:8000/examples/b"
     };
 
     const aSource = makeSource("a");
     await dispatch(actions.newSource(aSource));
-    await dispatch(actions.loadSourceText(aSource));
+    await dispatch(actions.loadSourceText({ source: aSource }));
 
     const bSource = makeSource("b");
     await dispatch(actions.newSource(bSource));
-    await dispatch(actions.loadSourceText(bSource));
+    await dispatch(actions.loadSourceText({ source: bSource }));
 
     await dispatch(
       actions.setSelectedLocation(aSource, {
         line: 1,
         column: 1,
         sourceId: aSource.id
       })
     );
@@ -205,21 +205,21 @@ describe("breakpoints", () => {
       sourceId: "b",
       line: 6,
       column: 2,
       sourceUrl: "http://localhost:8000/examples/b"
     };
 
     const aSource = makeSource("a");
     await dispatch(actions.newSource(aSource));
-    await dispatch(actions.loadSourceText(aSource));
+    await dispatch(actions.loadSourceText({ source: aSource }));
 
     const bSource = makeSource("b");
     await dispatch(actions.newSource(bSource));
-    await dispatch(actions.loadSourceText(bSource));
+    await dispatch(actions.loadSourceText({ source: bSource }));
 
     await dispatch(actions.addBreakpoint(loc1));
     await dispatch(actions.addBreakpoint(loc2));
 
     const breakpoint = selectors.getBreakpoint(getState(), loc1);
     if (!breakpoint) {
       throw new Error("no breakpoint");
     }
@@ -238,17 +238,17 @@ describe("breakpoints", () => {
       sourceId: "a",
       line: 5,
       column: 1,
       sourceUrl: "http://localhost:8000/examples/a"
     };
 
     const aSource = makeSource("a");
     await dispatch(actions.newSource(aSource));
-    await dispatch(actions.loadSourceText(aSource));
+    await dispatch(actions.loadSourceText({ source: aSource }));
 
     await dispatch(actions.addBreakpoint(loc));
     let bp = selectors.getBreakpoint(getState(), loc);
     if (!bp) {
       throw new Error("no breakpoint");
     }
 
     await dispatch(actions.disableBreakpoint(bp));
@@ -282,21 +282,21 @@ describe("breakpoints", () => {
       sourceId: "b",
       line: 6,
       column: 2,
       sourceUrl: "http://localhost:8000/examples/b"
     };
 
     const aSource = makeSource("a");
     await dispatch(actions.newSource(aSource));
-    await dispatch(actions.loadSourceText(aSource));
+    await dispatch(actions.loadSourceText({ source: aSource }));
 
     const bSource = makeSource("b");
     await dispatch(actions.newSource(bSource));
-    await dispatch(actions.loadSourceText(bSource));
+    await dispatch(actions.loadSourceText({ source: bSource }));
 
     await dispatch(actions.addBreakpoint(loc1));
     await dispatch(actions.addBreakpoint(loc2));
 
     await dispatch(actions.toggleAllBreakpoints(true));
 
     let bp1 = selectors.getBreakpoint(getState(), loc1);
     let bp2 = selectors.getBreakpoint(getState(), loc2);
@@ -315,17 +315,17 @@ describe("breakpoints", () => {
   it("should toggle a breakpoint at a location", async () => {
     const loc = { sourceId: "foo1", line: 5, column: 1 };
     const getBp = () => selectors.getBreakpoint(getState(), loc);
 
     const { dispatch, getState } = createStore(mockClient({ "5": [1] }));
 
     const source = makeSource("foo1");
     await dispatch(actions.newSource(source));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
 
     await dispatch(actions.selectLocation(loc));
 
     await dispatch(actions.toggleBreakpointAtLine(5));
     const bp = getBp();
     expect(bp && !bp.disabled).toBe(true);
 
     await dispatch(actions.toggleBreakpointAtLine(5));
@@ -335,17 +335,17 @@ describe("breakpoints", () => {
   it("should disable/enable a breakpoint at a location", async () => {
     const location = { sourceId: "foo1", line: 5, column: 1 };
     const getBp = () => selectors.getBreakpoint(getState(), location);
 
     const { dispatch, getState } = createStore(mockClient({ "5": [1] }));
 
     const source = makeSource("foo1");
     await dispatch(actions.newSource(source));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
 
     await dispatch(actions.selectLocation({ sourceId: "foo1", line: 1 }));
 
     await dispatch(actions.toggleBreakpointAtLine(5));
     let bp = getBp();
     expect(bp && !bp.disabled).toBe(true);
     bp = getBp();
     if (!bp) {
@@ -363,17 +363,17 @@ describe("breakpoints", () => {
       sourceId: "a",
       line: 5,
       column: 1,
       sourceUrl: "http://localhost:8000/examples/a"
     };
 
     const source = makeSource("a");
     await dispatch(actions.newSource(source));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
 
     await dispatch(actions.addBreakpoint(loc));
 
     let bp = selectors.getBreakpoint(getState(), loc);
     expect(bp && bp.options.condition).toBe(undefined);
 
     await dispatch(
       actions.setBreakpointOptions(loc, {
@@ -393,17 +393,17 @@ describe("breakpoints", () => {
       sourceId: "a",
       line: 5,
       column: 1,
       sourceUrl: "http://localhost:8000/examples/a"
     };
 
     const source = makeSource("a");
     await dispatch(actions.newSource(source));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
 
     await dispatch(actions.addBreakpoint(loc));
     let bp = selectors.getBreakpoint(getState(), loc);
     if (!bp) {
       throw new Error("no breakpoint");
     }
 
     await dispatch(actions.disableBreakpoint(bp));
@@ -431,17 +431,17 @@ describe("breakpoints", () => {
       sourceId: "a.js",
       line: 1,
       column: 0,
       sourceUrl: "http://localhost:8000/examples/a.js"
     };
 
     const source = makeSource("a.js");
     await dispatch(actions.newSource(source));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
 
     await dispatch(actions.addBreakpoint(loc));
     await dispatch(actions.togglePrettyPrint("a.js"));
 
     const breakpoint = selectors.getBreakpointsList(getState())[0];
 
     expect(
       breakpoint.location.sourceUrl &&
--- a/devtools/client/debugger/new/src/actions/pause/mapFrames.js
+++ b/devtools/client/debugger/new/src/actions/pause/mapFrames.js
@@ -3,21 +3,23 @@
  * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
 
 // @flow
 
 import {
   getFrames,
   getSymbols,
   getSource,
+  getSourceFromId,
   getSelectedFrame
 } from "../../selectors";
 
 import assert from "../../utils/assert";
 import { findClosestFunction } from "../../utils/ast";
+import { setSymbols } from "../sources/symbols";
 
 import type { Frame, ThreadId } from "../../types";
 import type { State } from "../../reducers/types";
 import type { ThunkArgs } from "../types";
 
 import { isGeneratedId } from "devtools-source-map";
 
 function isFrameBlackboxed(state, frame) {
@@ -62,16 +64,17 @@ function updateFrameLocations(
 export function mapDisplayNames(
   frames: Frame[],
   getState: () => State
 ): Frame[] {
   return frames.map(frame => {
     if (frame.isOriginal) {
       return frame;
     }
+
     const source = getSource(getState(), frame.location.sourceId);
 
     if (!source) {
       return frame;
     }
 
     const symbols = getSymbols(getState(), source);
 
@@ -147,33 +150,45 @@ async function expandFrames(
         generatedLocation: frame.generatedLocation,
         originalDisplayName: originalFrame.displayName
       });
     });
   }
   return result;
 }
 
+async function updateFrameSymbols(frames, { dispatch, getState }) {
+  await Promise.all(
+    frames.map(frame => {
+      const source = getSourceFromId(getState(), frame.location.sourceId);
+      return dispatch(setSymbols({ source }));
+    })
+  );
+}
+
 /**
  * Map call stack frame locations and display names to originals.
  * e.g.
  * 1. When the debuggee pauses
  * 2. When a source is pretty printed
  * 3. When symbols are loaded
  * @memberof actions/pause
  * @static
  */
 export function mapFrames(thread: ThreadId) {
-  return async function({ dispatch, getState, sourceMaps }: ThunkArgs) {
+  return async function(thunkArgs: ThunkArgs) {
+    const { dispatch, getState, sourceMaps } = thunkArgs;
     const frames = getFrames(getState(), thread);
     if (!frames) {
       return;
     }
 
     let mappedFrames = await updateFrameLocations(frames, sourceMaps);
+    await updateFrameSymbols(mappedFrames, thunkArgs);
+
     mappedFrames = await expandFrames(mappedFrames, sourceMaps, getState);
     mappedFrames = mapDisplayNames(mappedFrames, getState);
 
     const selectedFrameId = getSelectedFrameId(
       getState(),
       thread,
       mappedFrames
     );
--- a/devtools/client/debugger/new/src/actions/pause/mapScopes.js
+++ b/devtools/client/debugger/new/src/actions/pause/mapScopes.js
@@ -67,19 +67,19 @@ export function mapScopes(scopes: Promis
           !generatedSource ||
           generatedSource.isWasm ||
           source.isPrettyPrinted ||
           isGenerated(source)
         ) {
           return null;
         }
 
-        await dispatch(loadSourceText(source));
+        await dispatch(loadSourceText({ source }));
         if (isOriginal(source)) {
-          await dispatch(loadSourceText(generatedSource));
+          await dispatch(loadSourceText({ source: generatedSource }));
         }
 
         try {
           return await buildMappedScopes(
             source,
             frame,
             await scopes,
             sourceMaps,
--- a/devtools/client/debugger/new/src/actions/pause/tests/pause.spec.js
+++ b/devtools/client/debugger/new/src/actions/pause/tests/pause.spec.js
@@ -156,17 +156,16 @@ describe("pause", () => {
       const mockPauseInfo = createPauseInfo({
         sourceId: "await",
         line: 2,
         column: 0
       });
 
       const source = makeSource("await");
       await dispatch(actions.newSource(source));
-      await dispatch(actions.loadSourceText(source));
 
       await dispatch(actions.paused(mockPauseInfo));
       const getNextStepSpy = jest.spyOn(parser, "getNextStep");
       dispatch(actions.stepOver());
       expect(getNextStepSpy).toBeCalled();
       getNextStepSpy.mockRestore();
     });
 
@@ -176,17 +175,16 @@ describe("pause", () => {
       const mockPauseInfo = createPauseInfo({
         sourceId: "await",
         line: 2,
         column: 6
       });
 
       const source = makeSource("await");
       await dispatch(actions.newSource(source));
-      await dispatch(actions.loadSourceText(source));
 
       await dispatch(actions.paused(mockPauseInfo));
       const getNextStepSpy = jest.spyOn(parser, "getNextStep");
       dispatch(actions.stepOver());
       expect(getNextStepSpy).toBeCalled();
       getNextStepSpy.mockRestore();
     });
 
@@ -203,24 +201,24 @@ describe("pause", () => {
         scope: {
           bindings: { variables: { b: {} }, arguments: [{ a: {} }] }
         }
       });
 
       const source = makeSource("foo");
       await dispatch(actions.newSource(source));
       await dispatch(actions.newSource(makeOriginalSource("foo")));
-      await dispatch(actions.loadSourceText(source));
 
       await dispatch(actions.paused(mockPauseInfo));
       expect(selectors.getFrames(getState(), "FakeThread")).toEqual([
         {
           generatedLocation: { column: 0, line: 1, sourceId: "foo" },
           id: mockFrameId,
           location: { column: 0, line: 1, sourceId: "foo" },
+          originalDisplayName: "foo",
           scope: {
             bindings: { arguments: [{ a: {} }], variables: { b: {} } }
           },
           thread: "FakeThread"
         }
       ]);
 
       expect(selectors.getFrameScopes(getState(), "FakeThread")).toEqual({
@@ -267,19 +265,16 @@ describe("pause", () => {
       const store = createStore(mockThreadClient, {}, sourceMapsMock);
       const { dispatch, getState } = store;
       const mockPauseInfo = createPauseInfo(generatedLocation);
 
       const fooSource = makeSource("foo");
       const fooOriginalSource = makeSource("foo-original");
       await dispatch(actions.newSource(fooSource));
       await dispatch(actions.newSource(fooOriginalSource));
-      await dispatch(actions.loadSourceText(fooSource));
-      await dispatch(actions.loadSourceText(fooOriginalSource));
-      await dispatch(actions.setSymbols("foo-original"));
 
       await dispatch(actions.paused(mockPauseInfo));
       expect(selectors.getFrames(getState(), "FakeThread")).toEqual([
         {
           generatedLocation: { column: 0, line: 1, sourceId: "foo" },
           id: mockFrameId,
           location: { column: 0, line: 3, sourceId: "foo-original" },
           originalDisplayName: "fooOriginal",
@@ -334,18 +329,16 @@ describe("pause", () => {
       const { dispatch, getState } = store;
       const mockPauseInfo = createPauseInfo(generatedLocation);
 
       const source = makeSource("foo-wasm", { isWasm: true });
       const originalSource = makeOriginalSource("foo-wasm");
 
       await dispatch(actions.newSource(source));
       await dispatch(actions.newSource(originalSource));
-      await dispatch(actions.loadSourceText(source));
-      await dispatch(actions.loadSourceText(originalSource));
 
       await dispatch(actions.paused(mockPauseInfo));
       expect(selectors.getFrames(getState(), "FakeThread")).toEqual([
         {
           displayName: "fooBar",
           generatedLocation: { column: 0, line: 1, sourceId: "foo-wasm" },
           id: mockFrameId,
           isOriginal: true,
--- a/devtools/client/debugger/new/src/actions/project-text-search.js
+++ b/devtools/client/debugger/new/src/actions/project-text-search.js
@@ -83,17 +83,17 @@ export function searchSources(query: str
     dispatch(updateSearchStatus(statusType.fetching));
     const validSources = getSourceList(getState()).filter(
       source => !hasPrettySource(getState(), source.id) && !isThirdParty(source)
     );
     for (const source of validSources) {
       if (cancelled) {
         return;
       }
-      await dispatch(loadSourceText(source));
+      await dispatch(loadSourceText({ source }));
       await dispatch(searchSource(source.id, query));
     }
     dispatch(updateSearchStatus(statusType.done));
   };
 
   search.cancel = () => {
     cancelled = true;
   };
--- a/devtools/client/debugger/new/src/actions/sources/loadSourceText.js
+++ b/devtools/client/debugger/new/src/actions/sources/loadSourceText.js
@@ -2,33 +2,37 @@
  * 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 { PROMISE } from "../utils/middleware/promise";
 import {
   getSource,
+  getSourceFromId,
   getGeneratedSource,
   getSourcesEpoch
 } from "../../selectors";
 import { setBreakpointPositions } from "../breakpoints";
 
 import { prettyPrintSource } from "./prettyPrint";
 
 import * as parser from "../../workers/parser";
 import { isLoaded, isOriginal, isPretty } from "../../utils/source";
+import {
+  memoizeableAction,
+  type MemoizedAction
+} from "../../utils/memoizableAction";
+
 import { Telemetry } from "devtools-modules";
 
 import type { ThunkArgs } from "../types";
 
 import type { Source } from "../../types";
 
-const requests = new Map();
-
 // Measures the time it takes for a source to load
 const loadSourceHistogram = "DEVTOOLS_DEBUGGER_LOAD_SOURCE_MS";
 const telemetry = new Telemetry();
 
 async function loadSource(
   state,
   source: Source,
   { sourceMaps, client }
@@ -64,70 +68,52 @@ async function loadSource(
   return {
     text: response.source,
     contentType: response.contentType || "text/javascript"
   };
 }
 
 async function loadSourceTextPromise(
   source: Source,
-  epoch: number,
   { dispatch, getState, client, sourceMaps }: ThunkArgs
 ): Promise<?Source> {
-  if (isLoaded(source)) {
-    return source;
-  }
-
+  const epoch = getSourcesEpoch(getState());
   await dispatch({
     type: "LOAD_SOURCE_TEXT",
     sourceId: source.id,
     epoch,
     [PROMISE]: loadSource(getState(), source, { sourceMaps, client })
   });
 
   const newSource = getSource(getState(), source.id);
+
   if (!newSource) {
     return;
   }
 
   if (!newSource.isWasm && isLoaded(newSource)) {
     parser.setSource(newSource);
-    dispatch(setBreakpointPositions(newSource.id));
+    dispatch(setBreakpointPositions({ sourceId: newSource.id }));
   }
 
   return newSource;
 }
 
-/**
- * @memberof actions/sources
- * @static
- */
-export function loadSourceText(inputSource: ?Source) {
-  return async (thunkArgs: ThunkArgs) => {
-    if (!inputSource) {
-      return;
-    }
-
-    // This ensures that the falsy check above is preserved into the IIFE
-    // below in a way that Flow is happy with.
-    const source = inputSource;
-
-    const epoch = getSourcesEpoch(thunkArgs.getState());
-
-    const id = `${epoch}:${source.id}`;
-    let promise = requests.get(id);
-    if (!promise) {
-      promise = (async () => {
-        try {
-          return await loadSourceTextPromise(source, epoch, thunkArgs);
-        } catch (e) {
-          // TODO: This swallows errors for now. Ideally we would get rid of
-          // this once we have a better handle on our async state management.
-        } finally {
-          requests.delete(id);
-        }
-      })();
-      requests.set(id, promise);
-    }
-
-    return promise;
+export function loadSourceById(sourceId: string) {
+  return ({ getState, dispatch }: ThunkArgs) => {
+    const source = getSourceFromId(getState(), sourceId);
+    return dispatch(loadSourceText({ source }));
   };
 }
+
+export const loadSourceText: MemoizedAction<
+  { source: Source },
+  ?Source
+> = memoizeableAction("loadSourceText", {
+  exitEarly: ({ source }) => !source,
+  hasValue: ({ source }, { getState }) => isLoaded(source),
+  getValue: ({ source }, { getState }) => getSource(getState(), source.id),
+  createKey: ({ source }, { getState }) => {
+    const epoch = getSourcesEpoch(getState());
+    return `${epoch}:${source.id}`;
+  },
+  action: ({ source }, thunkArgs) => loadSourceTextPromise(source, thunkArgs)
+});
--- a/devtools/client/debugger/new/src/actions/sources/newSources.js
+++ b/devtools/client/debugger/new/src/actions/sources/newSources.js
@@ -187,17 +187,17 @@ function checkPendingBreakpoints(sourceI
       source
     );
 
     if (pendingBreakpoints.length === 0) {
       return;
     }
 
     // load the source text if there is a pending breakpoint for it
-    await dispatch(loadSourceText(source));
+    await dispatch(loadSourceText({ source }));
 
     await Promise.all(
       pendingBreakpoints.map(bp => {
         return dispatch(syncBreakpoint(sourceId, bp));
       })
     );
   };
 }
@@ -243,16 +243,16 @@ export function newSources(sources: Sour
       dispatch(checkSelectedSource(source.id));
     }
 
     // Adding new sources may have cleared this file's breakpoint positions
     // in cases where a new <script> loaded in the HTML, so we manually
     // re-request new breakpoint positions.
     for (const source of sourcesNeedingPositions) {
       if (!hasBreakpointPositions(getState(), source.id)) {
-        dispatch(setBreakpointPositions(source.id));
+        dispatch(setBreakpointPositions({ sourceId: source.id }));
       }
     }
 
     dispatch(restoreBlackBoxedSources(_newSources));
     dispatch(loadSourceMaps(_newSources));
   };
 }
--- a/devtools/client/debugger/new/src/actions/sources/prettyPrint.js
+++ b/devtools/client/debugger/new/src/actions/sources/prettyPrint.js
@@ -74,16 +74,31 @@ export function createPrettySource(sourc
 
     dispatch(({ type: "ADD_SOURCE", source: prettySource }: Action));
     await dispatch(selectSource(prettySource.id));
 
     return prettySource;
   };
 }
 
+function selectPrettyLocation(prettySource: Source) {
+  return async ({ dispatch, sourceMaps, getState }: ThunkArgs) => {
+    let location = getSelectedLocation(getState());
+
+    if (location) {
+      location = await sourceMaps.getOriginalLocation(location);
+      return dispatch(
+        selectSpecificLocation({ ...location, sourceId: prettySource.id })
+      );
+    }
+
+    return dispatch(selectSource(prettySource.id));
+  };
+}
+
 /**
  * Toggle the pretty printing of a source's text. All subsequent calls to
  * |getText| will return the pretty-toggled text. Nothing will happen for
  * non-javascript files.
  *
  * @memberof actions/sources
  * @static
  * @param string id The source form from the RDP.
@@ -98,51 +113,36 @@ export function togglePrettyPrint(source
       return {};
     }
 
     if (!source.isPrettyPrinted) {
       recordEvent("pretty_print");
     }
 
     if (!isLoaded(source)) {
-      await dispatch(loadSourceText(source));
+      await dispatch(loadSourceText({ source }));
     }
 
     assert(
       sourceMaps.isGeneratedId(sourceId),
       "Pretty-printing only allowed on generated sources"
     );
 
-    const selectedLocation = getSelectedLocation(getState());
     const url = getPrettySourceURL(source.url);
     const prettySource = getSourceByURL(getState(), url);
 
-    const options = {};
-    if (selectedLocation) {
-      options.location = await sourceMaps.getOriginalLocation(selectedLocation);
-    }
-
     if (prettySource) {
-      const _sourceId = prettySource.id;
-      return dispatch(
-        selectSpecificLocation({ ...options.location, sourceId: _sourceId })
-      );
+      return dispatch(selectPrettyLocation(prettySource));
     }
 
     const newPrettySource = await dispatch(createPrettySource(sourceId));
+    await dispatch(selectPrettyLocation(newPrettySource));
 
     await dispatch(remapBreakpoints(sourceId));
 
     const threads = getSourceThreads(getState(), source);
     await Promise.all(threads.map(thread => dispatch(mapFrames(thread))));
 
-    await dispatch(setSymbols(newPrettySource.id));
-
-    dispatch(
-      selectSpecificLocation({
-        ...options.location,
-        sourceId: newPrettySource.id
-      })
-    );
+    await dispatch(setSymbols({ source: newPrettySource }));
 
     return newPrettySource;
   };
 }
--- a/devtools/client/debugger/new/src/actions/sources/select.js
+++ b/devtools/client/debugger/new/src/actions/sources/select.js
@@ -140,17 +140,17 @@ export function selectLocation(
 
     const tabSources = getSourcesForTabs(getState());
     if (!tabSources.includes(source)) {
       dispatch(addTab(source));
     }
 
     dispatch(setSelectedLocation(source, location));
 
-    await dispatch(loadSourceText(source));
+    await dispatch(loadSourceText({ source }));
     const loadedSource = getSource(getState(), source.id);
 
     if (!loadedSource) {
       // If there was a navigation while we were loading the loadedSource
       return;
     }
 
     if (
@@ -159,17 +159,17 @@ export function selectLocation(
       !getPrettySource(getState(), loadedSource.id) &&
       shouldPrettyPrint(loadedSource) &&
       isMinified(loadedSource)
     ) {
       await dispatch(togglePrettyPrint(loadedSource.id));
       dispatch(closeTab(loadedSource));
     }
 
-    dispatch(setSymbols(loadedSource.id));
+    dispatch(setSymbols({ source: loadedSource }));
     dispatch(setOutOfScopeLocations());
 
     // If a new source is selected update the file search results
     const newSource = getSelectedSource(getState());
     if (currentSource && currentSource !== newSource) {
       dispatch(updateActiveFileSearch());
     }
   };
--- a/devtools/client/debugger/new/src/actions/sources/symbols.js
+++ b/devtools/client/debugger/new/src/actions/sources/symbols.js
@@ -1,39 +1,56 @@
 /* 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/>. */
-import { getSourceFromId, getSourceThreads, getSymbols } from "../../selectors";
+
+// @flow
+
+import { hasSymbols, getSymbols } from "../../selectors";
 
 import { PROMISE } from "../utils/middleware/promise";
-import { mapFrames } from "../pause";
 import { updateTab } from "../tabs";
+import { loadSourceText } from "./loadSourceText";
 
 import * as parser from "../../workers/parser";
 
 import { isLoaded } from "../../utils/source";
+import {
+  memoizeableAction,
+  type MemoizedAction
+} from "../../utils/memoizableAction";
 
-import type { SourceId } from "../../types";
-import type { ThunkArgs } from "../types";
+import type { Source } from "../../types";
+import type { Symbols } from "../../reducers/types";
 
-export function setSymbols(sourceId: SourceId) {
-  return async ({ dispatch, getState, sourceMaps }: ThunkArgs) => {
-    const source = getSourceFromId(getState(), sourceId);
+async function doSetSymbols(source, { dispatch, getState }) {
+  const sourceId = source.id;
 
-    if (source.isWasm || getSymbols(getState(), source) || !isLoaded(source)) {
-      return;
-    }
+  if (!isLoaded(source)) {
+    await dispatch(loadSourceText({ source }));
+  }
+
+  await dispatch({
+    type: "SET_SYMBOLS",
+    sourceId,
+    [PROMISE]: parser.getSymbols(sourceId)
+  });
 
-    await dispatch({
-      type: "SET_SYMBOLS",
-      sourceId,
-      [PROMISE]: parser.getSymbols(sourceId)
-    });
+  const symbols = getSymbols(getState(), source);
+  if (symbols && symbols.framework) {
+    dispatch(updateTab(source, symbols.framework));
+  }
+
+  return symbols;
+}
+
+type Args = { source: Source };
 
-    const threads = getSourceThreads(getState(), source);
-    await Promise.all(threads.map(thread => dispatch(mapFrames(thread))));
-
-    const symbols = getSymbols(getState(), source);
-    if (symbols.framework) {
-      dispatch(updateTab(source, symbols.framework));
-    }
-  };
-}
+export const setSymbols: MemoizedAction<Args, ?Symbols> = memoizeableAction(
+  "setSymbols",
+  {
+    exitEarly: ({ source }) => source.isWasm,
+    hasValue: ({ source }, { getState }) => hasSymbols(getState(), source),
+    getValue: ({ source }, { getState }) => getSymbols(getState(), source),
+    createKey: ({ source }) => source.id,
+    action: ({ source }, thunkArgs) => doSetSymbols(source, thunkArgs)
+  }
+);
--- a/devtools/client/debugger/new/src/actions/sources/tests/loadSource.spec.js
+++ b/devtools/client/debugger/new/src/actions/sources/tests/loadSource.spec.js
@@ -20,27 +20,27 @@ import { getBreakpointsList } from "../.
 
 describe("loadSourceText", () => {
   it("should load source text", async () => {
     const store = createStore(sourceThreadClient);
     const { dispatch, getState } = store;
 
     const foo1Source = makeSource("foo1");
     await dispatch(actions.newSource(foo1Source));
-    await dispatch(actions.loadSourceText(foo1Source));
+    await dispatch(actions.loadSourceText({ source: foo1Source }));
     const fooSource = selectors.getSource(getState(), "foo1");
 
     if (!fooSource || typeof fooSource.text != "string") {
       throw new Error("bad fooSource");
     }
     expect(fooSource.text.indexOf("return foo1")).not.toBe(-1);
 
     const baseFoo2Source = makeSource("foo2");
     await dispatch(actions.newSource(baseFoo2Source));
-    await dispatch(actions.loadSourceText(baseFoo2Source));
+    await dispatch(actions.loadSourceText({ source: baseFoo2Source }));
     const foo2Source = selectors.getSource(getState(), "foo2");
 
     if (!foo2Source || typeof foo2Source.text != "string") {
       throw new Error("bad fooSource");
     }
     expect(foo2Source.text.indexOf("return foo2")).not.toBe(-1);
   });
 
@@ -79,31 +79,22 @@ describe("loadSourceText", () => {
     await dispatch(actions.newSource(fooGenSource));
 
     const location = {
       sourceId: fooOrigSource.id,
       line: 1,
       column: 0
     };
     await dispatch(actions.addBreakpoint(location, {}));
-    const breakpoint = selectors.getBreakpoint(getState(), location);
-    if (!breakpoint) {
-      throw new Error("no breakpoint");
-    }
-
-    expect(breakpoint.text).toBe("var fooGen = 42;");
-    expect(breakpoint.originalText).toBe("var fooOrig = 42;");
-
-    await dispatch(actions.loadSourceText(fooOrigSource));
 
     const breakpoint1 = getBreakpointsList(getState())[0];
     expect(breakpoint1.text).toBe("var fooGen = 42;");
     expect(breakpoint1.originalText).toBe("var fooOrig = 42;");
 
-    await dispatch(actions.loadSourceText(fooGenSource));
+    await dispatch(actions.loadSourceText({ source: fooGenSource }));
 
     const breakpoint2 = getBreakpointsList(getState())[0];
     expect(breakpoint2.text).toBe("var fooGen = 42;");
     expect(breakpoint2.originalText).toBe("var fooOrig = 42;");
   });
 
   it("loads two sources w/ one request", async () => {
     let resolve;
@@ -116,21 +107,21 @@ describe("loadSourceText", () => {
         }),
       getBreakpointPositions: async () => ({})
     });
     const id = "foo";
     const baseSource = makeSource(id, { loadedState: "unloaded" });
 
     await dispatch(actions.newSource(baseSource));
 
-    let source = selectors.getSource(getState(), id);
-    dispatch(actions.loadSourceText(source));
+    let source = selectors.getSourceFromId(getState(), id);
+    dispatch(actions.loadSourceText({ source }));
 
-    source = selectors.getSource(getState(), id);
-    const loading = dispatch(actions.loadSourceText(source));
+    source = selectors.getSourceFromId(getState(), id);
+    const loading = dispatch(actions.loadSourceText({ source }));
 
     if (!resolve) {
       throw new Error("no resolve");
     }
     resolve({ source: "yay", contentType: "text/javascript" });
     await loading;
     expect(count).toEqual(1);
 
@@ -148,41 +139,42 @@ describe("loadSourceText", () => {
           resolve = r;
         }),
       getBreakpointPositions: async () => ({})
     });
     const id = "foo";
     const baseSource = makeSource(id, { loadedState: "unloaded" });
 
     await dispatch(actions.newSource(baseSource));
-    let source = selectors.getSource(getState(), id);
-    const loading = dispatch(actions.loadSourceText(source));
+    let source = selectors.getSourceFromId(getState(), id);
+    const loading = dispatch(actions.loadSourceText({ source }));
 
     if (!resolve) {
       throw new Error("no resolve");
     }
     resolve({ source: "yay", contentType: "text/javascript" });
     await loading;
 
-    source = selectors.getSource(getState(), id);
-    await dispatch(actions.loadSourceText(source));
+    source = selectors.getSourceFromId(getState(), id);
+    await dispatch(actions.loadSourceText({ source }));
     expect(count).toEqual(1);
 
     source = selectors.getSource(getState(), id);
     expect(source && source.text).toEqual("yay");
   });
 
   it("should cache subsequent source text loads", async () => {
     const { dispatch, getState } = createStore(sourceThreadClient);
 
     const source = makeSource("foo1");
-    await dispatch(actions.loadSourceText(source));
-    const prevSource = selectors.getSource(getState(), "foo1");
+    dispatch(actions.newSource(source));
+    await dispatch(actions.loadSourceText({ source }));
+    const prevSource = selectors.getSourceFromId(getState(), "foo1");
 
-    await dispatch(actions.loadSourceText(prevSource));
+    await dispatch(actions.loadSourceText({ source: prevSource }));
     const curSource = selectors.getSource(getState(), "foo1");
 
     expect(prevSource === curSource).toBeTruthy();
   });
 
   it("should indicate a loading source", async () => {
     const store = createStore(sourceThreadClient);
     const { dispatch } = store;
@@ -190,27 +182,27 @@ describe("loadSourceText", () => {
     const source = makeSource("foo2");
     await dispatch(actions.newSource(source));
 
     const wasLoading = watchForState(store, state => {
       const fooSource = selectors.getSource(state, "foo2");
       return fooSource && fooSource.loadedState === "loading";
     });
 
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
 
     expect(wasLoading()).toBe(true);
   });
 
   it("should indicate an errored source text", async () => {
     const { dispatch, getState } = createStore(sourceThreadClient);
 
     const source = makeSource("bad-id");
     await dispatch(actions.newSource(source));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
     const badSource = selectors.getSource(getState(), "bad-id");
 
     if (!badSource || !badSource.error) {
       throw new Error("bad badSource");
     }
     expect(badSource.error.indexOf("unknown source")).not.toBe(-1);
   });
 });
--- a/devtools/client/debugger/new/src/actions/tests/ast.spec.js
+++ b/devtools/client/debugger/new/src/actions/tests/ast.spec.js
@@ -12,17 +12,16 @@ import {
   makeSource,
   makeOriginalSource,
   makeFrame,
   waitForState
 } from "../../utils/test-head";
 
 import readFixture from "./helpers/readFixture";
 const {
-  getSource,
   getSymbols,
   getOutOfScopeLocations,
   getInScopeLines,
   isSymbolsLoading,
   getFramework
 } = selectors;
 
 import { prefs } from "../../utils/prefs";
@@ -65,18 +64,20 @@ const evaluationResult = {
 describe("ast", () => {
   describe("setSymbols", () => {
     describe("when the source is loaded", () => {
       it("should be able to set symbols", async () => {
         const store = createStore(threadClient);
         const { dispatch, getState } = store;
         const base = makeSource("base.js");
         await dispatch(actions.newSource(base));
-        await dispatch(actions.loadSourceText(base));
-        await dispatch(actions.setSymbols("base.js"));
+        await dispatch(actions.loadSourceText({ source: base }));
+
+        const loadedSource = selectors.getSourceFromId(getState(), base.id);
+        await dispatch(actions.setSymbols({ source: loadedSource }));
         await waitForState(store, state => !isSymbolsLoading(state, base));
 
         const baseSymbols = getSymbols(getState(), base);
         expect(baseSymbols).toMatchSnapshot();
       });
     });
 
     describe("when the source is not loaded", () => {
@@ -103,31 +104,30 @@ describe("ast", () => {
         const store = createStore(threadClient, {}, sourceMaps);
         const { dispatch, getState } = store;
         const source = makeOriginalSource("reactComponent.js");
 
         await dispatch(actions.newSource(makeSource("reactComponent.js")));
 
         await dispatch(actions.newSource(source));
 
-        await dispatch(
-          actions.loadSourceText(getSource(getState(), source.id))
-        );
-        await dispatch(actions.setSymbols(source.id));
+        await dispatch(actions.loadSourceText({ source }));
+        const loadedSource = selectors.getSourceFromId(getState(), source.id);
+        await dispatch(actions.setSymbols({ source: loadedSource }));
 
         expect(getFramework(getState(), source)).toBe("React");
       });
 
       it("should not give false positive on non react components", async () => {
         const store = createStore(threadClient);
         const { dispatch, getState } = store;
         const base = makeSource("base.js");
         await dispatch(actions.newSource(base));
-        await dispatch(actions.loadSourceText(base));
-        await dispatch(actions.setSymbols("base.js"));
+        await dispatch(actions.loadSourceText({ source: base }));
+        await dispatch(actions.setSymbols({ source: base }));
 
         expect(getFramework(getState(), base)).toBe(undefined);
       });
     });
   });
 
   describe("getOutOfScopeLocations", () => {
     beforeEach(async () => {
--- a/devtools/client/debugger/new/src/actions/tests/expressions.spec.js
+++ b/devtools/client/debugger/new/src/actions/tests/expressions.spec.js
@@ -32,16 +32,17 @@ const mockThreadClient = {
             } else {
               resolve("boo");
             }
           })
       )
     ),
   getFrameScopes: async () => {},
   sourceContents: () => ({ source: "", contentType: "text/javascript" }),
+  getBreakpointPositions: async () => [],
   autocomplete: () => {
     return new Promise(resolve => {
       resolve({
         from: "foo",
         matches: ["toLocaleString", "toSource", "toString", "toolbar", "top"],
         matchProp: "to"
       });
     });
@@ -56,17 +57,16 @@ describe("expressions", () => {
     expect(selectors.getExpressions(getState()).size).toBe(1);
   });
 
   it("should not add empty expressions", () => {
     const { dispatch, getState } = createStore(mockThreadClient);
 
     dispatch(actions.addExpression((undefined: any)));
     dispatch(actions.addExpression(""));
-
     expect(selectors.getExpressions(getState()).size).toBe(0);
   });
 
   it("should not add invalid expressions", async () => {
     const { dispatch, getState } = createStore(mockThreadClient);
     await dispatch(actions.addExpression("foo#"));
     const state = getState();
     expect(selectors.getExpressions(state).size).toBe(0);
@@ -92,72 +92,64 @@ describe("expressions", () => {
     expect(selectors.getExpression(getState(), "bar")).toBeUndefined();
   });
 
   it("should delete an expression", async () => {
     const { dispatch, getState } = createStore(mockThreadClient);
 
     await dispatch(actions.addExpression("foo"));
     await dispatch(actions.addExpression("bar"));
-
     expect(selectors.getExpressions(getState()).size).toBe(2);
 
     const expression = selectors.getExpression(getState(), "foo");
     dispatch(actions.deleteExpression(expression));
-
     expect(selectors.getExpressions(getState()).size).toBe(1);
     expect(selectors.getExpression(getState(), "bar").input).toBe("bar");
   });
 
   it("should evaluate expressions global scope", async () => {
     const { dispatch, getState } = createStore(mockThreadClient);
 
     await dispatch(actions.addExpression("foo"));
     await dispatch(actions.addExpression("bar"));
-
     expect(selectors.getExpression(getState(), "foo").value).toBe("bla");
     expect(selectors.getExpression(getState(), "bar").value).toBe("bla");
 
     await dispatch(actions.evaluateExpressions());
-
     expect(selectors.getExpression(getState(), "foo").value).toBe("bla");
     expect(selectors.getExpression(getState(), "bar").value).toBe("bla");
   });
 
   it("should evaluate expressions in specific scope", async () => {
     const { dispatch, getState } = createStore(mockThreadClient);
     await createFrames(dispatch);
-
     await dispatch(actions.newSource(makeSource("source")));
-
     await dispatch(actions.addExpression("foo"));
     await dispatch(actions.addExpression("bar"));
 
     expect(selectors.getExpression(getState(), "foo").value).toBe("boo");
     expect(selectors.getExpression(getState(), "bar").value).toBe("boo");
 
     await dispatch(actions.evaluateExpressions());
 
     expect(selectors.getExpression(getState(), "foo").value).toBe("boo");
     expect(selectors.getExpression(getState(), "bar").value).toBe("boo");
   });
 
   it("should get the autocomplete matches for the input", async () => {
     const { dispatch, getState } = createStore(mockThreadClient);
-
     await dispatch(actions.autocomplete("to", 2));
-
     expect(selectors.getAutocompleteMatchset(getState())).toMatchSnapshot();
   });
 });
 
 async function createFrames(dispatch) {
   const frame = makeMockFrame();
-
   await dispatch(actions.newSource(makeSource("example.js")));
+  await dispatch(actions.newSource(makeSource("source")));
 
   await dispatch(
     actions.paused({
       thread: "UnknownThread",
       frame,
       frames: [frame],
       why: { type: "just because" }
     })
--- a/devtools/client/debugger/new/src/actions/tests/pending-breakpoints.spec.js
+++ b/devtools/client/debugger/new/src/actions/tests/pending-breakpoints.spec.js
@@ -73,17 +73,17 @@ describe("when adding breakpoints", () =
       mockClient({ "5": [1] }),
       loadInitialState(),
       mockSourceMaps()
     );
 
     const source = makeSource("foo.js");
     await dispatch(actions.newSource(source));
     await dispatch(actions.newSource(makeSource("foo.js")));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
 
     const bp = generateBreakpoint("foo.js", 5, 1);
     const id = makePendingLocationId(bp.location);
 
     await dispatch(actions.addBreakpoint(bp.location));
     const pendingBps = selectors.getPendingBreakpoints(getState());
 
     expect(selectors.getPendingBreakpointList(getState())).toHaveLength(2);
@@ -114,18 +114,18 @@ describe("when adding breakpoints", () =
       const source2 = makeSource("foo2");
 
       await dispatch(actions.newSource(makeSource("foo")));
       await dispatch(actions.newSource(makeSource("foo2")));
 
       await dispatch(actions.newSource(source1));
       await dispatch(actions.newSource(source2));
 
-      await dispatch(actions.loadSourceText(source1));
-      await dispatch(actions.loadSourceText(source2));
+      await dispatch(actions.loadSourceText({ source: source1 }));
+      await dispatch(actions.loadSourceText({ source: source2 }));
 
       await dispatch(actions.addBreakpoint(breakpoint1.location));
       await dispatch(actions.addBreakpoint(breakpoint2.location));
 
       const pendingBps = selectors.getPendingBreakpoints(getState());
 
       // NOTE the sourceId should be `foo2/originalSource`, but is `foo2`
       // because we do not have a real source map for `getOriginalLocation`
@@ -139,17 +139,17 @@ describe("when adding breakpoints", () =
         mockClient({ "5": [0] }),
         loadInitialState(),
         mockSourceMaps()
       );
 
       const source = makeSource("foo");
       await dispatch(actions.newSource(makeSource("foo")));
       await dispatch(actions.newSource(source));
-      await dispatch(actions.loadSourceText(source));
+      await dispatch(actions.loadSourceText({ source }));
 
       await dispatch(
         actions.addBreakpoint(breakpoint1.location, { hidden: true })
       );
       const pendingBps = selectors.getPendingBreakpoints(getState());
 
       expect(pendingBps[breakpointLocationId1]).toBeUndefined();
     });
@@ -165,18 +165,18 @@ describe("when adding breakpoints", () =
       await dispatch(actions.newSource(makeSource("foo2")));
 
       const source1 = makeSource("foo");
       const source2 = makeSource("foo2");
 
       await dispatch(actions.newSource(source1));
       await dispatch(actions.newSource(source2));
 
-      await dispatch(actions.loadSourceText(source1));
-      await dispatch(actions.loadSourceText(source2));
+      await dispatch(actions.loadSourceText({ source: source1 }));
+      await dispatch(actions.loadSourceText({ source: source2 }));
 
       await dispatch(actions.addBreakpoint(breakpoint1.location));
       await dispatch(actions.addBreakpoint(breakpoint2.location));
       await dispatch(actions.removeBreakpoint(breakpoint1));
 
       const pendingBps = selectors.getPendingBreakpoints(getState());
       expect(pendingBps.hasOwnProperty(breakpointLocationId1)).toBe(false);
       expect(pendingBps.hasOwnProperty(breakpointLocationId2)).toBe(true);
@@ -192,17 +192,17 @@ describe("when changing an existing brea
       mockSourceMaps()
     );
     const bp = generateBreakpoint("foo");
     const id = makePendingLocationId(bp.location);
 
     const source = makeSource("foo");
     await dispatch(actions.newSource(source));
     await dispatch(actions.newSource(makeSource("foo")));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
 
     await dispatch(actions.addBreakpoint(bp.location));
     await dispatch(
       actions.setBreakpointOptions(bp.location, { condition: "2" })
     );
     const bps = selectors.getPendingBreakpoints(getState());
     const breakpoint = bps[id];
     expect(breakpoint.options.condition).toBe("2");
@@ -216,17 +216,17 @@ describe("when changing an existing brea
     );
     const bp = generateBreakpoint("foo");
     const id = makePendingLocationId(bp.location);
 
     await dispatch(actions.newSource(makeSource("foo")));
 
     const source = makeSource("foo");
     await dispatch(actions.newSource(source));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
 
     await dispatch(actions.addBreakpoint(bp.location));
     await dispatch(actions.disableBreakpoint(bp));
     const bps = selectors.getPendingBreakpoints(getState());
     const breakpoint = bps[id];
     expect(breakpoint.disabled).toBe(true);
   });
 
@@ -236,17 +236,17 @@ describe("when changing an existing brea
       loadInitialState(),
       mockSourceMaps()
     );
     const bp = generateBreakpoint("foo.js");
 
     const source = makeSource("foo.js");
     await dispatch(actions.newSource(source));
     await dispatch(actions.newSource(makeSource("foo.js")));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
 
     const id = makePendingLocationId(bp.location);
 
     await dispatch(actions.addBreakpoint(bp.location));
     await dispatch(
       actions.setBreakpointOptions(bp.location, { condition: "2" })
     );
     const bps = selectors.getPendingBreakpoints(getState());
@@ -273,17 +273,17 @@ describe("initializing when pending brea
       mockSourceMaps()
     );
     const bar = generateBreakpoint("bar.js", 5, 1);
 
     await dispatch(actions.newSource(makeSource("bar.js")));
 
     const source = makeSource("bar.js");
     await dispatch(actions.newSource(source));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
     await dispatch(actions.addBreakpoint(bar.location));
 
     const bps = selectors.getPendingBreakpointList(getState());
     expect(bps).toHaveLength(2);
   });
 
   it("adding bps doesn't remove existing pending breakpoints", async () => {
     const { dispatch, getState } = createStore(
@@ -291,17 +291,17 @@ describe("initializing when pending brea
       loadInitialState(),
       mockSourceMaps()
     );
     const bp = generateBreakpoint("foo.js");
 
     const source = makeSource("foo.js");
     await dispatch(actions.newSource(source));
     await dispatch(actions.newSource(makeSource("foo.js")));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
 
     await dispatch(actions.addBreakpoint(bp.location));
 
     const bps = selectors.getPendingBreakpointList(getState());
     expect(bps).toHaveLength(2);
   });
 });
 
@@ -313,17 +313,17 @@ describe("initializing with disabled pen
       mockSourceMaps()
     );
 
     const { getState, dispatch } = store;
     const source = makeSource("bar.js");
 
     await dispatch(actions.newSource(makeSource("bar.js")));
     await dispatch(actions.newSource(source));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
 
     await waitForState(store, state => {
       const bps = selectors.getBreakpointsForSource(state, source.id);
       return bps && Object.values(bps).length > 0;
     });
 
     const bp = selectors.getBreakpointForLocation(getState(), {
       line: 5,
@@ -349,17 +349,17 @@ describe("adding sources", () => {
     const { getState, dispatch } = store;
 
     expect(selectors.getBreakpointCount(getState())).toEqual(0);
 
     const source = makeSource("bar.js");
 
     await dispatch(actions.newSource(makeSource("bar.js")));
     await dispatch(actions.newSource(source));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
 
     await waitForState(store, state => selectors.getBreakpointCount(state) > 0);
 
     expect(selectors.getBreakpointCount(getState())).toEqual(1);
   });
 
   it("corresponding breakpoints are added to the original source", async () => {
     const source = makeSource("bar.js", { sourceMapURL: "foo" });
@@ -400,15 +400,15 @@ describe("adding sources", () => {
 
     expect(selectors.getBreakpointCount(getState())).toEqual(0);
 
     const source1 = makeSource("bar.js");
     const source2 = makeSource("foo.js");
     await dispatch(actions.newSource(makeSource("bar.js")));
     await dispatch(actions.newSource(makeSource("foo.js")));
     await dispatch(actions.newSources([source1, source2]));
-    await dispatch(actions.loadSourceText(source1));
-    await dispatch(actions.loadSourceText(source2));
+    await dispatch(actions.loadSourceText({ source: source1 }));
+    await dispatch(actions.loadSourceText({ source: source2 }));
 
     await waitForState(store, state => selectors.getBreakpointCount(state) > 0);
     expect(selectors.getBreakpointCount(getState())).toEqual(1);
   });
 });
--- a/devtools/client/debugger/new/src/actions/tests/project-text-search.spec.js
+++ b/devtools/client/debugger/new/src/actions/tests/project-text-search.spec.js
@@ -63,24 +63,30 @@ describe("project text search", () => {
 
     await dispatch(actions.searchSources(mockQuery));
 
     const results = getTextSearchResults(getState());
     expect(results).toMatchSnapshot();
   });
 
   it("should ignore sources with minified versions", async () => {
-    const source1 = makeSource("bar", { sourceMapURL: "bar:formatted" });
+    const source1 = makeSource("bar", {
+      sourceMapURL: "bar:formatted",
+      loadedState: "loaded",
+      source: "function bla(x, y) { const bar = 4; return 2;}",
+      contentType: "text/javascript"
+    });
     const source2 = makeSource("bar:formatted");
 
     const mockMaps = {
       getOriginalSourceText: async () => ({
         source: "function bla(x, y) {\n const bar = 4; return 2;\n}",
         contentType: "text/javascript"
       }),
+      applySourceMap: async () => {},
       getOriginalURLs: async () => [source2.url],
       getGeneratedRangesForOriginal: async () => [],
       getOriginalLocations: async items => items
     };
 
     const { dispatch, getState } = createStore(threadClient, {}, mockMaps);
     const mockQuery = "bla";
 
@@ -93,17 +99,17 @@ describe("project text search", () => {
     expect(results).toMatchSnapshot();
   });
 
   it("should search a specific source", async () => {
     const { dispatch, getState } = createStore(threadClient);
 
     const source = makeSource("bar");
     await dispatch(actions.newSource(source));
-    await dispatch(actions.loadSourceText(source));
+    await dispatch(actions.loadSourceText({ source }));
 
     dispatch(actions.addSearchQuery("bla"));
 
     const barSource = getSource(getState(), "bar");
     if (!barSource) {
       throw new Error("no barSource");
     }
     const sourceId = barSource.id;
--- a/devtools/client/debugger/new/src/components/Editor/DebugLine.js
+++ b/devtools/client/debugger/new/src/components/Editor/DebugLine.js
@@ -37,35 +37,35 @@ type TextClasses = {
 
 function isDocumentReady(source, frame) {
   return frame && isLoaded(source) && hasDocument(frame.location.sourceId);
 }
 
 export class DebugLine extends Component<Props> {
   debugExpression: null;
 
-  componentDidUpdate(prevProps: Props) {
-    const { why, frame, source } = this.props;
-
-    startOperation();
-    this.clearDebugLine(prevProps.why, prevProps.frame, prevProps.source);
-    this.setDebugLine(why, frame, source);
-    endOperation();
-  }
-
   componentDidMount() {
     const { why, frame, source } = this.props;
     this.setDebugLine(why, frame, source);
   }
 
   componentWillUnmount() {
     const { why, frame, source } = this.props;
     this.clearDebugLine(why, frame, source);
   }
 
+  componentDidUpdate(prevProps: Props) {
+    const { why, frame, source } = this.props;
+
+    startOperation();
+    this.clearDebugLine(prevProps.why, prevProps.frame, prevProps.source);
+    this.setDebugLine(why, frame, source);
+    endOperation();
+  }
+
   setDebugLine(why: Why, frame: Frame, source: Source) {
     if (!isDocumentReady(source, frame)) {
       return;
     }
     const sourceId = frame.location.sourceId;
     const doc = getDocument(sourceId);
 
     let { line, column } = toEditorPosition(frame.location);
--- a/devtools/client/debugger/new/src/components/Editor/index.js
+++ b/devtools/client/debugger/new/src/components/Editor/index.js
@@ -49,17 +49,16 @@ import ColumnBreakpoints from "./ColumnB
 import DebugLine from "./DebugLine";
 import HighlightLine from "./HighlightLine";
 import EmptyLines from "./EmptyLines";
 import EditorMenu from "./EditorMenu";
 import ConditionalPanel from "./ConditionalPanel";
 
 import {
   showSourceText,
-  updateDocument,
   showLoading,
   showErrorMessage,
   getEditor,
   clearEditor,
   getCursorLine,
   lineAtHeight,
   toSourceLine,
   getDocument,
@@ -122,35 +121,35 @@ class Editor extends PureComponent<Props
 
     this.state = {
       highlightedLineRange: null,
       editor: (null: any),
       contextMenu: null
     };
   }
 
-  componentWillReceiveProps(nextProps) {
-    if (!this.state.editor) {
-      return;
+  componentWillReceiveProps(nextProps: Props) {
+    let editor = this.state.editor;
+
+    if (!this.state.editor && nextProps.selectedSource) {
+      editor = this.setupEditor();
     }
 
     startOperation();
-    resizeBreakpointGutter(this.state.editor.codeMirror);
-    resizeToggleButton(this.state.editor.codeMirror);
-    endOperation();
-  }
+    this.setText(nextProps, editor);
+    this.setSize(nextProps, editor);
+    this.scrollToLocation(nextProps, editor);
 
-  componentWillUpdate(nextProps) {
-    if (!this.state.editor) {
-      return;
+    if (this.props.selectedSource != nextProps.selectedSource) {
+      this.props.updateViewport();
+      resizeBreakpointGutter(editor.codeMirror);
+      resizeToggleButton(editor.codeMirror);
     }
 
-    this.setText(nextProps);
-    this.setSize(nextProps);
-    this.scrollToLocation(nextProps);
+    endOperation();
   }
 
   setupEditor() {
     const editor = getEditor();
 
     // disables the default search shortcuts
     // $FlowIgnore
     editor._initShortcuts = () => {};
@@ -158,21 +157,16 @@ class Editor extends PureComponent<Props
     const node = ReactDOM.findDOMNode(this);
     if (node instanceof HTMLElement) {
       editor.appendToLocalElement(node.querySelector(".editor-mount"));
     }
 
     const { codeMirror } = editor;
     const codeMirrorWrapper = codeMirror.getWrapperElement();
 
-    startOperation();
-    resizeBreakpointGutter(codeMirror);
-    resizeToggleButton(codeMirror);
-    endOperation();
-
     codeMirror.on("gutterClick", this.onGutterClick);
 
     // Set code editor wrapper to be focusable
     codeMirrorWrapper.tabIndex = 0;
     codeMirrorWrapper.addEventListener("keydown", e => this.onKeyDown(e));
     codeMirrorWrapper.addEventListener("click", e => this.onClick(e));
     codeMirrorWrapper.addEventListener("mouseover", onMouseOver(codeMirror));
 
@@ -254,36 +248,16 @@ class Editor extends PureComponent<Props
     shortcuts.off(L10N.getStr("sourceTabs.closeTab.key"));
     shortcuts.off(L10N.getStr("toggleBreakpoint.key"));
     shortcuts.off(L10N.getStr("toggleCondPanel.breakpoint.key"));
     shortcuts.off(L10N.getStr("toggleCondPanel.logPoint.key"));
     shortcuts.off(searchAgainPrevKey);
     shortcuts.off(searchAgainKey);
   }
 
-  componentDidUpdate(prevProps, prevState) {
-    const { selectedSource } = this.props;
-    // NOTE: when devtools are opened, the editor is not set when
-    // the source loads so we need to wait until the editor is
-    // set to update the text and size.
-    if (!prevState.editor && selectedSource) {
-      if (!this.state.editor) {
-        const editor = this.setupEditor();
-        updateDocument(editor, selectedSource);
-      } else {
-        this.setText(this.props);
-        this.setSize(this.props);
-      }
-    }
-
-    if (prevProps.selectedSource != selectedSource) {
-      this.props.updateViewport();
-    }
-  }
-
   getCurrentLine() {
     const { codeMirror } = this.state.editor;
     const { selectedSource } = this.props;
     if (!selectedSource) {
       return;
     }
 
     const line = getCursorLine(codeMirror);
@@ -482,20 +456,18 @@ class Editor extends PureComponent<Props
         line: line,
         sourceId: selectedSource.id,
         sourceUrl: selectedSource.url
       },
       log
     );
   };
 
-  shouldScrollToLocation(nextProps) {
+  shouldScrollToLocation(nextProps, editor) {
     const { selectedLocation, selectedSource } = this.props;
-    const { editor } = this.state;
-
     if (
       !editor ||
       !nextProps.selectedSource ||
       !nextProps.selectedLocation ||
       !nextProps.selectedLocation.line ||
       !isLoaded(nextProps.selectedSource)
     ) {
       return false;
@@ -505,67 +477,67 @@ class Editor extends PureComponent<Props
       (!selectedSource || !isLoaded(selectedSource)) &&
       isLoaded(nextProps.selectedSource);
     const locationChanged = selectedLocation !== nextProps.selectedLocation;
     const symbolsChanged = nextProps.symbols != this.props.symbols;
 
     return isFirstLoad || locationChanged || symbolsChanged;
   }
 
-  scrollToLocation(nextProps) {
-    const { editor } = this.state;
+  scrollToLocation(nextProps, editor) {
     const { selectedLocation, selectedSource } = nextProps;
 
-    if (selectedLocation && this.shouldScrollToLocation(nextProps)) {
+    if (selectedLocation && this.shouldScrollToLocation(nextProps, editor)) {
       let { line, column } = toEditorPosition(selectedLocation);
 
       if (selectedSource && hasDocument(selectedSource.id)) {
         const doc = getDocument(selectedSource.id);
         const lineText: ?string = doc.getLine(line);
         column = Math.max(column, getIndentation(lineText));
       }
+
       scrollToColumn(editor.codeMirror, line, column);
     }
   }
 
-  setSize(nextProps) {
-    if (!this.state.editor) {
+  setSize(nextProps, editor) {
+    if (!editor) {
       return;
     }
 
     if (
       nextProps.startPanelSize !== this.props.startPanelSize ||
       nextProps.endPanelSize !== this.props.endPanelSize
     ) {
-      this.state.editor.codeMirror.setSize();
+      editor.codeMirror.setSize();
     }
   }
 
-  setText(props) {
+  setText(props, editor) {
     const { selectedSource, symbols } = props;
 
-    if (!this.state.editor) {
+    if (!editor) {
       return;
     }
 
     // check if we previously had a selected source
     if (!selectedSource) {
       return this.clearEditor();
     }
 
     if (!isLoaded(selectedSource)) {
-      return showLoading(this.state.editor);
+      return showLoading(editor);
     }
 
     if (selectedSource.error) {
       return this.showErrorMessage(selectedSource.error);
     }
 
     if (selectedSource) {
-      return showSourceText(this.state.editor, selectedSource, symbols);
+      return showSourceText(editor, selectedSource, symbols);
     }
   }
 
   clearEditor() {
     const { editor } = this.state;
     if (!editor) {
       return;
     }
--- a/devtools/client/debugger/new/src/reducers/ast.js
+++ b/devtools/client/debugger/new/src/reducers/ast.js
@@ -11,17 +11,19 @@
 
 import type { AstLocation, SymbolDeclarations } from "../workers/parser";
 
 import type { Source } from "../types";
 import type { Action, DonePromiseAction } from "../actions/types";
 
 type EmptyLinesType = number[];
 
-export type Symbols = SymbolDeclarations | {| loading: true |};
+export type LoadedSymbols = SymbolDeclarations;
+export type Symbols = LoadedSymbols | {| loading: true |};
+
 export type EmptyLinesMap = { [k: string]: EmptyLinesType };
 export type SymbolsMap = { [k: string]: Symbols };
 
 export type SourceMetaDataType = {
   framework: ?string
 };
 
 export type SourceMetaDataMap = { [k: string]: SourceMetaDataType };
--- a/devtools/client/debugger/new/src/reducers/sources.js
+++ b/devtools/client/debugger/new/src/reducers/sources.js
@@ -498,16 +498,24 @@ export function getGeneratedSource(
 
   if (isGenerated(source)) {
     return source;
   }
 
   return getSourceFromId(state, originalToGeneratedId(source.id));
 }
 
+export function getGeneratedSourceById(
+  state: OuterState,
+  sourceId: string
+): Source {
+  const generatedSourceId = originalToGeneratedId(sourceId);
+  return getSourceFromId(state, generatedSourceId);
+}
+
 export function getPendingSelectedLocation(state: OuterState) {
   return state.sources.pendingSelectedLocation;
 }
 
 export function getPrettySource(state: OuterState, id: ?string) {
   if (!id) {
     return;
   }
--- a/devtools/client/debugger/new/src/reducers/types.js
+++ b/devtools/client/debugger/new/src/reducers/types.js
@@ -45,9 +45,9 @@ export type PendingSelectedLocation = {
   line?: number,
   column?: number
 };
 
 export type { SourcesMap, SourcesMapByThread } from "./sources";
 export type { ActiveSearchType, OrientationType } from "./ui";
 export type { BreakpointsMap, XHRBreakpointsList } from "./breakpoints";
 export type { Command } from "./pause";
-export type { Symbols } from "./ast";
+export type { LoadedSymbols, Symbols } from "./ast";
--- a/devtools/client/debugger/new/src/selectors/visibleColumnBreakpoints.js
+++ b/devtools/client/debugger/new/src/selectors/visibleColumnBreakpoints.js
@@ -1,12 +1,12 @@
-// @flow
 /* 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/>. */
+// @flow
 
 import { groupBy } from "lodash";
 import { createSelector } from "reselect";
 
 import {
   getViewport,
   getSource,
   getSelectedSource,
@@ -159,17 +159,17 @@ export const visibleColumnBreakpoints: S
   getViewport,
   getSelectedSource,
   getColumnBreakpoints
 );
 
 export function getFirstBreakpointPosition(
   state: State,
   { line, sourceId }: SourceLocation
-) {
+): ?BreakpointPosition {
   const positions = getBreakpointPositionsForSource(state, sourceId);
   const source = getSource(state, sourceId);
 
   if (!source || !positions) {
     return;
   }
 
   return sortSelectedLocations(positions, source).find(
--- a/devtools/client/debugger/new/src/utils/breakpoint/astBreakpointLocation.js
+++ b/devtools/client/debugger/new/src/utils/breakpoint/astBreakpointLocation.js
@@ -1,15 +1,14 @@
 /* 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/>. */
 
 // @flow
 
-import { getSymbols } from "../../workers/parser";
 import { findClosestFunction } from "../ast";
 
 import type { SourceLocation, Source, ASTLocation } from "../../types";
 import type { Symbols } from "../../reducers/ast";
 
 export function getASTLocation(
   source: Source,
   symbols: ?Symbols,
@@ -28,18 +27,20 @@ export function getASTLocation(
       name: scope.name,
       offset: { line, column: undefined },
       index: scope.index
     };
   }
   return { name: undefined, offset: location, index: 0 };
 }
 
-export async function findFunctionByName(
-  source: Source,
+export function findFunctionByName(
+  symbols: Symbols,
   name: ?string,
   index: number
 ) {
-  const symbols = await getSymbols(source.id);
+  if (symbols.loading) {
+    return null;
+  }
+
   const functions = symbols.functions;
-
   return functions.find(node => node.name === name && node.index === index);
 }
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/new/src/utils/memoizableAction.js
@@ -0,0 +1,84 @@
+/* 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/>. */
+
+// @flow
+
+import type { ThunkArgs } from "../actions/types";
+
+export type MemoizedAction<
+  Args,
+  Result
+> = Args => ThunkArgs => Promise<?Result>;
+type MemoizableActionParams<Args, Result> = {
+  exitEarly?: (args: Args, thunkArgs: ThunkArgs) => boolean,
+  hasValue: (args: Args, thunkArgs: ThunkArgs) => boolean,
+  getValue: (args: Args, thunkArgs: ThunkArgs) => Result,
+  createKey: (args: Args, thunkArgs: ThunkArgs) => string,
+  action: (args: Args, thunkArgs: ThunkArgs) => Promise<Result>
+};
+
+/*
+ * memoizableActon is a utility for actions that should only be performed
+ * once per key. It is useful for loading sources, parsing symbols ...
+ *
+ * @exitEarly - if true, do not attempt to perform the action
+ * @hasValue - checks to see if the result is in the redux store
+ * @getValue - gets the result from the redux store
+ * @createKey - creates a key for the requests map
+ * @action - kicks off the async work for the action
+ *
+ *
+ * For Example
+ *
+ * export const setItem = memoizeableAction(
+ *   "setItem",
+ *   {
+ *     hasValue: ({ a }, { getState }) => hasItem(getState(), a),
+ *     getValue: ({ a }, { getState }) => getItem(getState(), a),
+ *     createKey: ({ a }) => a,
+ *     action: ({ a }, thunkArgs) => doSetItem(a, thunkArgs)
+ *   }
+ * );
+ * 
+ */
+export function memoizeableAction<Args, Result>(
+  name: string,
+  {
+    hasValue,
+    getValue,
+    createKey,
+    action,
+    exitEarly
+  }: MemoizableActionParams<Args, Result>
+): MemoizedAction<Args, Result> {
+  const requests = new Map();
+  return args => async (thunkArgs: ThunkArgs) => {
+    if (exitEarly && exitEarly(args, thunkArgs)) {
+      return;
+    }
+
+    if (hasValue(args, thunkArgs)) {
+      return getValue(args, thunkArgs);
+    }
+
+    const key = createKey(args, thunkArgs);
+    if (!requests.has(key)) {
+      requests.set(
+        key,
+        (async () => {
+          try {
+            await action(args, thunkArgs);
+          } catch (e) {
+            console.warn(`Action ${name} had an exception:`, e);
+          } finally {
+            requests.delete(key);
+          }
+        })()
+      );
+    }
+
+    await requests.get(key);
+    return getValue(args, thunkArgs);
+  };
+}
--- a/devtools/client/debugger/new/src/utils/moz.build
+++ b/devtools/client/debugger/new/src/utils/moz.build
@@ -26,16 +26,17 @@ CompiledModules(
     'fromJS.js',
     'function.js',
     'indentation.js',
     'isMinified.js',
     'location.js',
     'log.js',
     'makeRecord.js',
     'memoize.js',
+    'memoizableAction.js',
     'path.js',
     'prefs.js',
     'preview.js',
     'project-search.js',
     'quick-open.js',
     'result-list.js',
     'source-maps.js',
     'source-queue.js',
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-debugger-buttons.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-debugger-buttons.js
@@ -1,10 +1,11 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* 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 clickButton(dbg, button) {
   const resumeFired = waitForDispatch(dbg, "COMMAND");
   clickElement(dbg, button);
   return resumeFired;
 }
 
 async function clickStepOver(dbg) {
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-pretty-print-paused.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-pretty-print-paused.js
@@ -1,21 +1,28 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* 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/>. */
 
 // Tests pretty-printing a source that is currently paused.
 
 add_task(async function() {
   const dbg = await initDebugger("doc-minified.html", "math.min.js");
+  const thread = dbg.selectors.getCurrentThread(dbg.getState());
 
   await selectSource(dbg, "math.min.js");
   await addBreakpoint(dbg, "math.min.js", 2);
 
   invokeInTab("arithmetic");
   await waitForPaused(dbg);
   assertPausedLocation(dbg);
 
   clickElement(dbg, "prettyPrintButton");
   await waitForSelectedSource(dbg, "math.min.js:formatted");
+  await waitForState(
+    dbg,
+    state => dbg.selectors.getSelectedFrame(state, thread).location.line == 18
+  );
   assertPausedLocation(dbg);
+  await assertEditorBreakpoint(dbg, 18, true);
 
   await resume(dbg);
 });
--- a/devtools/client/debugger/new/test/mochitest/browser_dbg-xhr-run-to-completion.js
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-xhr-run-to-completion.js
@@ -2,17 +2,19 @@
  * 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/>. */
 
 // Test that XHR handlers are not called when pausing in the debugger.
 add_task(async function() {
   const dbg = await initDebugger("doc-xhr-run-to-completion.html");
   invokeInTab("singleRequest", "doc-xhr-run-to-completion.html");
   await waitForPaused(dbg);
+  await waitForSelectedLocation(dbg, 23);
   assertPausedLocation(dbg);
+
   resume(dbg);
   await once(Services.ppmm, "test passed");
 });
 
 // Test that XHR handlers are not called when pausing in the debugger,
 // including when there are multiple XHRs and multiple times we pause before
 // they can be processed.
 add_task(async function() {
--- a/devtools/client/debugger/new/test/mochitest/helpers.js
+++ b/devtools/client/debugger/new/test/mochitest/helpers.js
@@ -202,16 +202,26 @@ async function waitForElement(dbg, name,
   return findElement(dbg, name, ...args);
 }
 
 async function waitForElementWithSelector(dbg, selector) {
   await waitUntil(() => findElementWithSelector(dbg, selector));
   return findElementWithSelector(dbg, selector);
 }
 
+function waitForSelectedLocation(dbg, line ) {
+  return waitForState(
+    dbg,
+    state => {
+      const location = dbg.selectors.getSelectedLocation(state)
+      return location && location.line == line
+    }
+  );
+}
+
 function waitForSelectedSource(dbg, url) {
   const {
     getSelectedSource,
     hasSymbols,
     hasBreakpointPositions
   } = dbg.selectors;
 
   return waitForState(
--- a/devtools/client/shared/source-map/index.js
+++ b/devtools/client/shared/source-map/index.js
@@ -746,18 +746,22 @@ module.exports = __webpack_require__(182
 
 
 /* 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/>. */
 
 const md5 = __webpack_require__(105);
 
-function originalToGeneratedId(originalId) {
-  const match = originalId.match(/(.*)\/originalSource/);
+function originalToGeneratedId(sourceId) {
+  if (isGeneratedId(sourceId)) {
+    return sourceId;
+  }
+
+  const match = sourceId.match(/(.*)\/originalSource/);
   return match ? match[1] : "";
 }
 
 function generatedToOriginalId(generatedId, url) {
   return `${generatedId}/originalSource-${md5(url)}`;
 }
 
 function isOriginalId(id) {
--- a/devtools/client/shared/source-map/worker.js
+++ b/devtools/client/shared/source-map/worker.js
@@ -13215,18 +13215,22 @@ module.exports = {
 
 
 /* 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/>. */
 
 const md5 = __webpack_require__(105);
 
-function originalToGeneratedId(originalId) {
-  const match = originalId.match(/(.*)\/originalSource/);
+function originalToGeneratedId(sourceId) {
+  if (isGeneratedId(sourceId)) {
+    return sourceId;
+  }
+
+  const match = sourceId.match(/(.*)\/originalSource/);
   return match ? match[1] : "";
 }
 
 function generatedToOriginalId(generatedId, url) {
   return `${generatedId}/originalSource-${md5(url)}`;
 }
 
 function isOriginalId(id) {