diff options
Diffstat (limited to '')
-rw-r--r-- | devtools/client/debugger/src/reducers/pause.js | 409 |
1 files changed, 409 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/reducers/pause.js b/devtools/client/debugger/src/reducers/pause.js new file mode 100644 index 0000000000..b6de8dc2f2 --- /dev/null +++ b/devtools/client/debugger/src/reducers/pause.js @@ -0,0 +1,409 @@ +/* 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/>. */ + +/* eslint complexity: ["error", 36]*/ + +/** + * Pause reducer + * @module reducers/pause + */ + +import { prefs } from "../utils/prefs"; + +// Pause state associated with an individual thread. + +// Pause state describing all threads. + +export function initialPauseState(thread = "UnknownThread") { + return { + cx: { + navigateCounter: 0, + }, + // This `threadcx` is the `cx` variable we pass around in components and actions. + // This is pulled via getThreadContext(). + // This stores information about the currently selected thread and its paused state. + threadcx: { + navigateCounter: 0, + thread, + pauseCounter: 0, + }, + highlightedCalls: null, + threads: {}, + skipPausing: prefs.skipPausing, + mapScopes: prefs.mapScopes, + shouldPauseOnExceptions: prefs.pauseOnExceptions, + shouldPauseOnCaughtExceptions: prefs.pauseOnCaughtExceptions, + }; +} + +const resumedPauseState = { + isPaused: false, + frames: null, + framesLoading: false, + frameScopes: { + generated: {}, + original: {}, + mappings: {}, + }, + selectedFrameId: null, + why: null, + inlinePreview: {}, + highlightedCalls: null, +}; + +const createInitialPauseState = () => ({ + ...resumedPauseState, + isWaitingOnBreak: false, + command: null, + previousLocation: null, + expandedScopes: new Set(), + lastExpandedScopes: [], +}); + +export function getThreadPauseState(state, thread) { + // Thread state is lazily initialized so that we don't have to keep track of + // the current set of worker threads. + return state.threads[thread] || createInitialPauseState(); +} + +function update(state = initialPauseState(), action) { + // Actions need to specify any thread they are operating on. These helpers + // manage updating the pause state for that thread. + const threadState = () => { + if (!action.thread) { + throw new Error(`Missing thread in action ${action.type}`); + } + return getThreadPauseState(state, action.thread); + }; + + const updateThreadState = newThreadState => { + if (!action.thread) { + throw new Error(`Missing thread in action ${action.type}`); + } + return { + ...state, + threads: { + ...state.threads, + [action.thread]: { ...threadState(), ...newThreadState }, + }, + }; + }; + + switch (action.type) { + case "SELECT_THREAD": { + return { + ...state, + threadcx: { + ...state.threadcx, + thread: action.thread, + pauseCounter: state.threadcx.pauseCounter + 1, + }, + }; + } + + case "INSERT_THREAD": { + // When navigating to a new location, + // we receive NAVIGATE early, which clear things + // then we have REMOVE_THREAD of the previous thread. + // INSERT_THREAD will be the very first event with the new thread actor ID. + // Automatically select the new top level thread. + if (action.newThread.isTopLevel) { + return { + ...state, + threadcx: { + ...state.threadcx, + thread: action.newThread.actor, + pauseCounter: state.threadcx.pauseCounter + 1, + }, + }; + } + break; + } + + case "REMOVE_THREAD": { + if ( + action.threadActorID in state.threads || + action.threadActorID == state.threadcx.thread + ) { + // Remove the thread from the cached list + const threads = { ...state.threads }; + delete threads[action.threadActorID]; + let threadcx = state.threadcx; + + // And also switch to another thread if this was the currently selected one. + // As we don't store thread objects in this reducer, and only store thread actor IDs, + // we can't try to find the top level thread. So we pick the first available thread, + // and hope that's the top level one. + if (state.threadcx.thread == action.threadActorID) { + threadcx = { + ...threadcx, + thread: Object.keys(threads)[0], + pauseCounter: threadcx.pauseCounter + 1, + }; + } + return { + ...state, + threadcx, + threads, + }; + } + break; + } + + case "PAUSED": { + const { thread, frame, why } = action; + state = { + ...state, + threadcx: { + ...state.threadcx, + pauseCounter: state.threadcx.pauseCounter + 1, + thread, + }, + }; + + return updateThreadState({ + isWaitingOnBreak: false, + selectedFrameId: frame ? frame.id : undefined, + isPaused: true, + frames: frame ? [frame] : undefined, + framesLoading: true, + frameScopes: { ...resumedPauseState.frameScopes }, + why, + shouldBreakpointsPaneOpenOnPause: why.type === "breakpoint", + }); + } + + case "FETCHED_FRAMES": { + const { frames } = action; + + // We typically receive a PAUSED action before this one, + // with only the first frame. Here, we avoid replacing it + // with a copy of it in order to avoid triggerring selectors + // uncessarily + // (note that in jest, action's frames might be empty) + // (and if we resume in between PAUSED and FETCHED_FRAMES + // threadState().frames might be null) + if (threadState().frames) { + const previousFirstFrame = threadState().frames[0]; + if (previousFirstFrame.id == frames[0]?.id) { + frames.splice(0, 1, previousFirstFrame); + } + } + return updateThreadState({ frames, framesLoading: false }); + } + + case "MAP_FRAMES": { + const { selectedFrameId, frames } = action; + return updateThreadState({ frames, selectedFrameId }); + } + + case "MAP_FRAME_DISPLAY_NAMES": { + const { frames } = action; + return updateThreadState({ frames }); + } + + case "ADD_SCOPES": { + const { frame, status, value } = action; + const selectedFrameId = frame.id; + + const generated = { + ...threadState().frameScopes.generated, + [selectedFrameId]: { + pending: status !== "done", + scope: value, + }, + }; + + return updateThreadState({ + frameScopes: { + ...threadState().frameScopes, + generated, + }, + }); + } + + case "MAP_SCOPES": { + const { frame, status, value } = action; + const selectedFrameId = frame.id; + + const original = { + ...threadState().frameScopes.original, + [selectedFrameId]: { + pending: status !== "done", + scope: value?.scope, + }, + }; + + const mappings = { + ...threadState().frameScopes.mappings, + [selectedFrameId]: value?.mappings, + }; + + return updateThreadState({ + frameScopes: { + ...threadState().frameScopes, + original, + mappings, + }, + }); + } + + case "BREAK_ON_NEXT": + return updateThreadState({ isWaitingOnBreak: true }); + + case "SELECT_FRAME": + return updateThreadState({ selectedFrameId: action.frame.id }); + + case "PAUSE_ON_EXCEPTIONS": { + const { shouldPauseOnExceptions, shouldPauseOnCaughtExceptions } = action; + + prefs.pauseOnExceptions = shouldPauseOnExceptions; + prefs.pauseOnCaughtExceptions = shouldPauseOnCaughtExceptions; + + // Preserving for the old debugger + prefs.ignoreCaughtExceptions = !shouldPauseOnCaughtExceptions; + + return { + ...state, + shouldPauseOnExceptions, + shouldPauseOnCaughtExceptions, + }; + } + + case "COMMAND": + if (action.status === "start") { + return updateThreadState({ + ...resumedPauseState, + command: action.command, + previousLocation: getPauseLocation(threadState(), action), + }); + } + return updateThreadState({ command: null }); + + case "RESUME": { + if (action.thread == state.threadcx.thread) { + state = { + ...state, + threadcx: { + ...state.threadcx, + pauseCounter: state.threadcx.pauseCounter + 1, + }, + }; + } + + return updateThreadState({ + ...resumedPauseState, + expandedScopes: new Set(), + lastExpandedScopes: [...threadState().expandedScopes], + shouldBreakpointsPaneOpenOnPause: false, + }); + } + + case "EVALUATE_EXPRESSION": + return updateThreadState({ + command: action.status === "start" ? "expression" : null, + }); + + case "NAVIGATE": { + const navigateCounter = state.cx.navigateCounter + 1; + return { + ...state, + cx: { + navigateCounter, + }, + threadcx: { + navigateCounter, + thread: action.mainThread.actor, + pauseCounter: 0, + }, + threads: { + ...state.threads, + [action.mainThread.actor]: { + ...getThreadPauseState(state, action.mainThread.actor), + ...resumedPauseState, + }, + }, + }; + } + + case "TOGGLE_SKIP_PAUSING": { + const { skipPausing } = action; + prefs.skipPausing = skipPausing; + + return { ...state, skipPausing }; + } + + case "TOGGLE_MAP_SCOPES": { + const { mapScopes } = action; + prefs.mapScopes = mapScopes; + return { ...state, mapScopes }; + } + + case "SET_EXPANDED_SCOPE": { + const { path, expanded } = action; + const expandedScopes = new Set(threadState().expandedScopes); + if (expanded) { + expandedScopes.add(path); + } else { + expandedScopes.delete(path); + } + return updateThreadState({ expandedScopes }); + } + + case "ADD_INLINE_PREVIEW": { + const { frame, previews } = action; + const selectedFrameId = frame.id; + + return updateThreadState({ + inlinePreview: { + ...threadState().inlinePreview, + [selectedFrameId]: previews, + }, + }); + } + + case "HIGHLIGHT_CALLS": { + const { highlightedCalls } = action; + return updateThreadState({ ...threadState(), highlightedCalls }); + } + + case "UNHIGHLIGHT_CALLS": { + return updateThreadState({ + ...threadState(), + highlightedCalls: null, + }); + } + + case "RESET_BREAKPOINTS_PANE_STATE": { + return updateThreadState({ + ...threadState(), + shouldBreakpointsPaneOpenOnPause: false, + }); + } + } + + return state; +} + +function getPauseLocation(state, action) { + const { frames, previousLocation } = state; + + // NOTE: We store the previous location so that we ensure that we + // do not stop at the same location twice when we step over. + if (action.command !== "stepOver") { + return null; + } + + const frame = frames?.[0]; + if (!frame) { + return previousLocation; + } + + return { + location: frame.location, + generatedLocation: frame.generatedLocation, + }; +} + +export default update; |