Bug 1537779 - Improve column breakpoint UI performance. r=loganfsmyth
authorJason Laster <jlaster@mozilla.com>
Thu, 02 May 2019 19:21:21 +0000
changeset 531158 ff20472890a1e3fa12e9b24bd9ef0520faea54da
parent 531157 8049711eaaf944348ba3097264b98af40ed337f1
child 531159 37d758a90ed9fc26c1d09993ddf73823295ea5d0
push id11265
push userffxbld-merge
push dateMon, 13 May 2019 10:53:39 +0000
treeherdermozilla-beta@77e0fe8dbdd3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersloganfsmyth
bugs1537779
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 1537779 - Improve column breakpoint UI performance. r=loganfsmyth Differential Revision: https://phabricator.services.mozilla.com/D28868
devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js
devtools/client/debugger/src/actions/breakpoints/modify.js
devtools/client/debugger/src/actions/breakpoints/syncBreakpoint.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/sources/breakableLines.js
devtools/client/debugger/src/actions/sources/index.js
devtools/client/debugger/src/actions/sources/loadSourceText.js
devtools/client/debugger/src/actions/sources/moz.build
devtools/client/debugger/src/actions/sources/newSources.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/threadClient.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/project-text-search.spec.js
devtools/client/debugger/src/actions/tests/setProjectDirectoryRoot.spec.js
devtools/client/debugger/src/actions/tests/ui.spec.js
devtools/client/debugger/src/actions/types/SourceAction.js
devtools/client/debugger/src/actions/types/index.js
devtools/client/debugger/src/client/firefox/commands.js
devtools/client/debugger/src/client/firefox/types.js
devtools/client/debugger/src/reducers/ast.js
devtools/client/debugger/src/reducers/breakpoints.js
devtools/client/debugger/src/reducers/sources.js
devtools/client/debugger/src/selectors/test/visibleColumnBreakpoints.spec.js
devtools/client/debugger/src/selectors/visibleColumnBreakpoints.js
devtools/client/debugger/src/test/tests-setup.js
devtools/client/debugger/src/types.js
devtools/client/debugger/src/utils/breakable-lines.js
devtools/client/debugger/src/utils/breakpoint/breakpointPositions.js
devtools/client/debugger/src/utils/context.js
devtools/client/debugger/src/utils/moz.build
devtools/client/debugger/src/utils/source.js
devtools/client/debugger/src/utils/tests/breakable-lines.spec.js
devtools/client/debugger/test/mochitest/browser_dbg-editor-highlight.js
devtools/client/debugger/test/mochitest/browser_dbg-inline-cache.js
devtools/client/debugger/test/mochitest/browser_dbg-tabs.js
devtools/client/debugger/test/mochitest/helpers.js
devtools/server/actors/source.js
devtools/shared/specs/source.js
--- a/devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js
+++ b/devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js
@@ -1,45 +1,48 @@
 /* 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 SourceMaps, {
+import {
   isOriginalId,
+  isGeneratedId,
   originalToGeneratedId
 } from "devtools-source-map";
 import { uniqBy, zip } from "lodash";
 
 import {
   getSource,
   getSourceFromId,
   hasBreakpointPositions,
+  hasBreakpointPositionsForLine,
   getBreakpointPositionsForSource,
   getSourceActorsForSource
 } from "../../selectors";
 
 import type {
-  SourceId,
   MappedLocation,
   Range,
   SourceLocation,
   BreakpointPositions,
   Context
 } from "../../types";
+
 import { makeBreakpointId } from "../../utils/breakpoint";
 import {
   memoizeableAction,
   type MemoizedAction
 } from "../../utils/memoizableAction";
+import type { ThunkArgs } from "../../actions/types";
 
 async function mapLocations(
   generatedLocations: SourceLocation[],
-  { sourceMaps }: { sourceMaps: typeof SourceMaps }
+  { sourceMaps }: ThunkArgs
 ) {
   const originalLocations = await sourceMaps.getOriginalLocations(
     generatedLocations
   );
 
   return zip(originalLocations, generatedLocations).map(
     ([location, generatedLocation]) => ({ location, generatedLocation })
   );
@@ -70,17 +73,39 @@ function convertToList(results, source) 
         sourceUrl: url
       });
     }
   }
 
   return positions;
 }
 
-async function _setBreakpointPositions(cx, sourceId, thunkArgs) {
+function groupByLine(results, sourceId, line) {
+  const isOriginal = isOriginalId(sourceId);
+  const positions = {};
+
+  // Ensure that we have an entry for the line fetched
+  if (typeof line === "number") {
+    positions[line] = [];
+  }
+
+  for (const result of results) {
+    const location = isOriginal ? result.location : result.generatedLocation;
+
+    if (!positions[location.line]) {
+      positions[location.line] = [];
+    }
+
+    positions[location.line].push(result);
+  }
+
+  return positions;
+}
+
+async function _setBreakpointPositions(cx, sourceId, line, thunkArgs) {
   const { client, dispatch, getState, sourceMaps } = thunkArgs;
   let generatedSource = getSource(getState(), sourceId);
   if (!generatedSource) {
     return;
   }
 
   let results = {};
   if (isOriginalId(sourceId)) {
@@ -107,31 +132,37 @@ async function _setBreakpointPositions(c
           column: 0
         };
       }
 
       const bps = await client.getBreakpointPositions(
         getSourceActorsForSource(getState(), generatedSource.id),
         range
       );
-      for (const line in bps) {
-        results[line] = (results[line] || []).concat(bps[line]);
+      for (const bpLine in bps) {
+        results[bpLine] = (results[bpLine] || []).concat(bps[bpLine]);
       }
     }
   } else {
+    if (typeof line !== "number") {
+      throw new Error("Line is required for generated sources");
+    }
+
     results = await client.getBreakpointPositions(
-      getSourceActorsForSource(getState(), generatedSource.id)
+      getSourceActorsForSource(getState(), generatedSource.id),
+      { start: { line, column: 0 }, end: { line: line + 1, column: 0 } }
     );
   }
 
   let positions = convertToList(results, generatedSource);
   positions = await mapLocations(positions, thunkArgs);
 
   positions = filterBySource(positions, sourceId);
   positions = filterByUniqLocation(positions);
+  positions = groupByLine(positions, sourceId, line);
 
   const source = getSource(getState(), sourceId);
   // NOTE: it's possible that the source was removed during a navigate
   if (!source) {
     return;
   }
 
   dispatch({
@@ -139,45 +170,38 @@ async function _setBreakpointPositions(c
     cx,
     source: source,
     positions
   });
 
   return positions;
 }
 
-const runningFetches = {};
-export function isFetchingBreakpoints(id: SourceId) {
-  return id in runningFetches;
+function generatedSourceActorKey(state, sourceId) {
+  const generatedSource = getSource(
+    state,
+    isOriginalId(sourceId) ? originalToGeneratedId(sourceId) : sourceId
+  );
+  const actors = generatedSource
+    ? getSourceActorsForSource(state, generatedSource.id).map(
+        ({ actor }) => actor
+      )
+    : [];
+  return [sourceId, ...actors].join(":");
 }
 
 export const setBreakpointPositions: MemoizedAction<
-  { cx: Context, sourceId: string },
+  { cx: Context, sourceId: string, line?: number },
   ?BreakpointPositions
 > = memoizeableAction("setBreakpointPositions", {
-  hasValue: ({ sourceId }, { getState }) =>
-    hasBreakpointPositions(getState(), sourceId),
-  getValue: ({ sourceId }, { getState }) =>
+  hasValue: ({ sourceId, line }, { getState }) =>
+    isGeneratedId(sourceId) && line
+      ? hasBreakpointPositionsForLine(getState(), sourceId, line)
+      : hasBreakpointPositions(getState(), sourceId),
+  getValue: ({ sourceId, line }, { getState }) =>
     getBreakpointPositionsForSource(getState(), sourceId),
-  createKey({ sourceId }, { getState }) {
-    const generatedSource = getSource(
-      getState(),
-      isOriginalId(sourceId) ? originalToGeneratedId(sourceId) : sourceId
-    );
-    const actors = generatedSource
-      ? getSourceActorsForSource(getState(), generatedSource.id).map(
-          ({ actor }) => actor
-        )
-      : [];
-    return [sourceId, ...actors].join(":");
+  createKey({ sourceId, line }, { getState }) {
+    const key = generatedSourceActorKey(getState(), sourceId);
+    return isGeneratedId(sourceId) && line ? `${key}-${line}` : key;
   },
-  action: async ({ cx, sourceId }, thunkArgs) => {
-    runningFetches[sourceId] = (runningFetches[sourceId] | 0) + 1;
-    try {
-      return await _setBreakpointPositions(cx, sourceId, thunkArgs);
-    } finally {
-      runningFetches[sourceId] -= 0;
-      if (runningFetches[sourceId] === 0) {
-        delete runningFetches[sourceId];
-      }
-    }
-  }
+  action: async ({ cx, sourceId, line }, thunkArgs) =>
+    _setBreakpointPositions(cx, sourceId, line, thunkArgs)
 });
--- a/devtools/client/debugger/src/actions/breakpoints/modify.js
+++ b/devtools/client/debugger/src/actions/breakpoints/modify.js
@@ -102,19 +102,19 @@ export function addBreakpoint(
   initialLocation: SourceLocation,
   options: BreakpointOptions = {},
   disabled: boolean = false,
   shouldCancel: () => boolean = () => false
 ) {
   return async ({ dispatch, getState, sourceMaps, client }: ThunkArgs) => {
     recordEvent("add_breakpoint");
 
-    const { sourceId, column } = initialLocation;
+    const { sourceId, column, line } = initialLocation;
 
-    await dispatch(setBreakpointPositions({ cx, sourceId }));
+    await dispatch(setBreakpointPositions({ cx, sourceId, line }));
 
     const position: ?BreakpointPosition = column
       ? getBreakpointPositionsForLocation(getState(), initialLocation)
       : getFirstBreakpointPosition(getState(), initialLocation);
 
     if (!position) {
       return;
     }
--- a/devtools/client/debugger/src/actions/breakpoints/syncBreakpoint.js
+++ b/devtools/client/debugger/src/actions/breakpoints/syncBreakpoint.js
@@ -31,18 +31,19 @@ import type {
   Context
 } from "../../types";
 
 async function findBreakpointPosition(
   cx: Context,
   { getState, dispatch },
   location: SourceLocation
 ) {
+  const { sourceId, line } = location;
   const positions: BreakpointPositions = await dispatch(
-    setBreakpointPositions({ cx, sourceId: location.sourceId })
+    setBreakpointPositions({ cx, sourceId, line })
   );
 
   const position = findPosition(positions, location);
   return position && position.generatedLocation;
 }
 
 async function findNewLocation(
   cx: Context,
--- a/devtools/client/debugger/src/actions/breakpoints/tests/breakpointPositions.spec.js
+++ b/devtools/client/debugger/src/actions/breakpoints/tests/breakpointPositions.spec.js
@@ -14,93 +14,99 @@ import {
 import { createSource } from "../../tests/helpers/threadClient";
 
 describe("breakpointPositions", () => {
   it("fetches positions", async () => {
     const fooContent = createSource("foo", "");
 
     const store = createStore({
       getBreakpointPositions: async () => ({ "9": [1] }),
+      getBreakableLines: async () => [],
       sourceContents: async () => fooContent
     });
 
     const { dispatch, getState, cx } = store;
     const source = await dispatch(
       actions.newGeneratedSource(makeSource("foo"))
     );
     await dispatch(actions.loadSourceById(cx, source.id));
 
-    dispatch(actions.setBreakpointPositions({ cx, sourceId: "foo" }));
+    dispatch(actions.setBreakpointPositions({ cx, sourceId: "foo", line: 9 }));
 
     await waitForState(store, state =>
       selectors.hasBreakpointPositions(state, "foo")
     );
 
     expect(
       selectors.getBreakpointPositionsForSource(getState(), "foo")
-    ).toEqual([
-      {
-        location: {
-          line: 9,
-          column: 1,
-          sourceId: "foo",
-          sourceUrl: "http://localhost:8000/examples/foo"
-        },
-        generatedLocation: {
-          line: 9,
-          column: 1,
-          sourceId: "foo",
-          sourceUrl: "http://localhost:8000/examples/foo"
+    ).toEqual({
+      [9]: [
+        {
+          location: {
+            line: 9,
+            column: 1,
+            sourceId: "foo",
+            sourceUrl: "http://localhost:8000/examples/foo"
+          },
+          generatedLocation: {
+            line: 9,
+            column: 1,
+            sourceId: "foo",
+            sourceUrl: "http://localhost:8000/examples/foo"
+          }
         }
-      }
-    ]);
+      ]
+    });
   });
 
   it("doesn't re-fetch positions", async () => {
     const fooContent = createSource("foo", "");
 
     let resolve = _ => {};
     let count = 0;
     const store = createStore({
       getBreakpointPositions: () =>
         new Promise(r => {
           count++;
           resolve = r;
         }),
+      getBreakableLines: async () => [],
       sourceContents: async () => fooContent
     });
 
     const { dispatch, getState, cx } = store;
     const source = await dispatch(
       actions.newGeneratedSource(makeSource("foo"))
     );
     await dispatch(actions.loadSourceById(cx, source.id));
 
-    dispatch(actions.setBreakpointPositions({ cx, sourceId: "foo" }));
-    dispatch(actions.setBreakpointPositions({ cx, sourceId: "foo" }));
+    dispatch(actions.setBreakpointPositions({ cx, sourceId: "foo", line: 9 }));
+    dispatch(actions.setBreakpointPositions({ cx, sourceId: "foo", line: 9 }));
 
     resolve({ "9": [1] });
     await waitForState(store, state =>
       selectors.hasBreakpointPositions(state, "foo")
     );
 
     expect(
       selectors.getBreakpointPositionsForSource(getState(), "foo")
-    ).toEqual([
-      {
-        location: {
-          line: 9,
-          column: 1,
-          sourceId: "foo",
-          sourceUrl: "http://localhost:8000/examples/foo"
-        },
-        generatedLocation: {
-          line: 9,
-          column: 1,
-          sourceId: "foo",
-          sourceUrl: "http://localhost:8000/examples/foo"
+    ).toEqual({
+      [9]: [
+        {
+          location: {
+            line: 9,
+            column: 1,
+            sourceId: "foo",
+            sourceUrl: "http://localhost:8000/examples/foo"
+          },
+          generatedLocation: {
+            line: 9,
+            column: 1,
+            sourceId: "foo",
+            sourceUrl: "http://localhost:8000/examples/foo"
+          }
         }
-      }
-    ]);
+      ]
+    });
 
     expect(count).toEqual(1);
   });
 });
--- a/devtools/client/debugger/src/actions/breakpoints/tests/breakpoints.spec.js
+++ b/devtools/client/debugger/src/actions/breakpoints/tests/breakpoints.spec.js
@@ -12,17 +12,18 @@ import {
   getTelemetryEvents
 } from "../../../utils/test-head";
 
 import { simpleMockThreadClient } from "../../tests/helpers/threadClient.js";
 
 function mockClient(positionsResponse = {}) {
   return {
     ...simpleMockThreadClient,
-    getBreakpointPositions: async () => positionsResponse
+    getBreakpointPositions: async () => positionsResponse,
+    getBreakableLines: 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
@@ -65,17 +65,18 @@ const mockThreadClient = {
         case "foo-wasm/originalSource":
           return resolve({
             source: "fn fooBar() {}\nfn barZoo() { fooBar() }",
             contentType: "text/rust"
           });
       }
     });
   },
-  getBreakpointPositions: async () => ({})
+  getBreakpointPositions: async () => ({}),
+  getBreakableLines: async () => []
 };
 
 const mockFrameId = "1";
 
 function createPauseInfo(
   frameLocation = { sourceId: "foo1", line: 2 },
   frameOpts = {}
 ) {
@@ -167,17 +168,20 @@ describe("pause", () => {
       const cx = selectors.getThreadContext(getState());
       const getNextStepSpy = jest.spyOn(parser, "getNextStep");
       dispatch(actions.stepOver(cx));
       expect(getNextStepSpy).toBeCalled();
       getNextStepSpy.mockRestore();
     });
 
     it("should step over when paused after an await", async () => {
-      const store = createStore(mockThreadClient);
+      const store = createStore({
+        ...mockThreadClient,
+        getBreakpointPositions: async () => ({ [2]: [1] })
+      });
       const { dispatch, getState } = store;
       const mockPauseInfo = createPauseInfo({
         sourceId: "await",
         line: 2,
         column: 6
       });
 
       await dispatch(actions.newGeneratedSource(makeSource("await")));
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/src/actions/sources/breakableLines.js
@@ -0,0 +1,51 @@
+/* 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 { 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";
+
+function calculateBreakableLines(positions) {
+  const lines = [];
+  for (const line in positions) {
+    if (positions[line].length > 0) {
+      lines.push(Number(line));
+    }
+  }
+
+  return lines;
+}
+
+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);
+    } else {
+      breakableLines = await client.getBreakableLines(
+        getSourceActorsForSource(getState(), sourceId)
+      );
+    }
+
+    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/index.js
+++ b/devtools/client/debugger/src/actions/sources/index.js
@@ -1,11 +1,12 @@
 /* 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
 export * from "./blackbox";
+export * from "./breakableLines";
 export * from "./loadSourceText";
 export * from "./newSources";
 export * from "./prettyPrint";
 export * from "./select";
 export { setSymbols } from "./symbols";
--- a/devtools/client/debugger/src/actions/sources/loadSourceText.js
+++ b/devtools/client/debugger/src/actions/sources/loadSourceText.js
@@ -10,19 +10,20 @@ import {
   getSourceFromId,
   getSourceWithContent,
   getSourceContent,
   getGeneratedSource,
   getSourcesEpoch,
   getBreakpointsForSource,
   getSourceActorsForSource
 } from "../../selectors";
-import { setBreakpointPositions, addBreakpoint } from "../breakpoints";
+import { addBreakpoint } from "../breakpoints";
 
 import { prettyPrintSource } from "./prettyPrint";
+import { setBreakableLines } from "./breakableLines";
 import { isFulfilled } from "../../utils/async-value";
 
 import * as parser from "../../workers/parser";
 import { isOriginal, isPretty } from "../../utils/source";
 import {
   memoizeableAction,
   type MemoizedAction
 } from "../../utils/memoizableAction";
@@ -111,18 +112,18 @@ async function loadSourceTextPromise(
 
   if (!newSource.isWasm && content) {
     parser.setSource(
       newSource.id,
       isFulfilled(content)
         ? content.value
         : { type: "text", value: "", contentType: undefined }
     );
-    dispatch(setBreakpointPositions({ cx, sourceId: newSource.id }));
 
+    await dispatch(setBreakableLines(cx, source.id));
     // Update the text in any breakpoints for this source by re-adding them.
     const breakpoints = getBreakpointsForSource(getState(), source.id);
     for (const { location, options, disabled } of breakpoints) {
       await dispatch(addBreakpoint(cx, location, options, disabled));
     }
   }
 
   return newSource;
--- a/devtools/client/debugger/src/actions/sources/moz.build
+++ b/devtools/client/debugger/src/actions/sources/moz.build
@@ -4,15 +4,16 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DIRS += [
 
 ]
 
 CompiledModules(
     'blackbox.js',
+    'breakableLines.js',
     'index.js',
     'loadSourceText.js',
     'newSources.js',
     'prettyPrint.js',
     'select.js',
     'symbols.js'
 )
--- a/devtools/client/debugger/src/actions/sources/newSources.js
+++ b/devtools/client/debugger/src/actions/sources/newSources.js
@@ -14,40 +14,41 @@ import { flatten } from "lodash";
 
 import {
   stringToSourceActorId,
   type SourceActor
 } from "../../reducers/source-actors";
 import { insertSourceActors } from "../../actions/source-actors";
 import { makeSourceId } from "../../client/firefox/create";
 import { toggleBlackBox } from "./blackbox";
-import { syncBreakpoint, setBreakpointPositions } from "../breakpoints";
+import { syncBreakpoint } from "../breakpoints";
 import { loadSourceText } from "./loadSourceText";
-import { isFetchingBreakpoints } from "../breakpoints/breakpointPositions";
 import { togglePrettyPrint } from "./prettyPrint";
-import { selectLocation } from "../sources";
+import { selectLocation, setBreakableLines } from "../sources";
 import {
   getRawSourceURL,
   isPrettyURL,
   isOriginal,
   isUrlExtension,
   isInlineScript
 } from "../../utils/source";
 import {
   getBlackBoxList,
   getSource,
   getSourceFromId,
   hasSourceActor,
   getPendingSelectedLocation,
   getPendingBreakpointsForSource,
-  getContext
+  getContext,
+  isSourceLoadingOrLoaded
 } from "../../selectors";
 
 import { prefs } from "../../utils/prefs";
 import sourceQueue from "../../utils/source-queue";
+import { ContextError } from "../../utils/context";
 
 import type {
   Source,
   SourceId,
   Context,
   OriginalSourceData,
   GeneratedSourceData,
   QueuedSourceData
@@ -308,17 +309,16 @@ export function newGeneratedSources(sour
       // We are sometimes notified about a new source multiple times if we
       // request a new source list and also get a source event from the server.
       if (!hasSourceActor(getState(), actorId)) {
         newSourceActors.push({
           id: actorId,
           actor: source.actor,
           thread,
           source: newId,
-
           isBlackBoxed: source.isBlackBoxed,
           sourceMapURL: source.sourceMapURL,
           url: source.url,
           introductionUrl: source.introductionUrl,
           introductionType: source.introductionType
         });
       }
 
@@ -326,35 +326,32 @@ export function newGeneratedSources(sour
     }
 
     const newSources: Array<Source> = (Object.values(newSourcesObj): Array<
       any
     >);
 
     const cx = getContext(getState());
     dispatch(addSources(cx, newSources));
-
-    const sourceIDsNeedingPositions = newSourceActors
-      .map(actor => actor.source)
-      .filter(sourceId => {
-        const source = getSource(getState(), sourceId);
-        return (
-          source && isInlineScript(source) && isFetchingBreakpoints(sourceId)
-        );
-      });
-
     dispatch(insertSourceActors(newSourceActors));
 
-    // 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 sourceId of sourceIDsNeedingPositions) {
-      dispatch(setBreakpointPositions({ cx, sourceId }));
+    for (const newSourceActor of newSourceActors) {
+      // Fetch breakable lines for new HTML scripts
+      // when the HTML file has started loading
+      if (
+        isInlineScript(newSourceActor) &&
+        isSourceLoadingOrLoaded(getState(), newSourceActor.source)
+      ) {
+        dispatch(setBreakableLines(cx, newSourceActor.source)).catch(error => {
+          if (!(error instanceof ContextError)) {
+            throw error;
+          }
+        });
+      }
     }
-
     await dispatch(checkNewSources(cx, newSources));
 
     return resultIds.map(id => getSourceFromId(getState(), id));
   };
 }
 
 function addSources(cx, sources: Array<Source>) {
   return ({ dispatch, getState }: ThunkArgs) => {
--- a/devtools/client/debugger/src/actions/sources/tests/blackbox.spec.js
+++ b/devtools/client/debugger/src/actions/sources/tests/blackbox.spec.js
@@ -8,17 +8,20 @@ import {
   actions,
   selectors,
   createStore,
   makeSource
 } from "../../../utils/test-head";
 
 describe("blackbox", () => {
   it("should blackbox a source", async () => {
-    const store = createStore({ blackBox: async () => true });
+    const store = createStore({
+      blackBox: async () => true,
+      getBreakableLines: async () => []
+    });
     const { dispatch, getState, cx } = store;
 
     const foo1Source = await dispatch(
       actions.newGeneratedSource(makeSource("foo1"))
     );
     await dispatch(actions.toggleBlackBox(cx, foo1Source));
 
     const fooSource = selectors.getSource(getState(), "foo1");
--- a/devtools/client/debugger/src/actions/sources/tests/loadSource.spec.js
+++ b/devtools/client/debugger/src/actions/sources/tests/loadSource.spec.js
@@ -56,17 +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(
       {
         ...sourceThreadClient,
         sourceContents: async () => fooGenContent,
-        getBreakpointPositions: async () => ({ "1": [0] })
+        getBreakpointPositions: async () => ({ "1": [0] }),
+        getBreakableLines: async () => []
       },
       {},
       {
         getGeneratedRangesForOriginal: async () => [
           { start: { line: 1, column: 0 }, end: { line: 1, column: 1 } }
         ],
         getOriginalLocations: async items =>
           items.map(item => ({
@@ -150,17 +151,18 @@ describe("loadSourceText", () => {
     let resolve;
     let count = 0;
     const { dispatch, getState, cx } = createStore({
       sourceContents: () =>
         new Promise(r => {
           count++;
           resolve = r;
         }),
-      getBreakpointPositions: async () => ({})
+      getBreakpointPositions: async () => ({}),
+      getBreakableLines: async () => []
     });
     const id = "foo";
 
     await dispatch(actions.newGeneratedSource(makeSource(id)));
 
     let source = selectors.getSourceFromId(getState(), id);
     dispatch(actions.loadSourceText({ cx, source }));
 
@@ -187,17 +189,18 @@ describe("loadSourceText", () => {
     let resolve;
     let count = 0;
     const { dispatch, getState, cx } = createStore({
       sourceContents: () =>
         new Promise(r => {
           count++;
           resolve = r;
         }),
-      getBreakpointPositions: async () => ({})
+      getBreakpointPositions: async () => ({}),
+      getBreakableLines: 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
@@ -30,17 +30,18 @@ const threadClient = {
   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 () => ({})
+  getBreakpointPositions: async () => ({}),
+  getBreakableLines: async () => []
 };
 
 const sourceMaps = {
   getOriginalSourceText: async ({ id }) => ({
     id,
     text: sourceTexts[id],
     contentType: "text/javascript"
   }),
--- a/devtools/client/debugger/src/actions/tests/expressions.spec.js
+++ b/devtools/client/debugger/src/actions/tests/expressions.spec.js
@@ -32,17 +32,18 @@ const mockThreadClient = {
             } else {
               resolve("boo");
             }
           })
       )
     ),
   getFrameScopes: async () => {},
   sourceContents: () => ({ source: "", contentType: "text/javascript" }),
-  getBreakpointPositions: async () => [],
+  getBreakpointPositions: async () => ({}),
+  getBreakableLines: 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/threadClient.js
+++ b/devtools/client/debugger/src/actions/tests/helpers/threadClient.js
@@ -73,10 +73,11 @@ export const sourceThreadClient = {
 
       reject(`unknown source: ${source}`);
     });
   },
   setBreakpoint: async () => {},
   threadClient: async () => {},
   getFrameScopes: async () => {},
   evaluateExpressions: async () => {},
-  getBreakpointPositions: async () => ({})
+  getBreakpointPositions: async () => ({}),
+  getBreakableLines: async () => []
 };
--- a/devtools/client/debugger/src/actions/tests/navigation.spec.js
+++ b/devtools/client/debugger/src/actions/tests/navigation.spec.js
@@ -23,16 +23,17 @@ const {
 } = selectors;
 
 const threadClient = {
   sourceContents: async () => ({
     source: "function foo1() {\n  const foo = 5; return foo;\n}",
     contentType: "text/javascript"
   }),
   getBreakpointPositions: async () => ({}),
+  getBreakableLines: 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,17 +44,18 @@ import {
 
 import sourceMaps from "devtools-source-map";
 
 import { makePendingLocationId } from "../../utils/breakpoint";
 function mockClient(bpPos = {}) {
   return {
     ...simpleMockThreadClient,
 
-    getBreakpointPositions: async () => bpPos
+    getBreakpointPositions: async () => bpPos,
+    getBreakableLines: async () => []
   };
 }
 
 function mockSourceMaps() {
   return {
     ...sourceMaps,
     getOriginalSourceText: async source => ({
       id: source.id,
--- 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,17 +34,18 @@ const sources = {
   "bar:formatted": {
     source: "function bla(x, y) {\n const bar = 4; return 2;\n}",
     contentType: "text/javascript"
   }
 };
 
 const threadClient = {
   sourceContents: async ({ source }) => sources[source],
-  getBreakpointPositions: async () => ({})
+  getBreakpointPositions: async () => ({}),
+  getBreakableLines: 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
@@ -39,17 +39,19 @@ describe("setProjectDirectoryRoot", () =
     const { dispatch, getState, cx } = createStore();
     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({});
+    const store = createStore({
+      getBreakableLines: 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());
     const filteredSources = Object.values(filteredSourcesByThread)[0];
@@ -58,21 +60,25 @@ describe("setProjectDirectoryRoot", () =
     expect(firstSource.url).toEqual(
       "http://localhost:8000/examples/js/scopes.js"
     );
 
     expect(firstSource.relativeUrl).toEqual("scopes.js");
   });
 
   it("should update the child directory ", () => {
-    const { dispatch, getState, cx } = createStore();
+    const { dispatch, getState, cx } = createStore({
+      getBreakableLines: 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();
+    const { dispatch, getState, cx } = createStore({
+      getBreakableLines: async () => []
+    });
     dispatch(actions.setProjectDirectoryRoot(cx, "webpack://"));
     dispatch(actions.setProjectDirectoryRoot(cx, "webpack:///app"));
     expect(getProjectDirectoryRoot(getState())).toBe("webpack:///app");
   });
 });
--- a/devtools/client/debugger/src/actions/tests/ui.spec.js
+++ b/devtools/client/debugger/src/actions/tests/ui.spec.js
@@ -1,32 +1,23 @@
 /* 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 {
-  createStore,
-  selectors,
-  actions,
-  makeSource
-} from "../../utils/test-head";
+import { createStore, selectors, actions } from "../../utils/test-head";
 
 const {
   getActiveSearch,
   getFrameworkGroupingState,
   getPaneCollapse,
-  getHighlightedLineRange,
-  getProjectDirectoryRoot,
-  getDisplayedSources
+  getHighlightedLineRange
 } = selectors;
 
-import type { Source } from "../../types";
-
 describe("ui", () => {
   it("should toggle the visible state of project search", () => {
     const { dispatch, getState } = createStore();
     expect(getActiveSearch(getState())).toBe(null);
     dispatch(actions.setActiveSearch("project"));
     expect(getActiveSearch(getState())).toBe("project");
   });
 
@@ -77,70 +68,8 @@ describe("ui", () => {
   it("should clear highlight lines", () => {
     const { dispatch, getState } = createStore();
     const range = { start: 3, end: 5, sourceId: "2" };
     dispatch(actions.highlightLineRange(range));
     dispatch(actions.clearHighlightLineRange());
     expect(getHighlightedLineRange(getState())).toEqual({});
   });
 });
-
-describe("setProjectDirectoryRoot", () => {
-  it("should set domain directory as root", () => {
-    const { dispatch, getState, cx } = createStore();
-    dispatch(actions.setProjectDirectoryRoot(cx, "example.com"));
-    expect(getProjectDirectoryRoot(getState())).toBe("example.com");
-  });
-
-  it("should set a directory as root directory", () => {
-    const { dispatch, getState, cx } = createStore();
-    dispatch(actions.setProjectDirectoryRoot(cx, "/example.com/foo"));
-    expect(getProjectDirectoryRoot(getState())).toBe("/example.com/foo");
-  });
-
-  it("should add to the directory ", () => {
-    const { dispatch, getState, cx } = createStore();
-    dispatch(actions.setProjectDirectoryRoot(cx, "/example.com/foo"));
-    dispatch(actions.setProjectDirectoryRoot(cx, "/foo/bar"));
-    expect(getProjectDirectoryRoot(getState())).toBe("/example.com/foo/bar");
-  });
-
-  it("should update the directory ", () => {
-    const { dispatch, getState, cx } = createStore();
-    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({});
-    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());
-    const filteredSources = Object.values(filteredSourcesByThread)[0];
-    const firstSource: Source = (Object.values(filteredSources)[0]: any);
-
-    expect(firstSource.url).toEqual(
-      "http://localhost:8000/examples/js/scopes.js"
-    );
-
-    expect(firstSource.relativeUrl).toEqual("scopes.js");
-  });
-
-  it("should update the child directory ", () => {
-    const { dispatch, getState, cx } = createStore();
-    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();
-    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
@@ -69,9 +69,15 @@ export type SourceAction =
       +type: "CLOSE_TAB",
       +url: string,
       +tabs: any
     |}
   | {|
       +type: "CLOSE_TABS",
       +sources: Array<Source>,
       +tabs: any
+    |}
+  | {|
+      type: "SET_BREAKABLE_LINES",
+      +cx: Context,
+      breakableLines: number[],
+      sourceId: string
     |};
--- a/devtools/client/debugger/src/actions/types/index.js
+++ b/devtools/client/debugger/src/actions/types/index.js
@@ -29,16 +29,17 @@ import type { Panel } from "../../client
  * Argument parameters via Thunk middleware for {@link https://github.com/gaearon/redux-thunk|Redux Thunk}
  *
  * @memberof actions/breakpoints
  * @static
  * @typedef {Object} ThunkArgs
  */
 export type ThunkArgs = {
   dispatch: (action: any) => Promise<any>,
+  forkedDispatch: (action: any) => Promise<any>,
   getState: () => State,
   client: typeof clientCommands,
   sourceMaps: SourceMaps,
   panel: Panel
 };
 
 export type Thunk = ThunkArgs => any;
 
