summaryrefslogtreecommitdiffstats
path: root/devtools/client/performance-new/store/reducers.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/performance-new/store/reducers.js')
-rw-r--r--devtools/client/performance-new/store/reducers.js332
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),
+ };
+};