Source code

Revision control

Copy as Markdown

Other Tools

/* 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),
};
};