332 lines
9.5 KiB
JavaScript
332 lines
9.5 KiB
JavaScript
/* 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),
|
|
};
|
|
};
|