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 | 218 | ||||
-rw-r--r-- | devtools/client/performance-new/store/moz.build | 10 | ||||
-rw-r--r-- | devtools/client/performance-new/store/reducers.js | 332 | ||||
-rw-r--r-- | devtools/client/performance-new/store/selectors.js | 104 |
5 files changed, 667 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..2bb7ce126c --- /dev/null +++ b/devtools/client/performance-new/store/actions.js @@ -0,0 +1,218 @@ +/* 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("resource://devtools/client/performance-new/store/selectors.js"); + +/** + * @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").RecordingSettings} RecordingSettings + * @typedef {import("../@types/perf").Presets} Presets + * @typedef {import("../@types/perf").PanelWindow} PanelWindow + * @typedef {import("../@types/perf").MinimallyTypedGeckoProfile} MinimallyTypedGeckoProfile + */ + +/** + * @template T + * @typedef {import("../@types/perf").ThunkAction<T>} ThunkAction<T> + */ + +/** + * This is the result of the initial questions about the state of the profiler. + * + * @param {boolean} isActive + * @return {Action} + */ +exports.reportProfilerReady = isActive => ({ + type: "REPORT_PROFILER_READY", + isActive, +}); + +/** + * Dispatched when the profiler starting is observed. + * @return {Action} + */ +exports.reportProfilerStarted = () => ({ + type: "REPORT_PROFILER_STARTED", +}); + +/** + * Dispatched when the profiler stopping is observed. + * @return {Action} + */ +exports.reportProfilerStopped = () => ({ + type: "REPORT_PROFILER_STOPPED", +}); + +/** + * Updates the recording settings for the interval. + * @param {number} interval + * @return {Action} + */ +exports.changeInterval = interval => ({ + type: "CHANGE_INTERVAL", + interval, +}); + +/** + * Updates the recording settings for the entries. + * @param {number} entries + * @return {Action} + */ +exports.changeEntries = entries => ({ + 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 old popup supported restarting the browser, but + // this hasn't been updated yet for the about:profiling workflow, because + // jstracer is disabled for now. + if ( + !Services.env.get("JS_TRACE_LOGGING") && + features.includes("jstracer") + ) { + promptEnvRestart = "JS_TRACE_LOGGING"; + } + } + + dispatch({ + type: "CHANGE_FEATURES", + features, + promptEnvRestart, + }); + }; +}; + +/** + * Updates the recording settings for the threads. + * @param {string[]} threads + * @return {Action} + */ +exports.changeThreads = threads => ({ + type: "CHANGE_THREADS", + threads, +}); + +/** + * Change the preset. + * @param {Presets} presets + * @param {string} presetName + * @return {Action} + */ +exports.changePreset = (presets, presetName) => ({ + 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 {Action} + */ +exports.changeObjdirs = objdirs => ({ + 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 => { + return { + type: "INITIALIZE_STORE", + ...values, + }; +}; + +/** + * Whenever the preferences are updated, this action is dispatched to update the + * redux store. + * @param {RecordingSettings} recordingSettingsFromPreferences + * @return {Action} + */ +exports.updateSettingsFromPreferences = recordingSettingsFromPreferences => { + return { + type: "UPDATE_SETTINGS_FROM_PREFERENCES", + recordingSettingsFromPreferences, + }; +}; + +/** + * Start a new recording with the perfFront and update the internal recording state. + * @param {PerfFront} perfFront + * @return {ThunkAction<void>} + */ +exports.startRecording = perfFront => { + return ({ dispatch, getState }) => { + const recordingSettings = selectors.getRecordingSettings(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({ type: "REQUESTING_TO_START_RECORDING" }); + perfFront.startProfiler(recordingSettings); + }; +}; + +/** + * Stops the profiler, and opens the profile in a new window. + * @param {PerfFront} perfFront + * @return {ThunkAction<Promise<MinimallyTypedGeckoProfile>>} + */ +exports.getProfileAndStopProfiler = perfFront => { + return async ({ dispatch, getState }) => { + dispatch({ type: "REQUESTING_PROFILE" }); + const profile = await perfFront.getProfileAndStopProfiler(); + dispatch({ type: "OBTAINED_PROFILE" }); + return profile; + }; +}; + +/** + * Stops the profiler, but does not try to retrieve the profile. + * @param {PerfFront} perfFront + * @return {ThunkAction<void>} + */ +exports.stopProfilerAndDiscardProfile = perfFront => { + return async ({ dispatch, getState }) => { + dispatch({ type: "REQUESTING_TO_STOP_RECORDING" }); + + 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..3ace0f8d16 --- /dev/null +++ b/devtools/client/performance-new/store/reducers.js @@ -0,0 +1,332 @@ +/* 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").Action} Action + * @typedef {import("../@types/perf").State} State + * @typedef {import("../@types/perf").RecordingState} RecordingState + * @typedef {import("../@types/perf").InitializedValues} InitializedValues + * @typedef {import("../@types/perf").RecordingSettings} RecordingSettings + */ + +/** + * @template S + * @typedef {import("../@types/perf").Reducer<S>} Reducer<S> + */ + +/** + * The current state of the recording. + * @type {Reducer<RecordingState>} + */ +// eslint-disable-next-line complexity +function recordingState(state = "not-yet-known", action) { + switch (action.type) { + case "REPORT_PROFILER_READY": { + // It's theoretically possible we got an event that already let us know about + // the current state of the profiler. + if (state !== "not-yet-known") { + return state; + } + + const { isActive } = action; + if (isActive) { + return "recording"; + } + return "available-to-record"; + } + + case "REPORT_PROFILER_STARTED": + switch (state) { + case "not-yet-known": + // We couldn't have started it yet, so it must have been someone + // else. (fallthrough) + case "available-to-record": + // We aren't recording, someone else started it up. (fallthrough) + case "request-to-stop-profiler": + // We requested to stop the profiler, but someone else already started + // it up. (fallthrough) + case "request-to-get-profile-and-stop-profiler": + return "recording"; + + case "request-to-start-recording": + // Wait for the profiler to tell us that it has started. + return "recording"; + + case "recording": + // These state cases don't make sense to happen, and means we have a logical + // fallacy somewhere. + throw new Error( + "The profiler started recording, when it shouldn't have " + + `been able to. Current state: "${state}"` + ); + default: + throw new Error("Unhandled recording state"); + } + + case "REPORT_PROFILER_STOPPED": + switch (state) { + case "not-yet-known": + case "request-to-get-profile-and-stop-profiler": + case "request-to-stop-profiler": + return "available-to-record"; + + case "request-to-start-recording": + // Highly unlikely, but someone stopped the recorder, this is fine. + // Do nothing. + return state; + + case "recording": + return "available-to-record"; + + case "available-to-record": + throw new Error( + "The profiler stopped recording, when it shouldn't have been able to." + ); + default: + throw new Error("Unhandled recording state"); + } + + case "REQUESTING_TO_START_RECORDING": + return "request-to-start-recording"; + + case "REQUESTING_TO_STOP_RECORDING": + return "request-to-stop-profiler"; + + case "REQUESTING_PROFILE": + return "request-to-get-profile-and-stop-profiler"; + + case "OBTAINED_PROFILE": + return "available-to-record"; + + default: + return state; + } +} + +/** + * Whether or not the recording state unexpectedly stopped. This allows + * the UI to display a helpful message. + * @param {RecordingState | undefined} recState + * @param {boolean} state + * @param {Action} action + * @returns {boolean} + */ +function recordingUnexpectedlyStopped(recState, state = false, action) { + switch (action.type) { + case "REPORT_PROFILER_STOPPED": + if ( + recState === "recording" || + recState == "request-to-start-recording" + ) { + return true; + } + return state; + case "REPORT_PROFILER_STARTED": + return false; + 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 "INITIALIZE_STORE": + return action.isSupportedPlatform; + default: + return state; + } +} + +/** + * This object represents the default recording settings. They should be + * overriden by whatever is read from the Firefox preferences at load time. + * @type {RecordingSettings} + */ +const DEFAULT_RECORDING_SETTINGS = { + // The preset name. + presetName: "", + // The setting for the recording interval. Defaults to 1ms. + interval: 1, + // The number of entries in the profiler's circular buffer. + entries: 0, + // The features that are enabled for the profiler. + features: [], + // The thread list + threads: [], + // The objdirs list + objdirs: [], + // The client doesn't implement durations yet. See Bug 1587165. + duration: 0, +}; + +/** + * This small utility returns true if the parameters contain the same values. + * This is essentially a deepEqual operation specific to this structure. + * @param {RecordingSettings} a + * @param {RecordingSettings} b + * @return {boolean} + */ +function areSettingsEquals(a, b) { + if (a === b) { + return true; + } + + /* Simple properties */ + /* These types look redundant, but they actually help TypeScript assess that + * the following code is correct, as well as prevent typos. */ + /** @type {Array<"presetName" | "interval" | "entries" | "duration">} */ + const simpleProperties = ["presetName", "interval", "entries", "duration"]; + + /* arrays */ + /** @type {Array<"features" | "threads" | "objdirs">} */ + const arrayProperties = ["features", "threads", "objdirs"]; + + for (const property of simpleProperties) { + if (a[property] !== b[property]) { + return false; + } + } + + for (const property of arrayProperties) { + if (a[property].length !== b[property].length) { + return false; + } + + const arrayA = a[property].slice().sort(); + const arrayB = b[property].slice().sort(); + if (arrayA.some((valueA, i) => valueA !== arrayB[i])) { + return false; + } + } + + return true; +} + +/** + * This handles all values used as recording settings. + * @type {Reducer<RecordingSettings>} + */ +function recordingSettings(state = DEFAULT_RECORDING_SETTINGS, action) { + /** + * @template {keyof RecordingSettings} K + * @param {K} settingName + * @param {RecordingSettings[K]} settingValue + * @return {RecordingSettings} + */ + function changeOneSetting(settingName, settingValue) { + if (state[settingName] === settingValue) { + // Do not change the state if the new value equals the old value. + return state; + } + + return { + ...state, + [settingName]: settingValue, + presetName: "custom", + }; + } + + switch (action.type) { + case "CHANGE_INTERVAL": + return changeOneSetting("interval", action.interval); + case "CHANGE_ENTRIES": + return changeOneSetting("entries", action.entries); + case "CHANGE_FEATURES": + return changeOneSetting("features", action.features); + case "CHANGE_THREADS": + return changeOneSetting("threads", action.threads); + case "CHANGE_OBJDIRS": + return changeOneSetting("objdirs", action.objdirs); + case "CHANGE_PRESET": + return action.preset + ? { + ...state, + presetName: action.presetName, + interval: action.preset.interval, + entries: action.preset.entries, + features: action.preset.features, + threads: action.preset.threads, + // The client doesn't implement durations yet. See Bug 1587165. + duration: action.preset.duration, + } + : { + ...state, + presetName: action.presetName, // it's probably "custom". + }; + case "UPDATE_SETTINGS_FROM_PREFERENCES": + if (areSettingsEquals(state, action.recordingSettingsFromPreferences)) { + return state; + } + return { ...action.recordingSettingsFromPreferences }; + 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 { + presets: action.presets, + pageContext: action.pageContext, + supportedFeatures: action.supportedFeatures, + 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 = (state = undefined, action) => { + return { + recordingState: recordingState(state?.recordingState, action), + + // Treat this one specially - it also gets the recordingState. + recordingUnexpectedlyStopped: recordingUnexpectedlyStopped( + state?.recordingState, + state?.recordingUnexpectedlyStopped, + action + ), + + isSupportedPlatform: isSupportedPlatform( + state?.isSupportedPlatform, + action + ), + recordingSettings: recordingSettings(state?.recordingSettings, action), + initializedValues: initializedValues(state?.initializedValues, action), + promptEnvRestart: promptEnvRestart(state?.promptEnvRestart, action), + }; +}; diff --git a/devtools/client/performance-new/store/selectors.js b/devtools/client/performance-new/store/selectors.js new file mode 100644 index 0000000000..91f5bc9b65 --- /dev/null +++ b/devtools/client/performance-new/store/selectors.js @@ -0,0 +1,104 @@ +/* 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").RecordingSettings} RecordingSettings + * @typedef {import("../@types/perf").InitializedValues} InitializedValues + * @typedef {import("../@types/perf").PerfFront} PerfFront + * @typedef {import("../@types/perf").ReceiveProfile} ReceiveProfile + * @typedef {import("../@types/perf").RestartBrowserWithEnvironmentVariable} RestartBrowserWithEnvironmentVariable + * @typedef {import("../@types/perf").PageContext} PageContext + * @typedef {import("../@types/perf").Presets} Presets + */ +/** + * @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 | null>} */ +const getIsSupportedPlatform = state => state.isSupportedPlatform; + +/** @type {Selector<RecordingSettings>} */ +const getRecordingSettings = state => state.recordingSettings; + +/** @type {Selector<number>} */ +const getInterval = state => getRecordingSettings(state).interval; + +/** @type {Selector<number>} */ +const getEntries = state => getRecordingSettings(state).entries; + +/** @type {Selector<string[]>} */ +const getFeatures = state => getRecordingSettings(state).features; + +/** @type {Selector<string[]>} */ +const getThreads = state => getRecordingSettings(state).threads; + +/** @type {Selector<string>} */ +const getThreadsString = state => getThreads(state).join(","); + +/** @type {Selector<string[]>} */ +const getObjdirs = state => getRecordingSettings(state).objdirs; + +/** @type {Selector<Presets>} */ +const getPresets = state => getInitializedValues(state).presets; + +/** @type {Selector<string>} */ +const getPresetName = state => state.recordingSettings.presetName; + +/** + * When remote profiling, there will be a back button to the settings. + * + * @type {Selector<(() => void) | undefined>} + */ +const getOpenRemoteDevTools = state => + getInitializedValues(state).openRemoteDevTools; + +/** @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<PageContext>} */ +const getPageContext = state => getInitializedValues(state).pageContext; + +/** @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, + getOpenRemoteDevTools, + getRecordingSettings, + getInitializedValues, + getPageContext, + getPromptEnvRestart, + getSupportedFeatures, +}; |