--- a/devtools/client/debugger/src/client/firefox/commands.js
+++ b/devtools/client/debugger/src/client/firefox/commands.js
@@ -444,33 +444,58 @@ async function getBreakpointPositions(
   actors: Array<SourceActor>,
   range: ?Range
 ): Promise<{ [string]: number[] }> {
   const sourcePositions = {};
 
   for (const { thread, actor } of actors) {
     const sourceThreadClient = lookupThreadClient(thread);
     const sourceFront = sourceThreadClient.source({ actor });
-    const positions = await sourceFront.getBreakpointPositionsCompressed(
-      range
-    );
+    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 getBreakableLines(actors: Array<SourceActor>) {
+  let lines = [];
+  for (const { thread, actor } of actors) {
+    const sourceThreadClient = lookupThreadClient(thread);
+    const sourceFront = sourceThreadClient.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;
+}
+
 const clientCommands = {
   autocomplete,
   blackBox,
   createObjectClient,
   loadObjectProperties,
   releaseActor,
   interrupt,
   pauseGrip,
@@ -481,16 +506,17 @@ const clientCommands = {
   rewind,
   reverseStepIn,
   reverseStepOut,
   reverseStepOver,
   breakOnNext,
   sourceContents,
   getSourceForActor,
   getBreakpointPositions,
+  getBreakableLines,
   hasBreakpoint,
   setBreakpoint,
   setXHRBreakpoint,
   removeXHRBreakpoint,
   removeBreakpoint,
   evaluate,
   evaluateInFrame,
   evaluateExpressions,
--- a/devtools/client/debugger/src/client/firefox/types.js
+++ b/devtools/client/debugger/src/client/firefox/types.js
@@ -301,17 +301,18 @@ export type FunctionGrip = {|
 export type SourceClient = {
   source: () => { source: any, contentType?: string },
   _activeThread: ThreadClient,
   actor: string,
   getBreakpointPositionsCompressed: (range: ?Range) => Promise<any>,
   prettyPrint: number => Promise<*>,
   disablePrettyPrint: () => Promise<*>,
   blackBox: (range?: Range) => Promise<*>,
-  unblackBox: (range?: Range) => Promise<*>
+  unblackBox: (range?: Range) => Promise<*>,
+  getBreakableLines: () => Promise<number[]>
 };
 
 /**
  * ObjectClient
  * @memberof firefox
  * @static
  */
 export type ObjectClient = {
--- a/devtools/client/debugger/src/reducers/ast.js
+++ b/devtools/client/debugger/src/reducers/ast.js
@@ -39,26 +39,24 @@ export type PreviewValue = {|
   location: AstLocation,
   cursorPos: any,
   tokenPos: AstLocation,
   updating: false
 |};
 
 export type ASTState = {
   +symbols: SymbolsMap,
-  +emptyLines: EmptyLinesMap,
   +outOfScopeLocations: ?Array<AstLocation>,
   +inScopeLines: ?Array<number>,
   +preview: Preview
 };
 
 export function initialASTState(): ASTState {
   return {
     symbols: {},
-    emptyLines: {},
     outOfScopeLocations: null,
     inScopeLines: null,
     preview: null
   };
 }
 
 function update(state: ASTState = initialASTState(), action: Action): ASTState {
   switch (action.type) {
--- a/devtools/client/debugger/src/reducers/breakpoints.js
+++ b/devtools/client/debugger/src/reducers/breakpoints.js
@@ -8,17 +8,16 @@
  * Breakpoints reducer
  * @module reducers/breakpoints
  */
 
 import { isGeneratedId } from "devtools-source-map";
 import { isEqual } from "lodash";
 
 import { makeBreakpointId } from "../utils/breakpoint";
-import "../utils/breakable-lines";
 
 // eslint-disable-next-line max-len
 import { getBreakpointsList as getBreakpointsListSelector } from "../selectors/breakpoints";
 
 import type {
   XHRBreakpoint,
   Breakpoint,
   BreakpointId,
--- a/devtools/client/debugger/src/reducers/sources.js
+++ b/devtools/client/debugger/src/reducers/sources.js
@@ -29,17 +29,16 @@ import {
   makeReduceAllQuery,
   makeMapWithArgs,
   type Resource,
   type ResourceState,
   type ReduceQuery,
   type ReduceAllQuery
 } from "../utils/resource";
 
-import { findBreakableLines } from "../utils/breakable-lines";
 import { findPosition } from "../utils/breakpoint/breakpointPositions";
 import * as asyncValue from "../utils/async-value";
 import type { AsyncValue, SettledValue } from "../utils/async-value";
 import { originalToGeneratedId } from "devtools-source-map";
 import { prefs } from "../utils/prefs";
 
 import {
   hasSourceActor,
@@ -61,17 +60,19 @@ import type {
   BreakpointPositions
 } from "../types";
 import type { PendingSelectedLocation, Selector } from "./types";
 import type { Action, DonePromiseAction, FocusItem } from "../actions/types";
 import type { LoadSourceAction } from "../actions/types/SourceAction";
 import { uniq } from "lodash";
 
 export type SourcesMap = { [SourceId]: Source };
-type SourcesContentMap = { [SourceId]: SettledValue<SourceContent> | null };
+type SourcesContentMap = {
+  [SourceId]: AsyncValue<SourceContent> | null
+};
 export type BreakpointPositionsMap = { [SourceId]: BreakpointPositions };
 export type SourcesMapByThread = { [ThreadId]: SourcesMap };
 type SourceActorMap = { [SourceId]: Array<SourceActorId> };
 
 type UrlsMap = { [string]: SourceId[] };
 type PlainUrlsMap = { [string]: string[] };
 
 type SourceResource = Resource<{
@@ -196,29 +197,36 @@ 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": {
+      const { breakableLines, sourceId } = action;
+      return {
+        ...state,
+        breakableLines: {
+          ...state.breakableLines,
+          [sourceId]: breakableLines
+        }
+      };
+    }
+
     case "ADD_BREAKPOINT_POSITIONS": {
       const { source, positions } = action;
-      const breakableLines = findBreakableLines(source, positions);
+      const breakpointPositions = state.breakpointPositions[source.id];
 
       return {
         ...state,
         breakpointPositions: {
           ...state.breakpointPositions,
-          [source.id]: positions
-        },
-        breakableLines: {
-          ...state.breakableLines,
-          [source.id]: breakableLines
+          [source.id]: { ...breakpointPositions, ...positions }
         }
       };
     }
     case "NAVIGATE":
       return {
         ...initialSourcesState(),
         epoch: state.epoch + 1
       };
@@ -380,17 +388,17 @@ function updateLoadedState(
   // If there was a navigation between the time the action was started and
   // completed, we don't want to update the store.
   if (action.epoch !== state.epoch || !(sourceId in state.content)) {
     return state;
   }
 
   let content;
   if (action.status === "start") {
-    content = null;
+    content = asyncValue.pending();
   } else if (action.status === "error") {
     content = asyncValue.rejected(action.error);
   } else if (typeof action.value.text === "string") {
     content = asyncValue.fulfilled({
       type: "text",
       value: action.value.text,
       contentType: action.value.contentType
     });
@@ -736,34 +744,42 @@ export function getSourceWithContent(
     state.sources.sources,
     state.sources.content,
     id
   );
 }
 export function getSourceContent(
   state: OuterState,
   id: SourceId
-): AsyncValue<SourceContent> | null {
+): SettledValue<SourceContent> | null {
   // Assert the resource exists.
   getResource(state.sources.sources, id);
+  const content = state.sources.content[id];
 
-  return state.sources.content[id] || null;
+  if (!content || content.state === "pending") {
+    return null;
+  }
+
+  return content;
 }
 
 const contentLookup: WeakMap<Source, SourceWithContent> = new WeakMap();
 function getSourceWithContentInner(
   sources: SourceResourceState,
   content: SourcesContentMap,
   id: SourceId
 ): SourceWithContent {
   const source = getResource(sources, id);
-  const contentValue = content[source.id];
+  let contentValue = content[source.id];
 
   let result = contentLookup.get(source);
   if (!result || result.content !== contentValue) {
+    if (contentValue && contentValue.state === "pending") {
+      contentValue = null;
+    }
     result = {
       source,
       content: contentValue
     };
     contentLookup.set(source, result);
   }
 
   return result;
@@ -897,16 +913,25 @@ export function getBreakpointPositionsFo
 
 export function hasBreakpointPositions(
   state: OuterState,
   sourceId: string
 ): boolean {
   return !!getBreakpointPositionsForSource(state, sourceId);
 }
 
+export function hasBreakpointPositionsForLine(
+  state: OuterState,
+  sourceId: string,
+  line: number
+): boolean {
+  const positions = getBreakpointPositionsForSource(state, sourceId);
+  return !!(positions && positions[line]);
+}
+
 export function getBreakpointPositionsForLocation(
   state: OuterState,
   location: SourceLocation
 ): ?MappedLocation {
   const { sourceId } = location;
   const positions = getBreakpointPositionsForSource(state, sourceId);
   return findPosition(positions, location);
 }
@@ -922,9 +947,14 @@ export function getBreakableLines(state:
 export const getSelectedBreakableLines: Selector<Set<number>> = createSelector(
   state => {
     const sourceId = getSelectedSourceId(state);
     return sourceId && state.sources.breakableLines[sourceId];
   },
   breakableLines => new Set(breakableLines || [])
 );
 
+export function isSourceLoadingOrLoaded(state: OuterState, sourceId: string) {
+  const content = state.sources.content[sourceId];
+  return content !== null;
+}
+
 export default update;
--- a/devtools/client/debugger/src/selectors/test/visibleColumnBreakpoints.spec.js
+++ b/devtools/client/debugger/src/selectors/test/visibleColumnBreakpoints.spec.js
@@ -32,40 +32,40 @@ function bp(line, column) {
 }
 
 describe("visible column breakpoints", () => {
   it("simple", () => {
     const viewport = {
       start: { line: 1, column: 0 },
       end: { line: 10, column: 10 }
     };
-    const pausePoints = [pp(1, 1), pp(1, 5), pp(3, 1)];
+    const pausePoints = { [1]: [pp(1, 1), pp(1, 5)], [3]: [pp(3, 1)] };
     const breakpoints = [bp(1, 1), bp(4, 0), bp(4, 3)];
 
     const columnBps = getColumnBreakpoints(pausePoints, breakpoints, viewport);
     expect(columnBps).toMatchSnapshot();
   });
 
   it("ignores single breakpoints", () => {
     const viewport = {
       start: { line: 1, column: 0 },
       end: { line: 10, column: 10 }
     };
-    const pausePoints = [pp(1, 1), pp(1, 3), pp(2, 1)];
+    const pausePoints = { [1]: [pp(1, 1), pp(1, 3)], [2]: [pp(2, 1)] };
     const breakpoints = [bp(1, 1)];
     const columnBps = getColumnBreakpoints(pausePoints, breakpoints, viewport);
     expect(columnBps).toMatchSnapshot();
   });
 
   it("only shows visible breakpoints", () => {
     const viewport = {
       start: { line: 1, column: 0 },
       end: { line: 10, column: 10 }
     };
-    const pausePoints = [pp(1, 1), pp(1, 3), pp(20, 1)];
+    const pausePoints = { [1]: [pp(1, 1), pp(1, 3)], [20]: [pp(20, 1)] };
     const breakpoints = [bp(1, 1)];
 
     const columnBps = getColumnBreakpoints(pausePoints, breakpoints, viewport);
     expect(columnBps).toMatchSnapshot();
   });
 });
 
 describe("getFirstBreakpointPosition", () => {
--- a/devtools/client/debugger/src/selectors/visibleColumnBreakpoints.js
+++ b/devtools/client/debugger/src/selectors/visibleColumnBreakpoints.js
@@ -103,51 +103,61 @@ function filterVisible(positions, select
 function filterByBreakpoints(positions, selectedSource, breakpointMap) {
   return positions.filter(position => {
     const location = getSelectedLocation(position, selectedSource);
     return breakpointMap[location.line];
   });
 }
 
 function formatPositions(
-  positions: BreakpointPositions,
+  positions: BreakpointPosition[],
   selectedSource,
   breakpointMap
 ) {
   return (positions: any).map((position: BreakpointPosition) => {
     const location = getSelectedLocation(position, selectedSource);
     return {
       location,
       breakpoint: findBreakpoint(location, breakpointMap)
     };
   });
 }
 
+function convertToList(
+  breakpointPositions: BreakpointPositions
+): BreakpointPosition[] {
+  return ([].concat(...Object.values(breakpointPositions)): any);
+}
+
 export function getColumnBreakpoints(
   positions: ?BreakpointPositions,
   breakpoints: ?(Breakpoint[]),
   viewport: Range,
   selectedSource: ?Source
 ) {
   if (!positions) {
     return [];
   }
 
   // We only want to show a column breakpoint if several conditions are matched
   // - it is the first breakpoint to appear at an the original location
   // - the position is in the current viewport
   // - there is atleast one other breakpoint on that line
   // - there is a breakpoint on that line
   const breakpointMap = groupBreakpoints(breakpoints, selectedSource);
+  let newPositions = convertToList(positions);
+  newPositions = filterByLineCount(newPositions, selectedSource);
+  newPositions = filterVisible(newPositions, selectedSource, viewport);
+  newPositions = filterByBreakpoints(
+    newPositions,
+    selectedSource,
+    breakpointMap
+  );
 
-  positions = filterByLineCount(positions, selectedSource);
-  positions = filterVisible(positions, selectedSource, viewport);
-  positions = filterByBreakpoints(positions, selectedSource, breakpointMap);
-
-  return formatPositions(positions, selectedSource, breakpointMap);
+  return formatPositions(newPositions, selectedSource, breakpointMap);
 }
 
 const getVisibleBreakpointPositions = createSelector(
   getSelectedSource,
   getBreakpointPositions,
   (source, positions) => source && positions[source.id]
 );
 
@@ -167,12 +177,12 @@ export function getFirstBreakpointPositi
 ): ?BreakpointPosition {
   const positions = getBreakpointPositionsForSource(state, sourceId);
   const source = getSource(state, sourceId);
 
   if (!source || !positions) {
     return;
   }
 
-  return sortSelectedLocations(positions, source).find(
+  return sortSelectedLocations(convertToList(positions), source).find(
     position => getSelectedLocation(position, source).line == line
   );
 }
--- a/devtools/client/debugger/src/test/tests-setup.js
+++ b/devtools/client/debugger/src/test/tests-setup.js
@@ -111,17 +111,17 @@ function mockIndexeddDB() {
       store[key] = value;
     }
   };
 }
 
 // NOTE: We polyfill finally because TRY uses node 8
 if (!global.Promise.prototype.finally) {
   global.Promise.prototype.finally = function finallyPolyfill(callback) {
-    var constructor = this.constructor;
+    const constructor = this.constructor;
 
     return this.then(
       function(value) {
         return constructor.resolve(callback()).then(function() {
           return value;
         });
       },
       function(reason) {
--- a/devtools/client/debugger/src/types.js
+++ b/devtools/client/debugger/src/types.js
@@ -464,11 +464,11 @@ export type Cancellable = {
   cancel: () => void
 };
 
 export type EventListenerBreakpoints = string[];
 
 export type SourceDocuments = { [string]: Object };
 
 export type BreakpointPosition = MappedLocation;
-export type BreakpointPositions = BreakpointPosition[];
+export type BreakpointPositions = { [number]: BreakpointPosition[] };
 
 export type { Context, ThreadContext } from "./utils/context";
deleted file mode 100644
--- a/devtools/client/debugger/src/utils/breakable-lines.js
+++ /dev/null
@@ -1,23 +0,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/>. */
-
-// @flow
-
-import { getSelectedLocation } from "./selected-location";
-import type { BreakpointPositions, Source } from "../types";
-
-export function findBreakableLines(
-  source: Source,
-  breakpointPositions: BreakpointPositions
-): number[] {
-  if (!breakpointPositions || source.isWasm) {
-    return [];
-  }
-
-  return Array.from(
-    new Set(
-      breakpointPositions.map(point => getSelectedLocation(point, source).line)
-    )
-  );
-}
--- a/devtools/client/debugger/src/utils/breakpoint/breakpointPositions.js
+++ b/devtools/client/debugger/src/utils/breakpoint/breakpointPositions.js
@@ -11,12 +11,16 @@ import type { BreakpointPositions, Sourc
 export function findPosition(
   positions: ?BreakpointPositions,
   location: SourceLocation
 ) {
   if (!positions) {
     return null;
   }
 
-  return positions.find(pos =>
+  const lineBps = positions[location.line];
+  if (!lineBps) {
+    return null;
+  }
+  return lineBps.find(pos =>
     comparePosition(getSelectedLocation(pos, location), location)
   );
 }
--- a/devtools/client/debugger/src/utils/context.js
+++ b/devtools/client/debugger/src/utils/context.js
@@ -47,33 +47,35 @@ export type ThreadContext = {|
 
   // Whether the current thread is paused. This is determined from the other
   // Context properties and is here for convenient access.
   +isPaused: boolean
 |};
 
 export type Context = NavigateContext | ThreadContext;
 
+export class ContextError extends Error {}
+
 export function validateNavigateContext(state: State, cx: Context) {
   const newcx = getThreadContext(state);
 
   if (newcx.navigateCounter != cx.navigateCounter) {
-    throw new Error("Page has navigated");
+    throw new ContextError("Page has navigated");
   }
 }
 
 function validateThreadContext(state: State, cx: ThreadContext) {
   const newcx = getThreadContext(state);
 
   if (cx.thread != newcx.thread) {
-    throw new Error("Current thread has changed");
+    throw new ContextError("Current thread has changed");
   }
 
   if (cx.pauseCounter != newcx.pauseCounter) {
-    throw new Error("Current thread has paused or resumed");
+    throw new ContextError("Current thread has paused or resumed");
   }
 }
 
 export function validateContext(state: State, cx: Context) {
   validateNavigateContext(state, cx);
 
   if ("thread" in cx) {
     validateThreadContext(state, (cx: any));
--- a/devtools/client/debugger/src/utils/moz.build
+++ b/devtools/client/debugger/src/utils/moz.build
@@ -12,17 +12,16 @@ DIRS += [
 ]
 
 CompiledModules(
     'assert.js',
     'ast.js',
     'async-value.js',
     'asyncStoreHelper.js',
     'bootstrap.js',
-    'breakable-lines.js',
     'build-query.js',
     'clipboard.js',
     'connect.js',
     'context.js',
     'dbg.js',
     'defer.js',
     'DevToolsUtils.js',
     'expressions.js',
--- a/devtools/client/debugger/src/utils/source.js
+++ b/devtools/client/debugger/src/utils/source.js
@@ -16,17 +16,23 @@ import { endTruncateStr } from "./utils"
 import { truncateMiddleText } from "../utils/text";
 import { parse as parseURL } from "../utils/url";
 import { renderWasmText } from "./wasm";
 import { toEditorPosition } from "./editor";
 export { isMinified } from "./isMinified";
 import { getURL, getFileExtension } from "./sources-tree";
 import { prefs, features } from "./prefs";
 
-import type { SourceId, Source, SourceContent, SourceLocation } from "../types";
+import type {
+  SourceId,
+  Source,
+  SourceActor,
+  SourceContent,
+  SourceLocation
+} from "../types";
 import { isFulfilled, type AsyncValue } from "./async-value";
 import type { Symbols } from "../reducers/types";
 
 type transformUrlCallback = string => string;
 
 export const sourceTypes = {
   coffee: "coffeescript",
   js: "javascript",
@@ -392,17 +398,17 @@ export function getMode(
 
   if (isHTMLLike) {
     return { name: "htmlmixed" };
   }
 
   return { name: "text" };
 }
 
-export function isInlineScript(source: Source): boolean {
+export function isInlineScript(source: SourceActor): boolean {
   return source.introductionType === "scriptElement";
 }
 
 export function getTextAtPosition(
   sourceId: SourceId,
   asyncContent: AsyncValue<SourceContent> | null,
   location: SourceLocation
 ) {
deleted file mode 100644
--- a/devtools/client/debugger/src/utils/tests/breakable-lines.spec.js
+++ /dev/null
@@ -1,33 +0,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/>. */
-
-// @flow
-
-import { findBreakableLines } from "../breakable-lines";
-import { createSourceObject } from "../test-head";
-
-function ml(gLine) {
-  const generatedLocation = { line: gLine, column: 0, sourceId: "foo" };
-  return { generatedLocation, location: generatedLocation };
-}
-
-describe("breakableLines", () => {
-  it("no positions", () => {
-    const source = createSourceObject("foo");
-    const lines = findBreakableLines(source, []);
-    expect(lines).toEqual([]);
-  });
-
-  it("one position", () => {
-    const source = createSourceObject("foo");
-    const lines = findBreakableLines(source, [ml(1)]);
-    expect(lines).toEqual([1]);
-  });
-
-  it("outside positions are not included", () => {
-    const source = createSourceObject("foo");
-    const lines = findBreakableLines(source, [ml(1), ml(2), ml(10)]);
-    expect(lines).toEqual([1, 2, 10]);
-  });
-});
--- a/devtools/client/debugger/test/mochitest/browser_dbg-editor-highlight.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg-editor-highlight.js
@@ -4,18 +4,17 @@
 
 // Tests that the editor will always highight the right line, no
 // matter if the source text doesn't exist yet or even if the source
 // doesn't exist.
 
 add_task(async function() {
   const dbg = await initDebugger("doc-scripts.html", "long.js");
   const {
-    selectors: { getSource, getSourceContent },
-    getState
+    selectors: { getSource, getSourceContent }
   } = dbg;
   const sourceUrl = `${EXAMPLE_URL}long.js`;
 
   // The source itself doesn't even exist yet, and using
   // `selectSourceURL` will set a pending request to load this source
   // and highlight a specific line.
 
   await selectSource(dbg, sourceUrl, 66);
--- a/devtools/client/debugger/test/mochitest/browser_dbg-inline-cache.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg-inline-cache.js
@@ -86,16 +86,17 @@ add_task(async function() {
   makeChanges();
 
   info("Reload inside debugger with toolbox caching disabled (attempt 1)");
   await reloadTabAndDebugger(tab, dbg);
   pageValue = await getPageValue(tab);
   is(pageValue, "let x = 2;", "Content loads from network, has doc value 2");
   await waitForLoadedSource(dbg, "inline-cache.html");
   dbgValue = findSourceContent(dbg, "inline-cache.html");
+
   info(`Debugger text: ${dbgValue.value}`);
   ok(
     dbgValue.value.includes(pageValue),
     "Debugger loads from network, gets value 2 like page"
   );
 
   makeChanges();
 
--- a/devtools/client/debugger/test/mochitest/browser_dbg-tabs.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg-tabs.js
@@ -11,31 +11,31 @@ PromiseTestUtils.whitelistRejectionsGlob
 
 add_task(async function() {
   const dbg = await initDebugger("doc-scripts.html", "simple1", "simple2");
 
   await selectSource(dbg, "simple1");
   await selectSource(dbg, "simple2");
   is(countTabs(dbg), 2);
 
-  // Test reloading the debugger
+  info("Test reloading the debugger");
   await reload(dbg, "simple1", "simple2");
   is(countTabs(dbg), 2);
   await waitForSelectedSource(dbg);
 
-  // Test reloading the debuggee a second time
+  info("Test reloading the debuggee a second time");
   await reload(dbg, "simple1", "simple2");
   is(countTabs(dbg), 2);
   await waitForSelectedSource(dbg);
 });
 
 add_task(async function() {
   const dbg = await initDebugger("doc-scripts.html", "simple1", "simple2");
 
   await selectSource(dbg, "simple1");
   await selectSource(dbg, "simple2");
   await closeTab(dbg, "simple1");
   await closeTab(dbg, "simple2");
 
-  // Test reloading the debugger
+  info("Test reloading the debugger");
   await reload(dbg, "simple1", "simple2");
   is(countTabs(dbg), 0);
 });
--- a/devtools/client/debugger/test/mochitest/helpers.js
+++ b/devtools/client/debugger/test/mochitest/helpers.js
@@ -220,17 +220,17 @@ function waitForSelectedLocation(dbg, li
     return location && location.line == line;
   });
 }
 
 function waitForSelectedSource(dbg, url) {
   const {
     getSelectedSourceWithContent,
     hasSymbols,
-    hasBreakpointPositions
+    getBreakableLines
   } = dbg.selectors;
 
   return waitForState(
     dbg,
     state => {
       const { source, content } = getSelectedSourceWithContent() || {};
       if (!content) {
         return false;
@@ -240,17 +240,17 @@ function waitForSelectedSource(dbg, url)
         return true;
       }
 
       const newSource = findSource(dbg, url, { silent: true });
       if (newSource.id != source.id) {
         return false;
       }
 
-      return hasSymbols(source) && hasBreakpointPositions(source.id);
+      return hasSymbols(source) && getBreakableLines(source.id);
     },
     "selected source"
   );
 }
 
 /**
  * Assert that the debugger is not currently paused.
  * @memberof mochitest/asserts
@@ -303,17 +303,18 @@ function assertPausedLocation(dbg) {
   assertDebugLine(dbg, pauseLine);
 
   ok(isVisibleInEditor(dbg, getCM(dbg).display.gutters), "gutter is visible");
 }
 
 function assertDebugLine(dbg, line) {
   // Check the debug line
   const lineInfo = getCM(dbg).lineInfo(line - 1);
-  const { source, content } = dbg.selectors.getSelectedSourceWithContent() || {};
+  const { source, content } =
+    dbg.selectors.getSelectedSourceWithContent() || {};
   if (source && !content) {
     const url = source.url;
     ok(
       false,
       `Looks like the source ${url} is still loading. Try adding waitForLoadedSource in the test.`
     );
     return;
   }
@@ -499,17 +500,18 @@ function waitForTime(ms) {
 }
 
 function isSelectedFrameSelected(dbg, state) {
   const frame = dbg.selectors.getVisibleSelectedFrame();
 
   // Make sure the source text is completely loaded for the
   // source we are paused in.
   const sourceId = frame.location.sourceId;
-  const { source, content } = dbg.selectors.getSelectedSourceWithContent() || {};
+  const { source, content } =
+    dbg.selectors.getSelectedSourceWithContent() || {};
 
   if (!source) {
     return false;
   }
 
   if (!content) {
     return false;
   }
@@ -605,17 +607,19 @@ function findSource(dbg, url, { silent }
   }
 
   return source;
 }
 
 function findSourceContent(dbg, url, opts) {
   const source = findSource(dbg, url, opts);
 
-  if (!source) return null;
+  if (!source) {
+    return null;
+  }
 
   const content = dbg.selectors.getSourceContent(source.id);
 
   if (!content) {
     return null;
   }
 
   if (content.state !== "fulfilled") {
@@ -640,17 +644,19 @@ function waitForLoadedSource(dbg, url) {
   );
 }
 
 function waitForLoadedSources(dbg) {
   return waitForState(
     dbg,
     state => {
       const sources = dbg.selectors.getSourceList();
-      return sources.every(source => !!dbg.selectors.getSourceContent(source.id));
+      return sources.every(
+        source => !!dbg.selectors.getSourceContent(source.id)
+      );
     },
     "loaded source"
   );
 }
 
 function getContext(dbg) {
   return dbg.selectors.getContext();
 }
@@ -839,18 +845,17 @@ function setBreakpointOptions(dbg, sourc
     getContext(dbg),
     { sourceId, line, column },
     options
   );
 }
 
 function findBreakpoint(dbg, url, line) {
   const source = findSource(dbg, url);
-  const column = getFirstBreakpointColumn(dbg, { line, sourceId: source.id });
-  return dbg.selectors.getBreakpoint({ sourceId: source.id, line, column });
+  return dbg.selectors.getBreakpointsForSource(source.id, line)[0];
 }
 
 async function loadAndAddBreakpoint(dbg, filename, line, column) {
   const {
     selectors: { getBreakpoint, getBreakpointCount, getBreakpointsMap }
   } = dbg;
 
   await waitForSources(dbg, filename);
--- a/devtools/server/actors/source.js
+++ b/devtools/server/actors/source.js
@@ -255,16 +255,28 @@ const SourceActor = ActorClassWithSpec(s
         this._contentType = result.contentType;
         return result;
       }, error => {
         this._reportLoadSourceError(error);
         throw error;
       });
   },
 
+  getBreakableLines() {
+    const positions = this.getBreakpointPositions();
+    const lines = new Set();
+    for (const position of positions) {
+      if (!lines.has(position.line)) {
+        lines.add(position.line);
+      }
+    }
+
+    return Array.from(lines);
+  },
+
   getBreakpointPositions(query) {
     const {
       start: {
         line: startLine = 0,
         column: startColumn = 0,
       } = {},
       end: {
         line: endLine = Infinity,
@@ -286,17 +298,17 @@ const SourceActor = ActorClassWithSpec(s
       }
 
       const offsets = script.getPossibleBreakpoints();
       for (const { lineNumber, columnNumber } of offsets) {
         if (
           lineNumber < startLine ||
           (lineNumber === startLine && columnNumber < startColumn) ||
           lineNumber > endLine ||
-          (lineNumber === endLine && columnNumber > endColumn)
+          (lineNumber === endLine && columnNumber >= endColumn)
         ) {
           continue;
         }
 
         positions.push({
           line: lineNumber,
           column: columnNumber,
         });
--- a/devtools/shared/specs/source.js
+++ b/devtools/shared/specs/source.js
@@ -1,14 +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/. */
 "use strict";
 
-const {Arg, RetVal, generateActorSpec, types} = require("devtools/shared/protocol");
+const { Arg, RetVal, generateActorSpec, types } = require("devtools/shared/protocol");
 
 const longstringType = types.getType("longstring");
 const arraybufferType = types.getType("arraybuffer");
 // The sourcedata type needs some custom marshalling, because it is sometimes
 // returned as an arraybuffer and sometimes as a longstring.
 types.addType("sourcedata", {
   write: (value, context, detail) => {
     if (value.typeName === "arraybuffer") {
@@ -60,16 +60,22 @@ const sourceSpec = generateActorSpec({
     getBreakpointPositionsCompressed: {
       request: {
         query: Arg(0, "nullable:breakpointquery"),
       },
       response: {
         positions: RetVal("json"),
       },
     },
+    getBreakableLines: {
+      request: {},
+      response: {
+        lines: RetVal("json"),
+      },
+    },
     onSource: {
       // we are sending the type "source" to be compatible
       // with FF67 and older
       request: { type: "source" },
       response: RetVal("source.onsource"),
     },
     setPausePoints: {
       request: {