diff options
Diffstat (limited to 'devtools/client/performance-new/store/reducers.js')
-rw-r--r-- | devtools/client/performance-new/store/reducers.js | 332 |
1 files changed, 332 insertions, 0 deletions
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), + }; +}; |