Bug 1541631 - Part 8: Store breakable-line and breakpoint-position data in the source-actor store. r=jlast
☠☠ backed out by db9aaec44445 ☠ ☠
authorLogan Smyth <loganfsmyth@gmail.com>
Thu, 15 Aug 2019 16:29:44 +0000
changeset 488272 bc99fb7a9d125b00d33e54d002facbef7e8e3f2b
parent 488271 696e24030686ebe58588d5415b932077a6af0c11
child 488273 b66c924e9a1fc5ed5613707b56d13894f0a46518
push id36440
push userncsoregi@mozilla.com
push dateFri, 16 Aug 2019 03:57:48 +0000
treeherdermozilla-central@a58b7dc85887 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjlast
bugs1541631
milestone70.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 1541631 - Part 8: Store breakable-line and breakpoint-position data in the source-actor store. r=jlast Differential Revision: https://phabricator.services.mozilla.com/D42033
devtools/client/debugger/src/actions/ast/tests/setInScopeLines.spec.js
devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js
devtools/client/debugger/src/actions/breakpoints/tests/breakpointPositions.spec.js
devtools/client/debugger/src/actions/breakpoints/tests/breakpoints.spec.js
devtools/client/debugger/src/actions/pause/tests/pause.spec.js
devtools/client/debugger/src/actions/source-actors.js
devtools/client/debugger/src/actions/sources/breakableLines.js
devtools/client/debugger/src/actions/sources/tests/blackbox.spec.js
devtools/client/debugger/src/actions/sources/tests/loadSource.spec.js
devtools/client/debugger/src/actions/tests/ast.spec.js
devtools/client/debugger/src/actions/tests/expressions.spec.js
devtools/client/debugger/src/actions/tests/helpers/mockCommandClient.js
devtools/client/debugger/src/actions/tests/navigation.spec.js
devtools/client/debugger/src/actions/tests/pending-breakpoints.spec.js
devtools/client/debugger/src/actions/tests/preview.spec.js
devtools/client/debugger/src/actions/tests/project-text-search.spec.js
devtools/client/debugger/src/actions/tests/setProjectDirectoryRoot.spec.js
devtools/client/debugger/src/actions/types/SourceAction.js
devtools/client/debugger/src/actions/types/SourceActorAction.js
devtools/client/debugger/src/actions/utils/middleware/promise.js
devtools/client/debugger/src/client/firefox/commands.js
devtools/client/debugger/src/reducers/source-actors.js
devtools/client/debugger/src/reducers/sources.js
--- a/devtools/client/debugger/src/actions/ast/tests/setInScopeLines.spec.js
+++ b/devtools/client/debugger/src/actions/ast/tests/setInScopeLines.spec.js
@@ -23,18 +23,18 @@ const sourceTexts = {
 
 const mockCommandClient = {
   sourceContents: async ({ source }) => ({
     source: sourceTexts[source],
     contentType: "text/javascript",
   }),
   evaluateExpressions: async () => {},
   getFrameScopes: async () => {},
-  getBreakpointPositions: async () => ({}),
-  getBreakableLines: async () => [],
+  getSourceActorBreakpointPositions: async () => ({}),
+  getSourceActorBreakableLines: async () => [],
 };
 
 describe("getInScopeLine", () => {
   it("with selected line", async () => {
     const store = createStore(mockCommandClient);
     const { dispatch, getState } = store;
     const source = makeMockSource("scopes.js", "scopes.js");
 
--- a/devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js
+++ b/devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js
@@ -28,16 +28,17 @@ import type {
 
 import { makeBreakpointId } from "../../utils/breakpoint";
 import {
   memoizeableAction,
   type MemoizedAction,
 } from "../../utils/memoizableAction";
 import { fulfilled } from "../../utils/async-value";
 import type { ThunkArgs } from "../../actions/types";
+import { loadSourceActorBreakpointColumns } from "../source-actors";
 
 async function mapLocations(
   generatedLocations: SourceLocation[],
   { sourceMaps }: ThunkArgs
 ) {
   if (generatedLocations.length == 0) {
     return [];
   }
@@ -108,17 +109,17 @@ function groupByLine(results, sourceId, 
 
 async function _setBreakpointPositions(cx, sourceId, line, thunkArgs) {
   const { client, dispatch, getState, sourceMaps } = thunkArgs;
   let generatedSource = getSource(getState(), sourceId);
   if (!generatedSource) {
     return;
   }
 
-  let results = {};
+  const results = {};
   if (isOriginalId(sourceId)) {
     // Explicitly typing ranges is required to work around the following issue
     // https://github.com/facebook/flow/issues/5294
     const ranges: Range[] = await sourceMaps.getGeneratedRangesForOriginal(
       sourceId,
       generatedSource.url,
       true
     );
@@ -134,33 +135,48 @@ async function _setBreakpointPositions(c
       // in this case.
       if (range.end.column === Infinity) {
         range.end = {
           line: range.end.line + 1,
           column: 0,
         };
       }
 
-      const bps = await client.getBreakpointPositions(
-        getSourceActorsForSource(getState(), generatedSource.id),
-        range
+      const actorBps = await Promise.all(
+        getSourceActorsForSource(getState(), generatedSource.id).map(actor =>
+          client.getSourceActorBreakpointPositions(actor, range)
+        )
       );
-      for (const bpLine in bps) {
-        results[bpLine] = (results[bpLine] || []).concat(bps[bpLine]);
+
+      for (const actorPositions of actorBps) {
+        for (const rangeLine of Object.keys(actorPositions)) {
+          let columns = actorPositions[parseInt(rangeLine, 10)];
+          const existing = results[rangeLine];
+          if (existing) {
+            columns = [...new Set([...existing, ...columns])];
+          }
+
+          results[rangeLine] = columns;
+        }
       }
     }
   } else {
     if (typeof line !== "number") {
       throw new Error("Line is required for generated sources");
     }
 
-    results = await client.getBreakpointPositions(
-      getSourceActorsForSource(getState(), generatedSource.id),
-      { start: { line, column: 0 }, end: { line: line + 1, column: 0 } }
+    const actorColumns = await Promise.all(
+      getSourceActorsForSource(getState(), generatedSource.id).map(actor =>
+        dispatch(loadSourceActorBreakpointColumns({ id: actor.id, line }))
+      )
     );
+
+    for (const columns of actorColumns) {
+      results[line] = (results[line] || []).concat(columns);
+    }
   }
 
   let positions = convertToList(results, generatedSource);
   positions = await mapLocations(positions, thunkArgs);
 
   positions = filterBySource(positions, sourceId);
   positions = filterByUniqLocation(positions);
   positions = groupByLine(positions, sourceId, line);
--- a/devtools/client/debugger/src/actions/breakpoints/tests/breakpointPositions.spec.js
+++ b/devtools/client/debugger/src/actions/breakpoints/tests/breakpointPositions.spec.js
@@ -13,18 +13,18 @@ import {
 } from "../../../utils/test-head";
 import { createSource } from "../../tests/helpers/mockCommandClient";
 
 describe("breakpointPositions", () => {
   it("fetches positions", async () => {
     const fooContent = createSource("foo", "");
 
     const store = createStore({
-      getBreakpointPositions: async () => ({ "9": [1] }),
-      getBreakableLines: async () => [],
+      getSourceActorBreakpointPositions: async () => ({ "9": [1] }),
+      getSourceActorBreakableLines: async () => [],
       sourceContents: async () => fooContent,
     });
 
     const { dispatch, getState, cx } = store;
     const source = await dispatch(
       actions.newGeneratedSource(makeSource("foo"))
     );
     await dispatch(actions.loadSourceById(cx, source.id));
@@ -58,22 +58,22 @@ describe("breakpointPositions", () => {
   });
 
   it("doesn't re-fetch positions", async () => {
     const fooContent = createSource("foo", "");
 
     let resolve = _ => {};
     let count = 0;
     const store = createStore({
-      getBreakpointPositions: () =>
+      getSourceActorBreakpointPositions: () =>
         new Promise(r => {
           count++;
           resolve = r;
         }),
-      getBreakableLines: async () => [],
+      getSourceActorBreakableLines: async () => [],
       sourceContents: async () => fooContent,
     });
 
     const { dispatch, getState, cx } = store;
     const source = await dispatch(
       actions.newGeneratedSource(makeSource("foo"))
     );
     await dispatch(actions.loadSourceById(cx, source.id));
--- a/devtools/client/debugger/src/actions/breakpoints/tests/breakpoints.spec.js
+++ b/devtools/client/debugger/src/actions/breakpoints/tests/breakpoints.spec.js
@@ -12,18 +12,18 @@ import {
   getTelemetryEvents,
 } from "../../../utils/test-head";
 
 import { mockCommandClient } from "../../tests/helpers/mockCommandClient";
 
 function mockClient(positionsResponse = {}) {
   return {
     ...mockCommandClient,
-    getBreakpointPositions: async () => positionsResponse,
-    getBreakableLines: async () => [],
+    getSourceActorBreakpointPositions: async () => positionsResponse,
+    getSourceActorBreakableLines: async () => [],
   };
 }
 
 describe("breakpoints", () => {
   it("should add a breakpoint", async () => {
     const { dispatch, getState, cx } = createStore(mockClient({ "2": [1] }));
     const loc1 = {
       sourceId: "a",
--- a/devtools/client/debugger/src/actions/pause/tests/pause.spec.js
+++ b/devtools/client/debugger/src/actions/pause/tests/pause.spec.js
@@ -66,18 +66,18 @@ const mockCommandClient = {
         case "foo-wasm/originalSource":
           return resolve({
             source: "fn fooBar() {}\nfn barZoo() { fooBar() }",
             contentType: "text/rust",
           });
       }
     });
   },
-  getBreakpointPositions: async () => ({}),
-  getBreakableLines: async () => [],
+  getSourceActorBreakpointPositions: async () => ({}),
+  getSourceActorBreakableLines: async () => [],
   actorID: "threadActorID",
 };
 
 const mockFrameId = "1";
 
 function createPauseInfo(
   frameLocation = { sourceId: "foo1", line: 2 },
   frameOpts = {}
@@ -171,17 +171,17 @@ describe("pause", () => {
       dispatch(actions.stepOver(cx));
       expect(getNextStepSpy).toHaveBeenCalled();
       getNextStepSpy.mockRestore();
     });
 
     it("should step over when paused after an await", async () => {
       const store = createStore({
         ...mockCommandClient,
-        getBreakpointPositions: async () => ({ [2]: [1] }),
+        getSourceActorBreakpointPositions: async () => ({ [2]: [1] }),
       });
       const { dispatch, getState } = store;
       const mockPauseInfo = createPauseInfo({
         sourceId: "await",
         line: 2,
         column: 6,
       });
 
--- a/devtools/client/debugger/src/actions/source-actors.js
+++ b/devtools/client/debugger/src/actions/source-actors.js
@@ -1,16 +1,27 @@
 /* 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 { SourceActor } from "../reducers/source-actors";
+import {
+  getSourceActor,
+  getSourceActorBreakableLines,
+  getSourceActorBreakpointColumns,
+  type SourceActorId,
+  type SourceActor,
+} from "../reducers/source-actors";
+import {
+  memoizeableAction,
+  type MemoizedAction,
+} from "../utils/memoizableAction";
+import { PROMISE } from "./utils/middleware/promise";
 
 export function insertSourceActor(item: SourceActor) {
   return insertSourceActors([item]);
 }
 export function insertSourceActors(items: Array<SourceActor>) {
   return function({ dispatch }: ThunkArgs) {
     dispatch({
       type: "INSERT_SOURCE_ACTORS",
@@ -25,8 +36,53 @@ export function removeSourceActor(item: 
 export function removeSourceActors(items: Array<SourceActor>) {
   return function({ dispatch }: ThunkArgs) {
     dispatch({
       type: "REMOVE_SOURCE_ACTORS",
       items,
     });
   };
 }
+
+export const loadSourceActorBreakpointColumns: MemoizedAction<
+  { id: SourceActorId, line: number },
+  Array<number>
+> = memoizeableAction("loadSourceActorBreakpointColumns", {
+  createKey: ({ id, line }) => `${id}:${line}`,
+  getValue: ({ id, line }, { getState }) =>
+    getSourceActorBreakpointColumns(getState(), id, line),
+  action: async ({ id, line }, { dispatch, getState, client }) => {
+    await dispatch({
+      type: "SET_SOURCE_ACTOR_BREAKPOINT_COLUMNS",
+      sourceId: id,
+      line,
+      [PROMISE]: (async () => {
+        const positions = await client.getSourceActorBreakpointPositions(
+          getSourceActor(getState(), id),
+          {
+            start: { line, column: 0 },
+            end: { line: line + 1, column: 0 },
+          }
+        );
+
+        return positions[line] || [];
+      })(),
+    });
+  },
+});
+
+export const loadSourceActorBreakableLines: MemoizedAction<
+  { id: SourceActorId },
+  Array<number>
+> = memoizeableAction("loadSourceActorBreakableLines", {
+  createKey: args => args.id,
+  getValue: ({ id }, { getState }) =>
+    getSourceActorBreakableLines(getState(), id),
+  action: async ({ id }, { dispatch, getState, client }) => {
+    await dispatch({
+      type: "SET_SOURCE_ACTOR_BREAKABLE_LINES",
+      sourceId: id,
+      [PROMISE]: client.getSourceActorBreakableLines(
+        getSourceActor(getState(), id)
+      ),
+    });
+  },
+});
--- a/devtools/client/debugger/src/actions/sources/breakableLines.js
+++ b/devtools/client/debugger/src/actions/sources/breakableLines.js
@@ -5,16 +5,17 @@
 // @flow
 
 import { isOriginalId } from "devtools-source-map";
 import { getSourceActorsForSource, getBreakableLines } from "../../selectors";
 import { setBreakpointPositions } from "../breakpoints/breakpointPositions";
 import { union } from "lodash";
 import type { Context } from "../../types";
 import type { ThunkArgs } from "../../actions/types";
+import { loadSourceActorBreakableLines } from "../source-actors";
 
 function calculateBreakableLines(positions) {
   const lines = [];
   for (const line in positions) {
     if (positions[line].length > 0) {
       lines.push(Number(line));
     }
   }
@@ -25,27 +26,31 @@ function calculateBreakableLines(positio
 export function setBreakableLines(cx: Context, sourceId: string) {
   return async ({ getState, dispatch, client }: ThunkArgs) => {
     let breakableLines;
     if (isOriginalId(sourceId)) {
       const positions = await dispatch(
         setBreakpointPositions({ cx, sourceId })
       );
       breakableLines = calculateBreakableLines(positions);
+
+      const existingBreakableLines = getBreakableLines(getState(), sourceId);
+      if (existingBreakableLines) {
+        breakableLines = union(existingBreakableLines, breakableLines);
+      }
+
+      dispatch({
+        type: "SET_ORIGINAL_BREAKABLE_LINES",
+        cx,
+        sourceId,
+        breakableLines,
+      });
     } else {
-      breakableLines = await client.getBreakableLines(
-        getSourceActorsForSource(getState(), sourceId)
+      const actors = getSourceActorsForSource(getState(), sourceId);
+
+      await Promise.all(
+        actors.map(actor =>
+          dispatch(loadSourceActorBreakableLines({ id: actor.id }))
+        )
       );
     }
-
-    const existingBreakableLines = getBreakableLines(getState(), sourceId);
-    if (existingBreakableLines) {
-      breakableLines = union(existingBreakableLines, breakableLines);
-    }
-
-    dispatch({
-      type: "SET_BREAKABLE_LINES",
-      cx,
-      sourceId,
-      breakableLines,
-    });
   };
 }
--- a/devtools/client/debugger/src/actions/sources/tests/blackbox.spec.js
+++ b/devtools/client/debugger/src/actions/sources/tests/blackbox.spec.js
@@ -10,17 +10,17 @@ import {
   createStore,
   makeSource,
 } from "../../../utils/test-head";
 
 describe("blackbox", () => {
   it("should blackbox a source", async () => {
     const store = createStore({
       blackBox: async () => true,
-      getBreakableLines: async () => [],
+      getSourceActorBreakableLines: async () => [],
     });
     const { dispatch, getState, cx } = store;
 
     const foo1Source = await dispatch(
       actions.newGeneratedSource(makeSource("foo1"))
     );
     await dispatch(actions.toggleBlackBox(cx, foo1Source));
 
--- a/devtools/client/debugger/src/actions/sources/tests/loadSource.spec.js
+++ b/devtools/client/debugger/src/actions/sources/tests/loadSource.spec.js
@@ -56,18 +56,18 @@ describe("loadSourceText", () => {
   it("should update breakpoint text when a source loads", async () => {
     const fooOrigContent = createSource("fooOrig", "var fooOrig = 42;");
     const fooGenContent = createSource("fooGen", "var fooGen = 42;");
 
     const store = createStore(
       {
         ...mockCommandClient,
         sourceContents: async () => fooGenContent,
-        getBreakpointPositions: async () => ({ "1": [0] }),
-        getBreakableLines: async () => [],
+        getSourceActorBreakpointPositions: async () => ({ "1": [0] }),
+        getSourceActorBreakableLines: async () => [],
       },
       {},
       {
         getGeneratedRangesForOriginal: async () => [
           { start: { line: 1, column: 0 }, end: { line: 1, column: 1 } },
         ],
         getOriginalLocations: async (sourceId, items) =>
           items.map(item => ({
@@ -151,18 +151,18 @@ describe("loadSourceText", () => {
     let resolve;
     let count = 0;
     const { dispatch, getState, cx } = createStore({
       sourceContents: () =>
         new Promise(r => {
           count++;
           resolve = r;
         }),
-      getBreakpointPositions: async () => ({}),
-      getBreakableLines: async () => [],
+      getSourceActorBreakpointPositions: async () => ({}),
+      getSourceActorBreakableLines: async () => [],
     });
     const id = "foo";
 
     await dispatch(actions.newGeneratedSource(makeSource(id)));
 
     let source = selectors.getSourceFromId(getState(), id);
     dispatch(actions.loadSourceText({ cx, source }));
 
@@ -189,18 +189,18 @@ describe("loadSourceText", () => {
     let resolve;
     let count = 0;
     const { dispatch, getState, cx } = createStore({
       sourceContents: () =>
         new Promise(r => {
           count++;
           resolve = r;
         }),
-      getBreakpointPositions: async () => ({}),
-      getBreakableLines: async () => [],
+      getSourceActorBreakpointPositions: async () => ({}),
+      getSourceActorBreakableLines: async () => [],
     });
     const id = "foo";
 
     await dispatch(actions.newGeneratedSource(makeSource(id)));
     let source = selectors.getSourceFromId(getState(), id);
     const loading = dispatch(actions.loadSourceText({ cx, source }));
 
     if (!resolve) {
--- a/devtools/client/debugger/src/actions/tests/ast.spec.js
+++ b/devtools/client/debugger/src/actions/tests/ast.spec.js
@@ -12,27 +12,27 @@ import {
   makeSource,
   makeOriginalSource,
   waitForState,
 } from "../../utils/test-head";
 
 import readFixture from "./helpers/readFixture";
 const { getSymbols, isSymbolsLoading, getFramework } = selectors;
 
-const threadFront = {
+const mockCommandClient = {
   sourceContents: async ({ source }) => ({
     source: sourceTexts[source],
     contentType: "text/javascript",
   }),
   getFrameScopes: async () => {},
   evaluate: async expression => ({ result: evaluationResult[expression] }),
   evaluateExpressions: async expressions =>
     expressions.map(expression => ({ result: evaluationResult[expression] })),
-  getBreakpointPositions: async () => ({}),
-  getBreakableLines: async () => [],
+  getSourceActorBreakpointPositions: async () => ({}),
+  getSourceActorBreakableLines: async () => [],
 };
 
 const sourceMaps = {
   getOriginalSourceText: async ({ id }) => ({
     id,
     text: sourceTexts[id],
     contentType: "text/javascript",
   }),
@@ -51,17 +51,17 @@ const evaluationResult = {
   "this.bazz": { actor: "bazz", preview: {} },
   this: { actor: "this", preview: {} },
 };
 
 describe("ast", () => {
   describe("setSymbols", () => {
     describe("when the source is loaded", () => {
       it("should be able to set symbols", async () => {
-        const store = createStore(threadFront);
+        const store = createStore(mockCommandClient);
         const { dispatch, getState, cx } = store;
         const base = await dispatch(
           actions.newGeneratedSource(makeSource("base.js"))
         );
         await dispatch(actions.loadSourceText({ cx, source: base }));
 
         const loadedSource = selectors.getSourceFromId(getState(), base.id);
         await dispatch(actions.setSymbols({ cx, source: loadedSource }));
@@ -69,37 +69,37 @@ describe("ast", () => {
 
         const baseSymbols = getSymbols(getState(), base);
         expect(baseSymbols).toMatchSnapshot();
       });
     });
 
     describe("when the source is not loaded", () => {
       it("should return null", async () => {
-        const { getState, dispatch } = createStore(threadFront);
+        const { getState, dispatch } = createStore(mockCommandClient);
         const base = await dispatch(
           actions.newGeneratedSource(makeSource("base.js"))
         );
 
         const baseSymbols = getSymbols(getState(), base);
         expect(baseSymbols).toEqual(null);
       });
     });
 
     describe("when there is no source", () => {
       it("should return null", async () => {
-        const { getState } = createStore(threadFront);
+        const { getState } = createStore(mockCommandClient);
         const baseSymbols = getSymbols(getState());
         expect(baseSymbols).toEqual(null);
       });
     });
 
     describe("frameworks", () => {
       it("should detect react components", async () => {
-        const store = createStore(threadFront, {}, sourceMaps);
+        const store = createStore(mockCommandClient, {}, sourceMaps);
         const { cx, dispatch, getState } = store;
 
         const genSource = await dispatch(
           actions.newGeneratedSource(makeSource("reactComponent.js"))
         );
 
         const source = await dispatch(
           actions.newOriginalSource(makeOriginalSource(genSource))
@@ -108,17 +108,17 @@ describe("ast", () => {
         await dispatch(actions.loadSourceText({ cx, source }));
         const loadedSource = selectors.getSourceFromId(getState(), source.id);
         await dispatch(actions.setSymbols({ cx, source: loadedSource }));
 
         expect(getFramework(getState(), source)).toBe("React");
       });
 
       it("should not give false positive on non react components", async () => {
-        const store = createStore(threadFront);
+        const store = createStore(mockCommandClient);
         const { cx, dispatch, getState } = store;
         const base = await dispatch(
           actions.newGeneratedSource(makeSource("base.js"))
         );
         await dispatch(actions.loadSourceText({ cx, source: base }));
         await dispatch(actions.setSymbols({ cx, source: base }));
 
         expect(getFramework(getState(), base)).toBe(undefined);
--- a/devtools/client/debugger/src/actions/tests/expressions.spec.js
+++ b/devtools/client/debugger/src/actions/tests/expressions.spec.js
@@ -32,18 +32,18 @@ const mockThreadFront = {
             } else {
               resolve("boo");
             }
           })
       )
     ),
   getFrameScopes: async () => {},
   sourceContents: () => ({ source: "", contentType: "text/javascript" }),
-  getBreakpointPositions: async () => ({}),
-  getBreakableLines: async () => [],
+  getSourceActorBreakpointPositions: async () => ({}),
+  getSourceActorBreakableLines: async () => [],
   autocomplete: () => {
     return new Promise(resolve => {
       resolve({
         from: "foo",
         matches: ["toLocaleString", "toSource", "toString", "toolbar", "top"],
         matchProp: "to",
       });
     });
--- a/devtools/client/debugger/src/actions/tests/helpers/mockCommandClient.js
+++ b/devtools/client/debugger/src/actions/tests/helpers/mockCommandClient.js
@@ -44,11 +44,11 @@ export const mockCommandClient = {
       reject(`unknown source: ${source}`);
     });
   },
   setBreakpoint: async () => {},
   removeBreakpoint: (_id: string) => Promise.resolve(),
   threadFront: async () => {},
   getFrameScopes: async () => {},
   evaluateExpressions: async () => {},
-  getBreakpointPositions: async () => ({}),
-  getBreakableLines: async () => [],
+  getSourceActorBreakpointPositions: async () => ({}),
+  getSourceActorBreakableLines: async () => [],
 };
--- a/devtools/client/debugger/src/actions/tests/navigation.spec.js
+++ b/devtools/client/debugger/src/actions/tests/navigation.spec.js
@@ -22,18 +22,18 @@ const {
   getFileSearchResults,
 } = selectors;
 
 const threadFront = {
   sourceContents: async () => ({
     source: "function foo1() {\n  const foo = 5; return foo;\n}",
     contentType: "text/javascript",
   }),
-  getBreakpointPositions: async () => ({}),
-  getBreakableLines: async () => [],
+  getSourceActorBreakpointPositions: async () => ({}),
+  getSourceActorBreakableLines: async () => [],
   detachWorkers: () => {},
 };
 
 describe("navigation", () => {
   it("connect sets the debuggeeUrl", async () => {
     const { dispatch, getState } = createStore({
       fetchWorkers: () => Promise.resolve([]),
       getMainThread: () => "FakeThread",
--- a/devtools/client/debugger/src/actions/tests/pending-breakpoints.spec.js
+++ b/devtools/client/debugger/src/actions/tests/pending-breakpoints.spec.js
@@ -44,18 +44,18 @@ import {
 
 import sourceMaps from "devtools-source-map";
 
 import { makePendingLocationId } from "../../utils/breakpoint";
 function mockClient(bpPos = {}) {
   return {
     ...mockCommandClient,
 
-    getBreakpointPositions: async () => bpPos,
-    getBreakableLines: async () => [],
+    getSourceActorBreakpointPositions: async () => bpPos,
+    getSourceActorBreakableLines: async () => [],
   };
 }
 
 function mockSourceMaps() {
   return {
     ...sourceMaps,
     getOriginalSourceText: async source => ({
       id: source.id,
--- a/devtools/client/debugger/src/actions/tests/preview.spec.js
+++ b/devtools/client/debugger/src/actions/tests/preview.spec.js
@@ -26,18 +26,18 @@ function waitForPreview(store, expressio
 function mockThreadFront(overrides) {
   return {
     evaluateInFrame: async () => ({ result: {} }),
     getFrameScopes: async () => {},
     sourceContents: async () => ({
       source: "",
       contentType: "text/javascript",
     }),
-    getBreakpointPositions: async () => ({}),
-    getBreakableLines: async () => [],
+    getSourceActorBreakpointPositions: async () => ({}),
+    getSourceActorBreakableLines: async () => [],
     evaluateExpressions: async () => [],
     loadObjectProperties: async () => ({}),
     ...overrides,
   };
 }
 
 function dispatchSetPreview(dispatch, context, expression, target) {
   return dispatch(
--- a/devtools/client/debugger/src/actions/tests/project-text-search.spec.js
+++ b/devtools/client/debugger/src/actions/tests/project-text-search.spec.js
@@ -34,18 +34,18 @@ const sources = {
   "bar:formatted": {
     source: "function bla(x, y) {\n const bar = 4; return 2;\n}",
     contentType: "text/javascript",
   },
 };
 
 const threadFront = {
   sourceContents: async ({ source }) => sources[source],
-  getBreakpointPositions: async () => ({}),
-  getBreakableLines: async () => [],
+  getSourceActorBreakpointPositions: async () => ({}),
+  getSourceActorBreakableLines: async () => [],
 };
 
 describe("project text search", () => {
   it("should add a project text search query", () => {
     const { dispatch, getState, cx } = createStore();
     const mockQuery = "foo";
 
     dispatch(actions.addSearchQuery(cx, mockQuery));
--- a/devtools/client/debugger/src/actions/tests/setProjectDirectoryRoot.spec.js
+++ b/devtools/client/debugger/src/actions/tests/setProjectDirectoryRoot.spec.js
@@ -38,17 +38,17 @@ describe("setProjectDirectoryRoot", () =
     dispatch(actions.setProjectDirectoryRoot(cx, "/example.com/foo"));
     dispatch(actions.clearProjectDirectoryRoot(cx));
     dispatch(actions.setProjectDirectoryRoot(cx, "/example.com/bar"));
     expect(getProjectDirectoryRoot(getState())).toBe("/example.com/bar");
   });
 
   it("should filter sources", async () => {
     const store = createStore({
-      getBreakableLines: async () => [],
+      getSourceActorBreakableLines: async () => [],
     });
     const { dispatch, getState, cx } = store;
     await dispatch(actions.newGeneratedSource(makeSource("js/scopes.js")));
     await dispatch(actions.newGeneratedSource(makeSource("lib/vendor.js")));
 
     dispatch(actions.setProjectDirectoryRoot(cx, "localhost:8000/examples/js"));
 
     const filteredSourcesByThread = getDisplayedSources(getState());
@@ -60,24 +60,24 @@ describe("setProjectDirectoryRoot", () =
       "http://localhost:8000/examples/js/scopes.js"
     );
 
     expect(filteredSources.relativeUrl).toEqual("scopes.js");
   });
 
   it("should update the child directory ", () => {
     const { dispatch, getState, cx } = createStore({
-      getBreakableLines: async () => [],
+      getSourceActorBreakableLines: async () => [],
     });
     dispatch(actions.setProjectDirectoryRoot(cx, "example.com"));
     dispatch(actions.setProjectDirectoryRoot(cx, "example.com/foo/bar"));
     expect(getProjectDirectoryRoot(getState())).toBe("example.com/foo/bar");
   });
 
   it("should update the child directory when domain name is Webpack://", () => {
     const { dispatch, getState, cx } = createStore({
-      getBreakableLines: async () => [],
+      getSourceActorBreakableLines: async () => [],
     });
     dispatch(actions.setProjectDirectoryRoot(cx, "webpack://"));
     dispatch(actions.setProjectDirectoryRoot(cx, "webpack:///app"));
     expect(getProjectDirectoryRoot(getState())).toBe("webpack:///app");
   });
 });
--- a/devtools/client/debugger/src/actions/types/SourceAction.js
+++ b/devtools/client/debugger/src/actions/types/SourceAction.js
@@ -72,13 +72,13 @@ export type SourceAction =
       +tabs: any,
     |}
   | {|
       +type: "CLOSE_TABS",
       +sources: Array<Source>,
       +tabs: any,
     |}
   | {|
-      type: "SET_BREAKABLE_LINES",
+      type: "SET_ORIGINAL_BREAKABLE_LINES",
       +cx: Context,
       breakableLines: number[],
       sourceId: string,
     |};
--- a/devtools/client/debugger/src/actions/types/SourceActorAction.js
+++ b/devtools/client/debugger/src/actions/types/SourceActorAction.js
@@ -1,20 +1,43 @@
 /* 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 { SourceActor } from "../../reducers/source-actors.js";
+import { type PromiseAction } from "../utils/middleware/promise";
+import type {
+  SourceActorId,
+  SourceActor,
+} from "../../reducers/source-actors.js";
 
 export type SourceActorsInsertAction = {|
   type: "INSERT_SOURCE_ACTORS",
   items: Array<SourceActor>,
 |};
 export type SourceActorsRemoveAction = {|
   type: "REMOVE_SOURCE_ACTORS",
   items: Array<SourceActor>,
 |};
 
+export type SourceActorBreakpointColumnsAction = PromiseAction<
+  {|
+    type: "SET_SOURCE_ACTOR_BREAKPOINT_COLUMNS",
+    sourceId: SourceActorId,
+    line: number,
+  |},
+  Array<number>
+>;
+
+export type SourceActorBreakableLinesAction = PromiseAction<
+  {|
+    type: "SET_SOURCE_ACTOR_BREAKABLE_LINES",
+    sourceId: SourceActorId,
+  |},
+  Array<number>
+>;
+
 export type SourceActorAction =
   | SourceActorsInsertAction
-  | SourceActorsRemoveAction;
+  | SourceActorsRemoveAction
+  | SourceActorBreakpointColumnsAction
+  | SourceActorBreakableLinesAction;
--- a/devtools/client/debugger/src/actions/utils/middleware/promise.js
+++ b/devtools/client/debugger/src/actions/utils/middleware/promise.js
@@ -1,17 +1,16 @@
 /* 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 { fromPairs, toPairs } from "lodash";
 import { executeSoon } from "../../../utils/DevToolsUtils";
-
 import type { ThunkArgs } from "../../types";
 
 type BasePromiseAction = {|
   +"@@dispatch/promise": Promise<mixed>,
 |};
 
 export type StartPromiseAction = {|
   ...BasePromiseAction,
@@ -25,17 +24,35 @@ export type DonePromiseAction = {|
 |};
 
 export type ErrorPromiseAction = {|
   ...BasePromiseAction,
   +status: "error",
   +error: any,
 |};
 
-export type PromiseAction<Action, Value = any> =
+import {
+  pending,
+  rejected,
+  fulfilled,
+  type AsyncValue,
+} from "../../../utils/async-value";
+export function asyncActionAsValue<T>(
+  action: PromiseAction<mixed, T>
+): AsyncValue<T> {
+  if (action.status === "start") {
+    return pending();
+  }
+  if (action.status === "error") {
+    return rejected(action.error);
+  }
+  return fulfilled(action.value);
+}
+
+export type PromiseAction<+Action, Value = any> =
   // | {| ...Action, "@@dispatch/promise": Promise<Object> |}
   | {|
       ...BasePromiseAction,
       ...Action,
       +status: "start",
       value: void,
     |}
   | {|
--- a/devtools/client/debugger/src/client/firefox/commands.js
+++ b/devtools/client/debugger/src/client/firefox/commands.js
@@ -444,65 +444,48 @@ async function fetchWorkers(): Promise<W
   const { workers } = await tabTarget.listWorkers();
   return workers;
 }
 
 function getMainThread() {
   return threadFront.actor;
 }
 
-async function getBreakpointPositions(
-  actors: Array<SourceActor>,
-  range: ?Range
-): Promise<{ [string]: number[] }> {
-  const sourcePositions = {};
-
-  for (const { thread, actor } of actors) {
-    const sourceThreadFront = lookupThreadFront(thread);
-    const sourceFront = sourceThreadFront.source({ actor });
-    const positions = await sourceFront.getBreakpointPositionsCompressed(range);
-
-    for (const line of Object.keys(positions)) {
-      let columns = positions[line];
-      const existing = sourcePositions[line];
-      if (existing) {
-        columns = [...new Set([...existing, ...columns])];
-      }
-
-      sourcePositions[line] = columns;
-    }
-  }
-  return sourcePositions;
+async function getSourceActorBreakpointPositions(
+  { thread, actor }: SourceActor,
+  range: Range
+): Promise<{ [number]: number[] }> {
+  const sourceThreadFront = lookupThreadFront(thread);
+  const sourceFront = sourceThreadFront.source({ actor });
+  return sourceFront.getBreakpointPositionsCompressed(range);
 }
 
-async function getBreakableLines(actors: Array<SourceActor>) {
-  let lines = [];
-  for (const { thread, actor } of actors) {
-    const sourceThreadFront = lookupThreadFront(thread);
-    const sourceFront = sourceThreadFront.source({ actor });
-    let actorLines = [];
-    try {
-      actorLines = await sourceFront.getBreakableLines();
-    } catch (e) {
-      // Handle backward compatibility
-      if (
-        e.message &&
-        e.message.match(/does not recognize the packet type getBreakableLines/)
-      ) {
-        const pos = await sourceFront.getBreakpointPositionsCompressed();
-        actorLines = Object.keys(pos).map(line => Number(line));
-      } else if (!e.message || !e.message.match(/Connection closed/)) {
-        throw e;
-      }
+async function getSourceActorBreakableLines({
+  thread,
+  actor,
+}: SourceActor): Promise<Array<number>> {
+  const sourceThreadFront = lookupThreadFront(thread);
+  const sourceFront = sourceThreadFront.source({ actor });
+  let actorLines = [];
+  try {
+    actorLines = await sourceFront.getBreakableLines();
+  } catch (e) {
+    // Handle backward compatibility
+    if (
+      e.message &&
+      e.message.match(/does not recognize the packet type getBreakableLines/)
+    ) {
+      const pos = await sourceFront.getBreakpointPositionsCompressed();
+      actorLines = Object.keys(pos).map(line => Number(line));
+    } else if (!e.message || !e.message.match(/Connection closed/)) {
+      throw e;
     }
-
-    lines = [...new Set([...lines, ...actorLines])];
   }
 
-  return lines;
+  return actorLines;
 }
 
 const clientCommands = {
   autocomplete,
   blackBox,
   createObjectClient,
   loadObjectProperties,
   releaseActor,
@@ -512,18 +495,18 @@ const clientCommands = {
   stepIn,
   stepOut,
   stepOver,
   rewind,
   reverseStepOver,
   breakOnNext,
   sourceContents,
   getSourceForActor,
-  getBreakpointPositions,
-  getBreakableLines,
+  getSourceActorBreakpointPositions,
+  getSourceActorBreakableLines,
   hasBreakpoint,
   setBreakpoint,
   setXHRBreakpoint,
   removeXHRBreakpoint,
   removeBreakpoint,
   evaluate,
   evaluateInFrame,
   evaluateExpressions,
--- a/devtools/client/debugger/src/reducers/source-actors.js
+++ b/devtools/client/debugger/src/reducers/source-actors.js
@@ -2,29 +2,44 @@
  * 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 { Action } from "../actions/types";
 import type { SourceId, ThreadId } from "../types";
 import {
+  asSettled,
+  type AsyncValue,
+  type SettledValue,
+} from "../utils/async-value";
+import {
   createInitial,
   insertResources,
+  updateResources,
   removeResources,
   hasResource,
   getResource,
+  getMappedResource,
+  makeWeakQuery,
   makeIdQuery,
   makeReduceAllQuery,
   type Resource,
   type ResourceState,
+  type WeakQuery,
   type IdQuery,
   type ReduceAllQuery,
 } from "../utils/resource";
 
+import { asyncActionAsValue } from "../actions/utils/middleware/promise";
+import type {
+  SourceActorBreakpointColumnsAction,
+  SourceActorBreakableLinesAction,
+} from "../actions/types/SourceActorAction";
+
 export opaque type SourceActorId: string = string;
 export type SourceActor = {|
   +id: SourceActorId,
   +actor: string,
   +thread: ThreadId,
   +source: SourceId,
 
   +isBlackBoxed: boolean,
@@ -42,49 +57,107 @@ export type SourceActor = {|
 
   // The debugger's Debugger.Source API provides type information for the
   // cause of this source's creation.
   +introductionType: string | null,
 |};
 
 type SourceActorResource = Resource<{
   ...SourceActor,
+
+  // The list of breakpoint positions on each line of the file.
+  breakpointPositions: Map<number, AsyncValue<Array<number>>>,
+
+  // The list of lines that contain breakpoints.
+  breakableLines: AsyncValue<Array<number>> | null,
 }>;
 export type SourceActorsState = ResourceState<SourceActorResource>;
 export type SourceActorOuterState = { sourceActors: SourceActorsState };
 
 const initial: SourceActorsState = createInitial();
 
 export default function update(
   state: SourceActorsState = initial,
   action: Action
 ): SourceActorsState {
   switch (action.type) {
     case "INSERT_SOURCE_ACTORS": {
       const { items } = action;
-      state = insertResources(state, items);
+      state = insertResources(
+        state,
+        items.map(item => ({
+          ...item,
+          breakpointPositions: new Map(),
+          breakableLines: null,
+        }))
+      );
       break;
     }
     case "REMOVE_SOURCE_ACTORS": {
       const { items } = action;
       state = removeResources(state, items);
       break;
     }
 
     case "NAVIGATE": {
       state = initial;
       break;
     }
+
+    case "SET_SOURCE_ACTOR_BREAKPOINT_COLUMNS":
+      state = updateBreakpointColumns(state, action);
+      break;
+
+    case "SET_SOURCE_ACTOR_BREAKABLE_LINES":
+      state = updateBreakableLines(state, action);
+      break;
   }
 
   return state;
 }
 
-export function resourceAsSourceActor(r: SourceActorResource): SourceActor {
-  return r;
+function updateBreakpointColumns(
+  state: SourceActorsState,
+  action: SourceActorBreakpointColumnsAction
+): SourceActorsState {
+  const { sourceId, line } = action;
+  const value = asyncActionAsValue(action);
+
+  if (!hasResource(state, sourceId)) {
+    return state;
+  }
+
+  const breakpointPositions = new Map(
+    getResource(state, sourceId).breakpointPositions
+  );
+  breakpointPositions.set(line, value);
+
+  return updateResources(state, [{ id: sourceId, breakpointPositions }]);
+}
+
+function updateBreakableLines(
+  state: SourceActorsState,
+  action: SourceActorBreakableLinesAction
+): SourceActorsState {
+  const value = asyncActionAsValue(action);
+  const { sourceId } = action;
+
+  if (!hasResource(state, sourceId)) {
+    return state;
+  }
+
+  return updateResources(state, [{ id: sourceId, breakableLines: value }]);
+}
+
+export function resourceAsSourceActor({
+  breakpointPositions,
+  breakableLines,
+  ...sourceActor
+}: SourceActorResource): SourceActor {
+  return sourceActor;
 }
 
 // Because we are using an opaque type for our source actor IDs, these
 // functions are required to convert back and forth in order to get a string
 // version of the IDs. That should be super rarely used, but it means that
 // we can very easily see where we're relying on the string version of IDs.
 export function stringToSourceActorId(s: string): SourceActorId {
   return s;
@@ -96,17 +169,17 @@ export function hasSourceActor(
 ): boolean {
   return hasResource(state.sourceActors, id);
 }
 
 export function getSourceActor(
   state: SourceActorOuterState,
   id: SourceActorId
 ): SourceActor {
-  return getResource(state.sourceActors, id);
+  return getMappedResource(state.sourceActors, id, resourceAsSourceActor);
 }
 
 /**
  * Get all of the source actors for a set of IDs. Caches based on the identity
  * of "ids" when possible.
  */
 const querySourceActorsById: IdQuery<
   SourceActorResource,
@@ -161,8 +234,45 @@ const queryThreadsBySourceObject: Reduce
     }, {})
 );
 
 export function getThreadsBySource(
   state: SourceActorOuterState
 ): { [SourceId]: Array<ThreadId> } {
   return queryThreadsBySourceObject(state.sourceActors);
 }
+
+export function getSourceActorBreakableLines(
+  state: SourceActorOuterState,
+  id: SourceActorId
+): SettledValue<Array<number>> | null {
+  const { breakableLines } = getResource(state.sourceActors, id);
+
+  return asSettled(breakableLines);
+}
+
+export function getSourceActorBreakpointColumns(
+  state: SourceActorOuterState,
+  id: SourceActorId,
+  line: number
+): SettledValue<Array<number>> | null {
+  const { breakpointPositions } = getResource(state.sourceActors, id);
+
+  return asSettled(breakpointPositions.get(line) || null);
+}
+
+export const getBreakableLinesForSourceActors: WeakQuery<
+  SourceActorResource,
+  Array<SourceActorId>,
+  Array<number>
+> = makeWeakQuery({
+  filter: (state, ids) => ids,
+  map: ({ breakableLines }) => breakableLines,
+  reduce: items =>
+    Array.from(
+      items.reduce((acc, item) => {
+        if (item && item.state === "fulfilled") {
+          acc = acc.concat(item.value);
+        }
+        return acc;
+      }, [])
+    ),
+});
--- a/devtools/client/debugger/src/reducers/sources.js
+++ b/devtools/client/debugger/src/reducers/sources.js
@@ -42,16 +42,17 @@ import type { AsyncValue, SettledValue }
 import { originalToGeneratedId } from "devtools-source-map";
 import { prefs } from "../utils/prefs";
 
 import {
   hasSourceActor,
   getSourceActor,
   getSourceActors,
   getThreadsBySource,
+  getBreakableLinesForSourceActors,
   type SourceActorId,
   type SourceActorOuterState,
 } from "./source-actors";
 import type {
   Source,
   SourceId,
   SourceActor,
   SourceLocation,
@@ -211,17 +212,17 @@ function update(
         updateBlackBoxList(url, isBlackBoxed);
         return updateBlackboxFlag(state, id, isBlackBoxed);
       }
       break;
 
     case "SET_PROJECT_DIRECTORY_ROOT":
       return updateProjectDirectoryRoot(state, action.url);
 
-    case "SET_BREAKABLE_LINES": {
+    case "SET_ORIGINAL_BREAKABLE_LINES": {
       const { breakableLines, sourceId } = action;
       return {
         ...state,
         breakableLines: {
           ...state.breakableLines,
           [sourceId]: breakableLines,
         },
       };
@@ -959,28 +960,44 @@ export function getBreakpointPositionsFo
   state: OuterState,
   location: SourceLocation
 ): ?MappedLocation {
   const { sourceId } = location;
   const positions = getBreakpointPositionsForSource(state, sourceId);
   return findPosition(positions, location);
 }
 
-export function getBreakableLines(state: OuterState, sourceId: string) {
+export function getBreakableLines(
+  state: OuterState & SourceActorOuterState,
+  sourceId: string
+): ?Array<number> {
   if (!sourceId) {
     return null;
   }
+  const source = getSource(state, sourceId);
+  if (!source) {
+    return null;
+  }
 
-  return state.sources.breakableLines[sourceId];
+  if (isOriginalSource(source)) {
+    return state.sources.breakableLines[sourceId];
+  }
+
+  // We pull generated file breakable lines directly from the source actors
+  // so that breakable lines can be added as new source actors on HTML loads.
+  return getBreakableLinesForSourceActors(
+    state.sourceActors,
+    state.sources.actors[sourceId]
+  );
 }
 
 export const getSelectedBreakableLines: Selector<Set<number>> = createSelector(
   state => {
     const sourceId = getSelectedSourceId(state);
-    return sourceId && state.sources.breakableLines[sourceId];
+    return sourceId && getBreakableLines(state, sourceId);
   },
   breakableLines => new Set(breakableLines || [])
 );
 
 export function isSourceLoadingOrLoaded(state: OuterState, sourceId: string) {
   const { content } = getResource(state.sources.sources, sourceId);
   return content !== null;
 }