diff --git a/devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js b/devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js index 89b6dff2eb1c..6e1fce8fdc0e 100644 --- a/devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js +++ b/devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js @@ -14,10 +14,12 @@ import { getSource, getSourceFromId, hasBreakpointPositions, - getBreakpointPositionsForSource + getBreakpointPositionsForSource, + getSourceActorsForSource } from "../../selectors"; import type { + SourceId, MappedLocation, Range, SourceLocation, @@ -30,8 +32,6 @@ import { type MemoizedAction } from "../../utils/memoizableAction"; -// const requests = new Map(); - async function mapLocations( generatedLocations: SourceLocation[], { sourceMaps }: { sourceMaps: typeof SourceMaps } @@ -108,13 +108,18 @@ async function _setBreakpointPositions(cx, sourceId, thunkArgs) { }; } - const bps = await client.getBreakpointPositions(generatedSource, range); + const bps = await client.getBreakpointPositions( + getSourceActorsForSource(getState(), generatedSource.id), + range + ); for (const line in bps) { results[line] = (results[line] || []).concat(bps[line]); } } } else { - results = await client.getBreakpointPositions(generatedSource); + results = await client.getBreakpointPositions( + getSourceActorsForSource(getState(), generatedSource.id) + ); } let positions = convertToList(results, generatedSource); @@ -139,6 +144,11 @@ async function _setBreakpointPositions(cx, sourceId, thunkArgs) { return positions; } +const runningFetches = {}; +export function isFetchingBreakpoints(id: SourceId) { + return id in runningFetches; +} + export const setBreakpointPositions: MemoizedAction< { cx: Context, sourceId: string }, ?BreakpointPositions @@ -153,10 +163,21 @@ export const setBreakpointPositions: MemoizedAction< isOriginalId(sourceId) ? originalToGeneratedId(sourceId) : sourceId ); const actors = generatedSource - ? generatedSource.actors.map(({ actor }) => actor) + ? getSourceActorsForSource(getState(), generatedSource.id).map( + ({ actor }) => actor + ) : []; return [sourceId, ...actors].join(":"); }, - action: ({ cx, sourceId }, thunkArgs) => - _setBreakpointPositions(cx, sourceId, thunkArgs) + 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]; + } + } + } }); diff --git a/devtools/client/debugger/src/actions/breakpoints/tests/__snapshots__/breakpoints.spec.js.snap b/devtools/client/debugger/src/actions/breakpoints/tests/__snapshots__/breakpoints.spec.js.snap index a1fdcc249e40..d4526e55a216 100644 --- a/devtools/client/debugger/src/actions/breakpoints/tests/__snapshots__/breakpoints.spec.js.snap +++ b/devtools/client/debugger/src/actions/breakpoints/tests/__snapshots__/breakpoints.spec.js.snap @@ -35,13 +35,6 @@ Array [ }, ], "source": Object { - "actors": Array [ - Object { - "actor": "a-1-actor", - "source": "a", - "thread": "FakeThread", - }, - ], "contentType": "text/javascript", "id": "a", "introductionType": null, @@ -129,13 +122,6 @@ Array [ }, ], "source": Object { - "actors": Array [ - Object { - "actor": "a-1-actor", - "source": "a", - "thread": "FakeThread", - }, - ], "contentType": "text/javascript", "id": "a", "introductionType": null, diff --git a/devtools/client/debugger/src/actions/breakpoints/tests/breakpointPositions.spec.js b/devtools/client/debugger/src/actions/breakpoints/tests/breakpointPositions.spec.js index 310ae0b9745d..01d21a110973 100644 --- a/devtools/client/debugger/src/actions/breakpoints/tests/breakpointPositions.spec.js +++ b/devtools/client/debugger/src/actions/breakpoints/tests/breakpointPositions.spec.js @@ -11,15 +11,22 @@ import { makeSource, waitForState } from "../../../utils/test-head"; +import { createSource } from "../../tests/helpers/threadClient"; describe("breakpointPositions", () => { it("fetches positions", async () => { + const fooContent = createSource("foo", ""); + const store = createStore({ - getBreakpointPositions: async () => ({ "9": [1] }) + getBreakpointPositions: async () => ({ "9": [1] }), + sourceContents: async () => fooContent }); const { dispatch, getState, cx } = store; - await dispatch(actions.newGeneratedSource(makeSource("foo"))); + const source = await dispatch( + actions.newGeneratedSource(makeSource("foo")) + ); + await dispatch(actions.loadSourceById(cx, source.id)); dispatch(actions.setBreakpointPositions({ cx, sourceId: "foo" })); @@ -48,6 +55,8 @@ describe("breakpointPositions", () => { }); it("doesn't re-fetch positions", async () => { + const fooContent = createSource("foo", ""); + let resolve = _ => {}; let count = 0; const store = createStore({ @@ -55,11 +64,15 @@ describe("breakpointPositions", () => { new Promise(r => { count++; resolve = r; - }) + }), + sourceContents: async () => fooContent }); const { dispatch, getState, cx } = store; - await dispatch(actions.newGeneratedSource(makeSource("foo"))); + 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" })); diff --git a/devtools/client/debugger/src/actions/debuggee.js b/devtools/client/debugger/src/actions/debuggee.js index bfe693e3ac82..85ba98a58dbe 100644 --- a/devtools/client/debugger/src/actions/debuggee.js +++ b/devtools/client/debugger/src/actions/debuggee.js @@ -6,8 +6,9 @@ import { differenceBy } from "lodash"; import type { Action, ThunkArgs } from "./types"; +import { removeSourceActors } from "./source-actors"; -import { getContext, getWorkers } from "../selectors"; +import { getContext, getWorkers, getSourceActorsForThread } from "../selectors"; export function updateWorkers() { return async function({ dispatch, getState, client }: ThunkArgs) { @@ -19,6 +20,11 @@ export function updateWorkers() { const addedWorkers = differenceBy(workers, currentWorkers, w => w.actor); const removedWorkers = differenceBy(currentWorkers, workers, w => w.actor); if (removedWorkers.length > 0) { + const sourceActors = getSourceActorsForThread( + getState(), + removedWorkers.map(w => w.actor) + ); + dispatch(removeSourceActors(sourceActors)); dispatch( ({ type: "REMOVE_WORKERS", diff --git a/devtools/client/debugger/src/actions/sources/blackbox.js b/devtools/client/debugger/src/actions/sources/blackbox.js index 03680f5ae2b5..487ceb98803d 100644 --- a/devtools/client/debugger/src/actions/sources/blackbox.js +++ b/devtools/client/debugger/src/actions/sources/blackbox.js @@ -12,7 +12,7 @@ import { isOriginalId, originalToGeneratedId } from "devtools-source-map"; import { recordEvent } from "../../utils/telemetry"; import { features } from "../../utils/prefs"; -import { getSourceFromId } from "../../selectors"; +import { getSourceActorsForSource } from "../../selectors"; import { PROMISE } from "../utils/middleware/promise"; @@ -20,9 +20,8 @@ import type { Source, Context } from "../../types"; import type { ThunkArgs } from "../types"; async function blackboxActors(state, client, sourceId, isBlackBoxed, range?) { - const source = getSourceFromId(state, sourceId); - for (const sourceActor of source.actors) { - await client.blackBox(sourceActor, isBlackBoxed, range); + for (const actor of getSourceActorsForSource(state, sourceId)) { + await client.blackBox(actor, isBlackBoxed, range); } return { isBlackBoxed: !isBlackBoxed }; } diff --git a/devtools/client/debugger/src/actions/sources/loadSourceText.js b/devtools/client/debugger/src/actions/sources/loadSourceText.js index 189bc432b016..72e80c5e4505 100644 --- a/devtools/client/debugger/src/actions/sources/loadSourceText.js +++ b/devtools/client/debugger/src/actions/sources/loadSourceText.js @@ -10,7 +10,8 @@ import { getSourceFromId, getGeneratedSource, getSourcesEpoch, - getBreakpointsForSource + getBreakpointsForSource, + getSourceActorsForSource } from "../../selectors"; import { setBreakpointPositions, addBreakpoint } from "../breakpoints"; @@ -36,14 +37,23 @@ const telemetry = new Telemetry(); async function loadSource( state, source: Source, - { sourceMaps, client } + { sourceMaps, client, getState } ): Promise { if (isPretty(source) && isOriginal(source)) { const generatedSource = getGeneratedSource(state, source); - return prettyPrintSource(sourceMaps, source, generatedSource); + if (!generatedSource) { + throw new Error("Unable to find minified original."); + } + + return prettyPrintSource( + sourceMaps, + source, + generatedSource, + getSourceActorsForSource(state, generatedSource.id) + ); } if (isOriginal(source)) { @@ -58,12 +68,13 @@ async function loadSource( return result; } - if (!source.actors.length) { + const actors = getSourceActorsForSource(state, source.id); + if (!actors.length) { throw new Error("No source actor for loadSource"); } telemetry.start(loadSourceHistogram, source); - const response = await client.sourceContents(source.actors[0]); + const response = await client.sourceContents(actors[0]); telemetry.finish(loadSourceHistogram, source); return { @@ -82,7 +93,7 @@ async function loadSourceTextPromise( type: "LOAD_SOURCE_TEXT", sourceId: source.id, epoch, - [PROMISE]: loadSource(getState(), source, { sourceMaps, client }) + [PROMISE]: loadSource(getState(), source, { sourceMaps, client, getState }) }); const newSource = getSource(getState(), source.id); diff --git a/devtools/client/debugger/src/actions/sources/newSources.js b/devtools/client/debugger/src/actions/sources/newSources.js index abc830a5b87a..fe241ea39b10 100644 --- a/devtools/client/debugger/src/actions/sources/newSources.js +++ b/devtools/client/debugger/src/actions/sources/newSources.js @@ -12,25 +12,32 @@ import { generatedToOriginalId } from "devtools-source-map"; 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 { loadSourceText } from "./loadSourceText"; +import { isFetchingBreakpoints } from "../breakpoints/breakpointPositions"; import { togglePrettyPrint } from "./prettyPrint"; import { selectLocation } from "../sources"; import { getRawSourceURL, isPrettyURL, isOriginal, - isInlineScript, - isUrlExtension + isUrlExtension, + isInlineScript } from "../../utils/source"; import { getBlackBoxList, getSource, + getSourceFromId, + hasSourceActor, getPendingSelectedLocation, getPendingBreakpointsForSource, - hasBreakpointPositions, getContext } from "../../selectors"; @@ -245,8 +252,8 @@ export function newOriginalSource(sourceInfo: OriginalSourceData) { }; } export function newOriginalSources(sourceInfo: Array) { - return async ({ dispatch }: ThunkArgs) => { - const sources = sourceInfo.map(({ id, url }) => ({ + return async ({ dispatch, getState }: ThunkArgs) => { + const sources: Array = sourceInfo.map(({ id, url }) => ({ id, url, relativeUrl: url, @@ -256,11 +263,15 @@ export function newOriginalSources(sourceInfo: Array) { loadedState: "unloaded", introductionUrl: null, introductionType: undefined, - isExtension: false, - actors: [] + isExtension: false })); - return dispatch(newInnerSources(sources)); + const cx = getContext(getState()); + dispatch(addSources(cx, sources)); + + await dispatch(checkNewSources(cx, sources)); + + return sources; }; } @@ -271,65 +282,101 @@ export function newGeneratedSource(sourceInfo: GeneratedSourceData) { }; } export function newGeneratedSources(sourceInfo: Array) { - return async ({ dispatch, client }: ThunkArgs) => { + return async ({ + dispatch, + getState, + client + }: ThunkArgs): Promise> => { const supportsWasm = client.hasWasmSupport(); - const sources: Array = sourceInfo.map(({ thread, source, id }) => { - id = id || makeSourceId(source); - const sourceActor = { - actor: source.actor, - source: id, - thread - }; - const createdSource: any = { - id, - url: source.url, - relativeUrl: source.url, - isPrettyPrinted: false, - sourceMapURL: source.sourceMapURL, - introductionUrl: source.introductionUrl, - introductionType: source.introductionType, - isBlackBoxed: false, - loadedState: "unloaded", - isWasm: !!supportsWasm && source.introductionType === "wasm", - isExtension: (source.url && isUrlExtension(source.url)) || false, - actors: [sourceActor] - }; - return createdSource; - }); - return dispatch(newInnerSources(sources)); - }; -} + const resultIds = []; + const newSources: Array = []; + const newSourceActors: Array = []; -function newInnerSources(sources: Source[]) { - return async ({ dispatch, getState }: ThunkArgs) => { - const cx = getContext(getState()); + for (const { thread, source, id } of sourceInfo) { + const newId = id || makeSourceId(source); - const _newSources = sources.filter( - source => !getSource(getState(), source.id) || isInlineScript(source) - ); + if (!getSource(getState(), newId)) { + newSources.push( + ({ + id: newId, + url: source.url, + relativeUrl: source.url, + isPrettyPrinted: false, + sourceMapURL: source.sourceMapURL, + introductionUrl: source.introductionUrl, + introductionType: source.introductionType, + isBlackBoxed: false, + loadedState: "unloaded", + isWasm: !!supportsWasm && source.introductionType === "wasm", + isExtension: (source.url && isUrlExtension(source.url)) || false + }: any) + ); + } - const sourcesNeedingPositions = _newSources.filter(source => - hasBreakpointPositions(getState(), source.id) - ); + const actorId = stringToSourceActorId(source.actor); - dispatch({ type: "ADD_SOURCES", cx, sources }); + // 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, - for (const source of _newSources) { - dispatch(checkSelectedSource(cx, source.id)); + isBlackBoxed: source.isBlackBoxed, + sourceMapURL: source.sourceMapURL, + url: source.url, + introductionUrl: source.introductionUrl, + introductionType: source.introductionType + }); + } + + resultIds.push(newId); } + 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