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