diff options
Diffstat (limited to 'devtools/client/performance-new/store')
-rw-r--r-- | devtools/client/performance-new/store/README.md | 3 | ||||
-rw-r--r-- | devtools/client/performance-new/store/actions.js | 247 | ||||
-rw-r--r-- | devtools/client/performance-new/store/moz.build | 10 | ||||
-rw-r--r-- | devtools/client/performance-new/store/reducers.js | 230 | ||||
-rw-r--r-- | devtools/client/performance-new/store/selectors.js | 179 |
5 files changed, 669 insertions, 0 deletions
diff --git a/devtools/client/performance-new/store/README.md b/devtools/client/performance-new/store/README.md new file mode 100644 index 0000000000..dc6396bc6b --- /dev/null +++ b/devtools/client/performance-new/store/README.md @@ -0,0 +1,3 @@ +# Performance New Store + +This folder contains the Redux store logic for both the DevTools Panel and about:profiling. The store is NOT used for the popup, which does not use React / Redux. diff --git a/devtools/client/performance-new/store/actions.js b/devtools/client/performance-new/store/actions.js new file mode 100644 index 0000000000..bac265d159 --- /dev/null +++ b/devtools/client/performance-new/store/actions.js @@ -0,0 +1,247 @@ +/* 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/. */ +// @ts-check +"use strict"; + +const selectors = require("devtools/client/performance-new/store/selectors"); +const { + translatePreferencesToState, + translatePreferencesFromState, +} = require("devtools/client/performance-new/preference-management"); +const { + getEnvironmentVariable, +} = require("devtools/client/performance-new/browser"); + +/** + * @typedef {import("../@types/perf").Action} Action + * @typedef {import("../@types/perf").Library} Library + * @typedef {import("../@types/perf").PerfFront} PerfFront + * @typedef {import("../@types/perf").SymbolTableAsTuple} SymbolTableAsTuple + * @typedef {import("../@types/perf").RecordingState} RecordingState + * @typedef {import("../@types/perf").InitializeStoreValues} InitializeStoreValues + * @typedef {import("../@types/perf").Presets} Presets + * @typedef {import("../@types/perf").PanelWindow} PanelWindow + */ + +/** + * @template T + * @typedef {import("../@types/perf").ThunkAction<T>} ThunkAction<T> + */ + +/** + * The recording state manages the current state of the recording panel. + * @param {RecordingState} state - A valid state in `recordingState`. + * @param {{ didRecordingUnexpectedlyStopped: boolean }} options + * @return {Action} + */ +const changeRecordingState = (exports.changeRecordingState = ( + state, + options = { didRecordingUnexpectedlyStopped: false } +) => ({ + type: "CHANGE_RECORDING_STATE", + state, + didRecordingUnexpectedlyStopped: options.didRecordingUnexpectedlyStopped, +})); + +/** + * This is the result of the initial questions about the state of the profiler. + * + * @param {boolean} isSupportedPlatform - This is a supported platform. + * @param {RecordingState} recordingState - A valid state in `recordingState`. + * @return {Action} + */ +exports.reportProfilerReady = (isSupportedPlatform, recordingState) => ({ + type: "REPORT_PROFILER_READY", + isSupportedPlatform, + recordingState, +}); + +/** + * Dispatch the given action, and then update the recording settings. + * @param {Action} action + * @return {ThunkAction<void>} + */ +function _dispatchAndUpdatePreferences(action) { + return ({ dispatch, getState }) => { + if (typeof action !== "object") { + throw new Error( + "This function assumes that the dispatched action is a simple object and " + + "synchronous." + ); + } + dispatch(action); + const setRecordingPreferences = selectors.getSetRecordingPreferencesFn( + getState() + ); + const recordingSettings = selectors.getRecordingSettings(getState()); + setRecordingPreferences(translatePreferencesFromState(recordingSettings)); + }; +} + +/** + * Updates the recording settings for the interval. + * @param {number} interval + * @return {ThunkAction<void>} + */ +exports.changeInterval = interval => + _dispatchAndUpdatePreferences({ + type: "CHANGE_INTERVAL", + interval, + }); + +/** + * Updates the recording settings for the entries. + * @param {number} entries + * @return {ThunkAction<void>} + */ +exports.changeEntries = entries => + _dispatchAndUpdatePreferences({ + type: "CHANGE_ENTRIES", + entries, + }); + +/** + * Updates the recording settings for the features. + * @param {string[]} features + * @return {ThunkAction<void>} + */ +exports.changeFeatures = features => { + return ({ dispatch, getState }) => { + let promptEnvRestart = null; + if (selectors.getPageContext(getState()) === "aboutprofiling") { + // TODO Bug 1615431 - The popup supported restarting the browser, but + // this hasn't been updated yet for the about:profiling workflow. + if ( + !getEnvironmentVariable("JS_TRACE_LOGGING") && + features.includes("jstracer") + ) { + promptEnvRestart = "JS_TRACE_LOGGING"; + } + } + + dispatch( + _dispatchAndUpdatePreferences({ + type: "CHANGE_FEATURES", + features, + promptEnvRestart, + }) + ); + }; +}; + +/** + * Updates the recording settings for the threads. + * @param {string[]} threads + * @return {ThunkAction<void>} + */ +exports.changeThreads = threads => + _dispatchAndUpdatePreferences({ + type: "CHANGE_THREADS", + threads, + }); + +/** + * Change the preset. + * @param {Presets} presets + * @param {string} presetName + * @return {ThunkAction<void>} + */ +exports.changePreset = (presets, presetName) => + _dispatchAndUpdatePreferences({ + type: "CHANGE_PRESET", + presetName, + // Also dispatch the preset so that the reducers can pre-fill the values + // from a preset. + preset: presets[presetName], + }); + +/** + * Updates the recording settings for the objdirs. + * @param {string[]} objdirs + * @return {ThunkAction<void>} + */ +exports.changeObjdirs = objdirs => + _dispatchAndUpdatePreferences({ + type: "CHANGE_OBJDIRS", + objdirs, + }); + +/** + * Receive the values to initialize the store. See the reducer for what values + * are expected. + * @param {InitializeStoreValues} values + * @return {Action} + */ +exports.initializeStore = values => { + const { recordingPreferences, ...initValues } = values; + return { + ...initValues, + type: "INITIALIZE_STORE", + recordingSettingsFromPreferences: translatePreferencesToState( + recordingPreferences + ), + }; +}; + +/** + * Start a new recording with the perfFront and update the internal recording state. + * @return {ThunkAction<void>} + */ +exports.startRecording = () => { + return ({ dispatch, getState }) => { + const recordingSettings = selectors.getRecordingSettings(getState()); + const perfFront = selectors.getPerfFront(getState()); + // In the case of the profiler popup, the startProfiler can be synchronous. + // In order to properly allow the React components to handle the state changes + // make sure and change the recording state first, then start the profiler. + dispatch(changeRecordingState("request-to-start-recording")); + perfFront.startProfiler(recordingSettings); + }; +}; + +/** + * Stops the profiler, and opens the profile in a new window. + * @return {ThunkAction<void>} + */ +exports.getProfileAndStopProfiler = () => { + return async ({ dispatch, getState }) => { + const perfFront = selectors.getPerfFront(getState()); + dispatch(changeRecordingState("request-to-get-profile-and-stop-profiler")); + const profile = await perfFront.getProfileAndStopProfiler(); + + const getSymbolTable = selectors.getSymbolTableGetter(getState())(profile); + const receiveProfile = selectors.getReceiveProfileFn(getState()); + const profilerViewMode = selectors.getProfilerViewMode(getState()); + receiveProfile(profile, profilerViewMode, getSymbolTable); + dispatch(changeRecordingState("available-to-record")); + }; +}; + +/** + * Stops the profiler, but does not try to retrieve the profile. + * @return {ThunkAction<void>} + */ +exports.stopProfilerAndDiscardProfile = () => { + return async ({ dispatch, getState }) => { + const perfFront = selectors.getPerfFront(getState()); + dispatch(changeRecordingState("request-to-stop-profiler")); + + try { + await perfFront.stopProfilerAndDiscardProfile(); + } catch (error) { + /** @type {any} */ + const anyWindow = window; + /** @type {PanelWindow} - Coerce the window into the PanelWindow. */ + const { gIsPanelDestroyed } = anyWindow; + + if (gIsPanelDestroyed) { + // This error is most likely "Connection closed, pending request" as the + // command can race with closing the panel. Do not report an error. It's + // most likely fine. + } else { + throw error; + } + } + }; +}; diff --git a/devtools/client/performance-new/store/moz.build b/devtools/client/performance-new/store/moz.build new file mode 100644 index 0000000000..16c3f8c65a --- /dev/null +++ b/devtools/client/performance-new/store/moz.build @@ -0,0 +1,10 @@ +# vim: set filetype=python: +# 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/. + +DevToolsModules( + "actions.js", + "reducers.js", + "selectors.js", +) diff --git a/devtools/client/performance-new/store/reducers.js b/devtools/client/performance-new/store/reducers.js new file mode 100644 index 0000000000..40a07ecc86 --- /dev/null +++ b/devtools/client/performance-new/store/reducers.js @@ -0,0 +1,230 @@ +/* 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/. */ +// @ts-check +"use strict"; + +const { combineReducers } = require("devtools/client/shared/vendor/redux"); + +/** + * @typedef {import("../@types/perf").Action} Action + * @typedef {import("../@types/perf").State} State + * @typedef {import("../@types/perf").RecordingState} RecordingState + * @typedef {import("../@types/perf").InitializedValues} InitializedValues + */ + +/** + * @template S + * @typedef {import("../@types/perf").Reducer<S>} Reducer<S> + */ + +/** + * The current state of the recording. + * @type {Reducer<RecordingState>} + */ +function recordingState(state = "not-yet-known", action) { + switch (action.type) { + case "CHANGE_RECORDING_STATE": + return action.state; + case "REPORT_PROFILER_READY": + return action.recordingState; + default: + return state; + } +} + +/** + * Whether or not the recording state unexpectedly stopped. This allows + * the UI to display a helpful message. + * @type {Reducer<boolean>} + */ +function recordingUnexpectedlyStopped(state = false, action) { + switch (action.type) { + case "CHANGE_RECORDING_STATE": + return action.didRecordingUnexpectedlyStopped; + default: + return state; + } +} + +/** + * The profiler needs to be queried asynchronously on whether or not + * it supports the user's platform. + * @type {Reducer<boolean | null>} + */ +function isSupportedPlatform(state = null, action) { + switch (action.type) { + case "REPORT_PROFILER_READY": + return action.isSupportedPlatform; + default: + return state; + } +} + +// Right now all recording setting the defaults are reset every time the panel +// is opened. These should be persisted between sessions. See Bug 1453014. + +/** + * The setting for the recording interval. Defaults to 1ms. + * @type {Reducer<number>} + */ +function interval(state = 1, action) { + switch (action.type) { + case "CHANGE_INTERVAL": + return action.interval; + case "CHANGE_PRESET": + return action.preset ? action.preset.interval : state; + case "INITIALIZE_STORE": + return action.recordingSettingsFromPreferences.interval; + default: + return state; + } +} + +/** + * The number of entries in the profiler's circular buffer. + * @type {Reducer<number>} + */ +function entries(state = 0, action) { + switch (action.type) { + case "CHANGE_ENTRIES": + return action.entries; + case "CHANGE_PRESET": + return action.preset ? action.preset.entries : state; + case "INITIALIZE_STORE": + return action.recordingSettingsFromPreferences.entries; + default: + return state; + } +} + +/** + * The features that are enabled for the profiler. + * @type {Reducer<string[]>} + */ +function features(state = [], action) { + switch (action.type) { + case "CHANGE_FEATURES": + return action.features; + case "CHANGE_PRESET": + return action.preset ? action.preset.features : state; + case "INITIALIZE_STORE": + return action.recordingSettingsFromPreferences.features; + default: + return state; + } +} + +/** + * The current threads list. + * @type {Reducer<string[]>} + */ +function threads(state = [], action) { + switch (action.type) { + case "CHANGE_THREADS": + return action.threads; + case "CHANGE_PRESET": + return action.preset ? action.preset.threads : state; + case "INITIALIZE_STORE": + return action.recordingSettingsFromPreferences.threads; + default: + return state; + } +} + +/** + * The current objdirs list. + * @type {Reducer<string[]>} + */ +function objdirs(state = [], action) { + switch (action.type) { + case "CHANGE_OBJDIRS": + return action.objdirs; + case "INITIALIZE_STORE": + return action.recordingSettingsFromPreferences.objdirs; + default: + return state; + } +} + +/** + * The current preset name, used to select + * @type {Reducer<string>} + */ +function presetName(state = "", action) { + switch (action.type) { + case "INITIALIZE_STORE": + return action.recordingSettingsFromPreferences.presetName; + case "CHANGE_PRESET": + return action.presetName; + case "CHANGE_INTERVAL": + case "CHANGE_ENTRIES": + case "CHANGE_FEATURES": + case "CHANGE_THREADS": + // When updating any values, switch the preset over to "custom". + return "custom"; + default: + return state; + } +} + +/** + * These are all the values used to initialize the profiler. They should never + * change once added to the store. + * + * @type {Reducer<InitializedValues | null>} + */ +function initializedValues(state = null, action) { + switch (action.type) { + case "INITIALIZE_STORE": + return { + perfFront: action.perfFront, + receiveProfile: action.receiveProfile, + setRecordingPreferences: action.setRecordingPreferences, + presets: action.presets, + pageContext: action.pageContext, + getSymbolTableGetter: action.getSymbolTableGetter, + supportedFeatures: action.supportedFeatures, + openAboutProfiling: action.openAboutProfiling, + openRemoteDevTools: action.openRemoteDevTools, + }; + default: + return state; + } +} + +/** + * Some features may need a browser restart with an environment flag. Request + * one here. + * + * @type {Reducer<string | null>} + */ +function promptEnvRestart(state = null, action) { + switch (action.type) { + case "CHANGE_FEATURES": + return action.promptEnvRestart; + default: + return state; + } +} + +/** + * The main reducer for the performance-new client. + * @type {Reducer<State>} + */ +module.exports = combineReducers({ + // TODO - The object going into `combineReducers` is not currently type checked + // as being correct for. For instance, recordingState here could be removed, or + // not return the right state, and TypeScript will not create an error. + recordingState, + recordingUnexpectedlyStopped, + isSupportedPlatform, + interval, + entries, + features, + threads, + objdirs, + presetName, + initializedValues, + promptEnvRestart, +}); diff --git a/devtools/client/performance-new/store/selectors.js b/devtools/client/performance-new/store/selectors.js new file mode 100644 index 0000000000..d9332ccff3 --- /dev/null +++ b/devtools/client/performance-new/store/selectors.js @@ -0,0 +1,179 @@ +/* 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/. */ +// @ts-check +"use strict"; + +/** + * @typedef {import("../@types/perf").RecordingState} RecordingState + * @typedef {import("../@types/perf").RecordingStateFromPreferences} RecordingStateFromPreferences + * @typedef {import("../@types/perf").InitializedValues} InitializedValues + * @typedef {import("../@types/perf").PerfFront} PerfFront + * @typedef {import("../@types/perf").ReceiveProfile} ReceiveProfile + * @typedef {import("../@types/perf").SetRecordingPreferences} SetRecordingPreferences + * @typedef {import("../@types/perf").GetSymbolTableCallback} GetSymbolTableCallback + * @typedef {import("../@types/perf").RestartBrowserWithEnvironmentVariable} RestartBrowserWithEnvironmentVariable + * @typedef {import("../@types/perf").GetEnvironmentVariable} GetEnvironmentVariable + * @typedef {import("../@types/perf").PageContext} PageContext + * @typedef {import("../@types/perf").Presets} Presets + * @typedef {import("../@types/perf").ProfilerViewMode} ProfilerViewMode + */ +/** + * @template S + * @typedef {import("../@types/perf").Selector<S>} Selector<S> + */ + +/** @type {Selector<RecordingState>} */ +const getRecordingState = state => state.recordingState; + +/** @type {Selector<boolean>} */ +const getRecordingUnexpectedlyStopped = state => + state.recordingUnexpectedlyStopped; + +/** @type {Selector<boolean>} */ +const getIsSupportedPlatform = state => state.isSupportedPlatform; + +/** @type {Selector<number>} */ +const getInterval = state => state.interval; + +/** @type {Selector<number>} */ +const getEntries = state => state.entries; + +/** @type {Selector<string[]>} */ +const getFeatures = state => state.features; + +/** @type {Selector<string[]>} */ +const getThreads = state => state.threads; + +/** @type {Selector<string>} */ +const getThreadsString = state => getThreads(state).join(","); + +/** @type {Selector<string[]>} */ +const getObjdirs = state => state.objdirs; + +/** @type {Selector<Presets>} */ +const getPresets = state => getInitializedValues(state).presets; + +/** @type {Selector<string>} */ +const getPresetName = state => state.presetName; + +/** @type {Selector<ProfilerViewMode | undefined>} */ +const getProfilerViewMode = state => state.profilerViewMode; + +/** + * When remote profiling, there will be a back button to the settings. + * + * @type {Selector<(() => void) | undefined>} + */ +const getOpenRemoteDevTools = state => + getInitializedValues(state).openRemoteDevTools; + +/** + * Get the functon to open about:profiling. This assumes that the function exists, + * otherwise it will throw an error. + * + * @type {Selector<() => void>} + */ +const getOpenAboutProfiling = state => { + const { openAboutProfiling } = getInitializedValues(state); + if (!openAboutProfiling) { + throw new Error("Expected to get an openAboutProfiling function."); + } + return openAboutProfiling; +}; + +/** + * Warning! This function returns a new object on every run, and so should not + * be used directly as a React prop. + * + * @type {Selector<RecordingStateFromPreferences>} + */ +const getRecordingSettings = state => { + const presets = getPresets(state); + const presetName = getPresetName(state); + const preset = presets[presetName]; + if (preset) { + // Use the the settings from the preset. + return { + presetName: presetName, + entries: preset.entries, + interval: preset.interval, + features: preset.features, + threads: preset.threads, + objdirs: getObjdirs(state), + // The client doesn't implement durations yet. See Bug 1587165. + duration: preset.duration, + }; + } + + // Use the the custom settings from the panel. + return { + presetName: "custom", + entries: getEntries(state), + interval: getInterval(state), + features: getFeatures(state), + threads: getThreads(state), + objdirs: getObjdirs(state), + // The client doesn't implement durations yet. See Bug 1587165. + duration: 0, + }; +}; + +/** @type {Selector<InitializedValues>} */ +const getInitializedValues = state => { + const values = state.initializedValues; + if (!values) { + throw new Error("The store must be initialized before it can be used."); + } + return values; +}; + +/** @type {Selector<PerfFront>} */ +const getPerfFront = state => getInitializedValues(state).perfFront; + +/** @type {Selector<ReceiveProfile>} */ +const getReceiveProfileFn = state => getInitializedValues(state).receiveProfile; + +/** @type {Selector<SetRecordingPreferences>} */ +const getSetRecordingPreferencesFn = state => + getInitializedValues(state).setRecordingPreferences; + +/** @type {Selector<PageContext>} */ +const getPageContext = state => getInitializedValues(state).pageContext; + +/** @type {Selector<(profile: MinimallyTypedGeckoProfile) => GetSymbolTableCallback>} */ +const getSymbolTableGetter = state => + getInitializedValues(state).getSymbolTableGetter; + +/** @type {Selector<string[]>} */ +const getSupportedFeatures = state => + getInitializedValues(state).supportedFeatures; + +/** @type {Selector<string | null>} */ +const getPromptEnvRestart = state => state.promptEnvRestart; + +module.exports = { + getRecordingState, + getRecordingUnexpectedlyStopped, + getIsSupportedPlatform, + getInterval, + getEntries, + getFeatures, + getThreads, + getThreadsString, + getObjdirs, + getPresets, + getPresetName, + getProfilerViewMode, + getOpenRemoteDevTools, + getOpenAboutProfiling, + getRecordingSettings, + getInitializedValues, + getPerfFront, + getReceiveProfileFn, + getSetRecordingPreferencesFn, + getPageContext, + getSymbolTableGetter, + getPromptEnvRestart, + getSupportedFeatures, +}; |