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.js247
-rw-r--r--devtools/client/performance-new/store/moz.build10
-rw-r--r--devtools/client/performance-new/store/reducers.js230
-rw-r--r--devtools/client/performance-new/store/selectors.js179
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,
+};