tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

reducers.js (9750B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 // @ts-check
      5 "use strict";
      6 
      7 /**
      8 * @typedef {import("../@types/perf").Action} Action
      9 * @typedef {import("../@types/perf").State} State
     10 * @typedef {import("../@types/perf").RecordingState} RecordingState
     11 * @typedef {import("../@types/perf").InitializedValues} InitializedValues
     12 * @typedef {import("../@types/perf").RecordingSettings} RecordingSettings
     13 */
     14 
     15 /**
     16 * @template S
     17 * @typedef {import("../@types/perf").Reducer<S>} Reducer<S>
     18 */
     19 
     20 /**
     21 * The current state of the recording.
     22 *
     23 * @type {Reducer<RecordingState>}
     24 */
     25 // eslint-disable-next-line complexity
     26 function recordingState(state = "not-yet-known", action) {
     27  switch (action.type) {
     28    case "REPORT_PROFILER_READY": {
     29      // It's theoretically possible we got an event that already let us know about
     30      // the current state of the profiler.
     31      if (state !== "not-yet-known") {
     32        return state;
     33      }
     34 
     35      const { isActive } = action;
     36      if (isActive) {
     37        return "recording";
     38      }
     39      return "available-to-record";
     40    }
     41 
     42    case "REPORT_PROFILER_STARTED":
     43      switch (state) {
     44        case "not-yet-known":
     45        // We couldn't have started it yet, so it must have been someone
     46        // else. (fallthrough)
     47        case "available-to-record":
     48        // We aren't recording, someone else started it up. (fallthrough)
     49        case "request-to-stop-profiler":
     50        // We requested to stop the profiler, but someone else already started
     51        // it up. (fallthrough)
     52        case "request-to-get-profile-and-stop-profiler":
     53          return "recording";
     54 
     55        case "request-to-start-recording":
     56          // Wait for the profiler to tell us that it has started.
     57          return "recording";
     58 
     59        case "recording":
     60          // These state cases don't make sense to happen, and means we have a logical
     61          // fallacy somewhere.
     62          throw new Error(
     63            "The profiler started recording, when it shouldn't have " +
     64              `been able to. Current state: "${state}"`
     65          );
     66        default:
     67          throw new Error("Unhandled recording state");
     68      }
     69 
     70    case "REPORT_PROFILER_STOPPED":
     71      switch (state) {
     72        case "not-yet-known":
     73        case "request-to-get-profile-and-stop-profiler":
     74        case "request-to-stop-profiler":
     75          return "available-to-record";
     76 
     77        case "request-to-start-recording":
     78          // Highly unlikely, but someone stopped the recorder, this is fine.
     79          // Do nothing.
     80          return state;
     81 
     82        case "recording":
     83          return "available-to-record";
     84 
     85        case "available-to-record":
     86          throw new Error(
     87            "The profiler stopped recording, when it shouldn't have been able to."
     88          );
     89        default:
     90          throw new Error("Unhandled recording state");
     91      }
     92 
     93    case "REQUESTING_TO_START_RECORDING":
     94      return "request-to-start-recording";
     95 
     96    case "REQUESTING_TO_STOP_RECORDING":
     97      return "request-to-stop-profiler";
     98 
     99    case "REQUESTING_PROFILE":
    100      return "request-to-get-profile-and-stop-profiler";
    101 
    102    case "OBTAINED_PROFILE":
    103      return "available-to-record";
    104 
    105    default:
    106      return state;
    107  }
    108 }
    109 
    110 /**
    111 * Whether or not the recording state unexpectedly stopped. This allows
    112 * the UI to display a helpful message.
    113 *
    114 * @param {RecordingState | undefined} recState
    115 * @param {boolean} state
    116 * @param {Action} action
    117 * @returns {boolean}
    118 */
    119 function recordingUnexpectedlyStopped(recState, state = false, action) {
    120  switch (action.type) {
    121    case "REPORT_PROFILER_STOPPED":
    122      if (
    123        recState === "recording" ||
    124        recState == "request-to-start-recording"
    125      ) {
    126        return true;
    127      }
    128      return state;
    129    case "REPORT_PROFILER_STARTED":
    130      return false;
    131    default:
    132      return state;
    133  }
    134 }
    135 
    136 /**
    137 * The profiler needs to be queried asynchronously on whether or not
    138 * it supports the user's platform.
    139 *
    140 * @type {Reducer<boolean | null>}
    141 */
    142 function isSupportedPlatform(state = null, action) {
    143  switch (action.type) {
    144    case "INITIALIZE_STORE":
    145      return action.isSupportedPlatform;
    146    default:
    147      return state;
    148  }
    149 }
    150 
    151 /**
    152 * This object represents the default recording settings. They should be
    153 * overriden by whatever is read from the Firefox preferences at load time.
    154 *
    155 * @type {RecordingSettings}
    156 */
    157 const DEFAULT_RECORDING_SETTINGS = {
    158  // The preset name.
    159  presetName: "",
    160  // The setting for the recording interval. Defaults to 1ms.
    161  interval: 1,
    162  // The number of entries in the profiler's circular buffer.
    163  entries: 0,
    164  // The features that are enabled for the profiler.
    165  features: [],
    166  // The thread list
    167  threads: [],
    168  // The objdirs list
    169  objdirs: [],
    170  // The client doesn't implement durations yet. See Bug 1587165.
    171  duration: 0,
    172 };
    173 
    174 /**
    175 * This small utility returns true if the parameters contain the same values.
    176 * This is essentially a deepEqual operation specific to this structure.
    177 *
    178 * @param {RecordingSettings} a
    179 * @param {RecordingSettings} b
    180 * @return {boolean}
    181 */
    182 function areSettingsEquals(a, b) {
    183  if (a === b) {
    184    return true;
    185  }
    186 
    187  /* Simple properties */
    188  /* These types look redundant, but they actually help TypeScript assess that
    189   * the following code is correct, as well as prevent typos. */
    190  /** @type {Array<"presetName" | "interval" | "entries" | "duration">} */
    191  const simpleProperties = ["presetName", "interval", "entries", "duration"];
    192 
    193  /* arrays */
    194  /** @type {Array<"features" | "threads" | "objdirs">} */
    195  const arrayProperties = ["features", "threads", "objdirs"];
    196 
    197  for (const property of simpleProperties) {
    198    if (a[property] !== b[property]) {
    199      return false;
    200    }
    201  }
    202 
    203  for (const property of arrayProperties) {
    204    if (a[property].length !== b[property].length) {
    205      return false;
    206    }
    207 
    208    const arrayA = a[property].slice().sort();
    209    const arrayB = b[property].slice().sort();
    210    if (arrayA.some((valueA, i) => valueA !== arrayB[i])) {
    211      return false;
    212    }
    213  }
    214 
    215  return true;
    216 }
    217 
    218 /**
    219 * This handles all values used as recording settings.
    220 *
    221 * @type {Reducer<RecordingSettings>}
    222 */
    223 function recordingSettings(state = DEFAULT_RECORDING_SETTINGS, action) {
    224  /**
    225   * @template {keyof RecordingSettings} K
    226   * @param {K} settingName
    227   * @param {RecordingSettings[K]} settingValue
    228   * @return {RecordingSettings}
    229   */
    230  function changeOneSetting(settingName, settingValue) {
    231    if (state[settingName] === settingValue) {
    232      // Do not change the state if the new value equals the old value.
    233      return state;
    234    }
    235 
    236    return {
    237      ...state,
    238      [settingName]: settingValue,
    239      presetName: "custom",
    240    };
    241  }
    242 
    243  switch (action.type) {
    244    case "CHANGE_INTERVAL":
    245      return changeOneSetting("interval", action.interval);
    246    case "CHANGE_ENTRIES":
    247      return changeOneSetting("entries", action.entries);
    248    case "CHANGE_FEATURES":
    249      return changeOneSetting("features", action.features);
    250    case "CHANGE_THREADS":
    251      return changeOneSetting("threads", action.threads);
    252    case "CHANGE_OBJDIRS":
    253      return changeOneSetting("objdirs", action.objdirs);
    254    case "CHANGE_PRESET":
    255      return action.preset
    256        ? {
    257            ...state,
    258            presetName: action.presetName,
    259            interval: action.preset.interval,
    260            entries: action.preset.entries,
    261            features: action.preset.features,
    262            threads: action.preset.threads,
    263            // The client doesn't implement durations yet. See Bug 1587165.
    264            duration: action.preset.duration,
    265          }
    266        : {
    267            ...state,
    268            presetName: action.presetName, // it's probably "custom".
    269          };
    270    case "UPDATE_SETTINGS_FROM_PREFERENCES":
    271      if (areSettingsEquals(state, action.recordingSettingsFromPreferences)) {
    272        return state;
    273      }
    274      return { ...action.recordingSettingsFromPreferences };
    275    default:
    276      return state;
    277  }
    278 }
    279 
    280 /**
    281 * These are all the values used to initialize the profiler. They should never
    282 * change once added to the store.
    283 *
    284 * @type {Reducer<InitializedValues | null>}
    285 */
    286 function initializedValues(state = null, action) {
    287  switch (action.type) {
    288    case "INITIALIZE_STORE":
    289      return {
    290        presets: action.presets,
    291        pageContext: action.pageContext,
    292        supportedFeatures: action.supportedFeatures,
    293        openRemoteDevTools: action.openRemoteDevTools,
    294      };
    295    default:
    296      return state;
    297  }
    298 }
    299 
    300 /**
    301 * Some features may need a browser restart with an environment flag. Request
    302 * one here.
    303 *
    304 * @type {Reducer<string | null>}
    305 */
    306 function promptEnvRestart(state = null, action) {
    307  switch (action.type) {
    308    case "CHANGE_FEATURES":
    309      return action.promptEnvRestart;
    310    default:
    311      return state;
    312  }
    313 }
    314 
    315 /**
    316 * The main reducer for the performance-new client.
    317 *
    318 * @type {Reducer<State>}
    319 */
    320 module.exports = (state = undefined, action) => {
    321  return {
    322    recordingState: recordingState(state?.recordingState, action),
    323 
    324    // Treat this one specially - it also gets the recordingState.
    325    recordingUnexpectedlyStopped: recordingUnexpectedlyStopped(
    326      state?.recordingState,
    327      state?.recordingUnexpectedlyStopped,
    328      action
    329    ),
    330 
    331    isSupportedPlatform: isSupportedPlatform(
    332      state?.isSupportedPlatform,
    333      action
    334    ),
    335    recordingSettings: recordingSettings(state?.recordingSettings, action),
    336    initializedValues: initializedValues(state?.initializedValues, action),
    337    promptEnvRestart: promptEnvRestart(state?.promptEnvRestart, action),
    338  };
    339 };