tor-browser

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

background.sys.mjs (20099B)


      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 
      6 /**
      7 * This file contains all of the background logic for controlling the state and
      8 * configuration of the profiler. It is in a JSM so that the logic can be shared
      9 * with both the popup client, and the keyboard shortcuts. The shortcuts don't need
     10 * access to any UI, and need to be loaded independent of the popup.
     11 */
     12 
     13 // The following are not lazily loaded as they are needed during initialization.
     14 
     15 import { createLazyLoaders } from "resource://devtools/client/performance-new/shared/typescript-lazy-load.sys.mjs";
     16 
     17 /**
     18 * @typedef {import("../@types/perf").PerformancePref} PerformancePref
     19 * @typedef {import("../@types/perf").ProfilerWebChannel} ProfilerWebChannel
     20 * @typedef {import("../@types/perf").PageContext} PageContext
     21 * @typedef {import("../@types/perf").MessageFromFrontend} MessageFromFrontend
     22 * @typedef {import("../@types/perf").RequestFromFrontend} RequestFromFrontend
     23 * @typedef {import("../@types/perf").ResponseToFrontend} ResponseToFrontend
     24 * @typedef {import("../@types/perf").SymbolicationService} SymbolicationService
     25 * @typedef {import("../@types/perf").ProfilerBrowserInfo} ProfilerBrowserInfo
     26 * @typedef {import("../@types/perf").ProfileCaptureResult} ProfileCaptureResult
     27 * @typedef {import("../@types/perf").ProfilerFaviconData} ProfilerFaviconData
     28 * @typedef {import("../@types/perf").JSSources} JSSources
     29 */
     30 
     31 /** @type {PerformancePref["PopupFeatureFlag"]} */
     32 const POPUP_FEATURE_FLAG_PREF = "devtools.performance.popup.feature-flag";
     33 
     34 // The version of the profiler WebChannel.
     35 // This is reported from the STATUS_QUERY message, and identifies the
     36 // capabilities of the WebChannel. The front-end can handle old WebChannel
     37 // versions and has a full list of versions and capabilities here:
     38 // https://github.com/firefox-devtools/profiler/blob/main/src/app-logic/web-channel.js
     39 const CURRENT_WEBCHANNEL_VERSION = 6;
     40 
     41 const lazyRequire = {};
     42 // eslint-disable-next-line mozilla/lazy-getter-object-name
     43 ChromeUtils.defineESModuleGetters(lazyRequire, {
     44  require: "resource://devtools/shared/loader/Loader.sys.mjs",
     45 });
     46 // Lazily load the require function, when it's needed.
     47 // Avoid using ChromeUtils.defineESModuleGetters for now as:
     48 // * we can't replace createLazyLoaders as we still load commonjs+jsm+esm
     49 //   It will be easier once we only load sys.mjs files.
     50 // * we would need to find a way to accomodate typescript to this special function.
     51 // @ts-ignore:next-line
     52 function require(path) {
     53  // @ts-ignore:next-line
     54  return lazyRequire.require(path);
     55 }
     56 
     57 // The following utilities are lazily loaded as they are not needed when controlling the
     58 // global state of the profiler, and only are used during specific funcationality like
     59 // symbolication or capturing a profile.
     60 const lazy = createLazyLoaders({
     61  BrowserModule: () =>
     62    require("resource://devtools/client/performance-new/shared/browser.js"),
     63  Errors: () =>
     64    ChromeUtils.importESModule(
     65      "resource://devtools/shared/performance-new/errors.sys.mjs"
     66    ),
     67  PrefsPresets: () =>
     68    ChromeUtils.importESModule(
     69      "resource://devtools/shared/performance-new/prefs-presets.sys.mjs"
     70    ),
     71  RecordingUtils: () =>
     72    ChromeUtils.importESModule(
     73      "resource://devtools/shared/performance-new/recording-utils.sys.mjs"
     74    ),
     75  CustomizableUI: () =>
     76    ChromeUtils.importESModule(
     77      "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs"
     78    ),
     79  PerfSymbolication: () =>
     80    ChromeUtils.importESModule(
     81      "resource://devtools/shared/performance-new/symbolication.sys.mjs"
     82    ),
     83  ProfilerMenuButton: () =>
     84    ChromeUtils.importESModule(
     85      "resource://devtools/client/performance-new/popup/menu-button.sys.mjs"
     86    ),
     87  PlacesUtils: () =>
     88    ChromeUtils.importESModule("resource://gre/modules/PlacesUtils.sys.mjs")
     89      .PlacesUtils,
     90 });
     91 
     92 /** @type {{[key:string]: number} | null} */
     93 let gPreviousMozLogValues = null;
     94 
     95 /**
     96 * This function is called when the profile is captured with the shortcut keys,
     97 * with the profiler toolbarbutton, with the button inside the popup, or with
     98 * the about:logging page.
     99 *
    100 * @param {PageContext} pageContext
    101 * @return {Promise<void>}
    102 */
    103 export async function captureProfile(pageContext) {
    104  if (!Services.profiler.IsActive()) {
    105    // The profiler is not active, ignore.
    106    return;
    107  }
    108  if (Services.profiler.IsPaused()) {
    109    // The profiler is already paused for capture, ignore.
    110    return;
    111  }
    112 
    113  const { profileCaptureResult, additionalInformation } = await lazy
    114    .RecordingUtils()
    115    .getProfileDataAsGzippedArrayBufferThenStop();
    116  cleanupMozLogs();
    117  const profilerViewMode = lazy
    118    .PrefsPresets()
    119    .getProfilerViewModeForCurrentPreset(pageContext);
    120  const sharedLibraries = additionalInformation?.sharedLibraries
    121    ? additionalInformation.sharedLibraries
    122    : Services.profiler.sharedLibraries;
    123  const objdirs = lazy.PrefsPresets().getObjdirPrefValue();
    124 
    125  const { createLocalSymbolicationService } = lazy.PerfSymbolication();
    126  const symbolicationService = createLocalSymbolicationService(
    127    sharedLibraries,
    128    objdirs
    129  );
    130 
    131  const { openProfilerTab } = lazy.BrowserModule();
    132  const browser = await openProfilerTab({ profilerViewMode });
    133  registerProfileCaptureForBrowser(
    134    browser,
    135    profileCaptureResult,
    136    symbolicationService,
    137    additionalInformation?.jsSources ?? null
    138  );
    139 }
    140 
    141 /**
    142 * This function is called when the profiler is started with the shortcut
    143 * keys, with the profiler toolbarbutton, or with the button inside the
    144 * popup.
    145 *
    146 * @param {PageContext} pageContext
    147 */
    148 export function startProfiler(pageContext) {
    149  const { entries, interval, features, threads, mozLogs, duration } = lazy
    150    .PrefsPresets()
    151    .getRecordingSettings(pageContext, Services.profiler.GetFeatures());
    152 
    153  // Get the active Browser ID from browser.
    154  const { getActiveBrowserID } = lazy.RecordingUtils();
    155  const activeTabID = getActiveBrowserID();
    156 
    157  if (typeof mozLogs == "string") {
    158    updateMozLogs(mozLogs);
    159  }
    160 
    161  Services.profiler.StartProfiler(
    162    entries,
    163    interval,
    164    features,
    165    threads,
    166    activeTabID,
    167    duration
    168  );
    169 }
    170 
    171 /**
    172 * Given a MOZ_LOG string, toggles the expected preferences to enable the
    173 * LogModules mentioned in the string at the expected level of logging.
    174 * This will also record preference values in order to reset them on stop.
    175 * `mozLogs` is a string similar to the one passed as MOZ_LOG env variable.
    176 *
    177 * @param {string} mozLogs
    178 */
    179 function updateMozLogs(mozLogs) {
    180  gPreviousMozLogValues = {};
    181  for (const module of mozLogs.split(",")) {
    182    const lastColon = module.lastIndexOf(":");
    183    const logName = module.slice(0, lastColon).trim();
    184    const value = parseInt(module.slice(lastColon + 1).trim(), 10);
    185    const prefName = `logging.${logName}`;
    186    gPreviousMozLogValues[prefName] = Services.prefs.getIntPref(
    187      prefName,
    188      undefined
    189    );
    190    // MOZ_LOG aren't profiler specific and enabled globally in Firefox.
    191    // Preferences are the easiest (only?) way to toggle them from JavaScript.
    192    Services.prefs.setIntPref(prefName, value);
    193  }
    194 }
    195 
    196 /**
    197 * This function is called directly by devtools/startup/DevToolsStartup.jsm when
    198 * using the shortcut keys to capture a profile.
    199 *
    200 * @type {() => void}
    201 */
    202 export function stopProfiler() {
    203  Services.profiler.StopProfiler();
    204 
    205  cleanupMozLogs();
    206 }
    207 
    208 /**
    209 * This function should be called when we are done profiler in order to reset
    210 * the MOZ_LOG enabled while profiling.
    211 *
    212 * @type {() => void}
    213 */
    214 export function cleanupMozLogs() {
    215  if (gPreviousMozLogValues) {
    216    for (const [prefName, value] of Object.entries(gPreviousMozLogValues)) {
    217      if (typeof value == "number") {
    218        Services.prefs.setIntPref(prefName, value);
    219      } else {
    220        Services.prefs.clearUserPref(prefName);
    221      }
    222    }
    223    gPreviousMozLogValues = null;
    224  }
    225 }
    226 
    227 /**
    228 * This function is called directly by devtools/startup/DevToolsStartup.jsm when
    229 * using the shortcut keys to start and stop the profiler.
    230 *
    231 * @param {PageContext} pageContext
    232 * @return {void}
    233 */
    234 export function toggleProfiler(pageContext) {
    235  if (Services.profiler.IsPaused()) {
    236    // The profiler is currently paused, which means that the user is already
    237    // attempting to capture a profile. Ignore this request.
    238    return;
    239  }
    240  if (Services.profiler.IsActive()) {
    241    stopProfiler();
    242  } else {
    243    startProfiler(pageContext);
    244  }
    245 }
    246 
    247 /**
    248 * @param {PageContext} pageContext
    249 */
    250 export function restartProfiler(pageContext) {
    251  stopProfiler();
    252  startProfiler(pageContext);
    253 }
    254 
    255 /**
    256 * This map stores information that is associated with a "profile capturing"
    257 * action, so that we can look up this information for WebChannel messages
    258 * from the profiler tab.
    259 * Most importantly, this stores the captured profile. When the profiler tab
    260 * requests the profile, we can respond to the message with the correct profile.
    261 * This works even if the request happens long after the tab opened. It also
    262 * works for an "old" tab even if new profiles have been captured since that
    263 * tab was opened.
    264 * Supporting tab refresh is important because the tab sometimes reloads itself:
    265 * If an old version of the front-end is cached in the service worker, and the
    266 * browser supplies a profile with a newer format version, then the front-end
    267 * updates its service worker and reloads itself, so that the updated version
    268 * can parse the profile.
    269 *
    270 * This is a WeakMap so that the profile can be garbage-collected when the tab
    271 * is closed.
    272 *
    273 * @type {WeakMap<MockedExports.Browser, ProfilerBrowserInfo>}
    274 */
    275 const infoForBrowserMap = new WeakMap();
    276 
    277 /**
    278 * This handler computes the response for any messages coming
    279 * from the WebChannel from profiler.firefox.com.
    280 *
    281 * @param {RequestFromFrontend} request
    282 * @param {MockedExports.Browser} browser - The tab's browser.
    283 * @return {Promise<ResponseToFrontend>}
    284 */
    285 async function getResponseForMessage(request, browser) {
    286  switch (request.type) {
    287    case "STATUS_QUERY": {
    288      // The content page wants to know if this channel exists. It does, so respond
    289      // back to the ping.
    290      const { ProfilerMenuButton } = lazy.ProfilerMenuButton();
    291      return {
    292        version: CURRENT_WEBCHANNEL_VERSION,
    293        menuButtonIsEnabled: ProfilerMenuButton.isInNavbar(),
    294      };
    295    }
    296    case "ENABLE_MENU_BUTTON": {
    297      const { ownerDocument } = browser;
    298      if (!ownerDocument) {
    299        throw new Error(
    300          "Could not find the owner document for the current browser while enabling " +
    301            "the profiler menu button"
    302        );
    303      }
    304      // Ensure the widget is enabled.
    305      Services.prefs.setBoolPref(POPUP_FEATURE_FLAG_PREF, true);
    306 
    307      // Force the preset to be "firefox-platform" if we enable the menu button
    308      // via web channel. If user goes through profiler.firefox.com to enable
    309      // it, it means that either user is a platform developer or filing a bug
    310      // report for performance engineers to look at.
    311      const supportedFeatures = Services.profiler.GetFeatures();
    312      lazy
    313        .PrefsPresets()
    314        .changePreset("aboutprofiling", "firefox-platform", supportedFeatures);
    315 
    316      // Enable the profiler menu button.
    317      const { ProfilerMenuButton } = lazy.ProfilerMenuButton();
    318      ProfilerMenuButton.addToNavbar();
    319 
    320      // Dispatch the change event manually, so that the shortcuts will also be
    321      // added.
    322      const { CustomizableUI } = lazy.CustomizableUI();
    323      CustomizableUI.dispatchToolboxEvent("customizationchange");
    324 
    325      // Open the popup with a message.
    326      ProfilerMenuButton.openPopup(ownerDocument);
    327 
    328      // There is no response data for this message.
    329      return undefined;
    330    }
    331    case "GET_PROFILE": {
    332      const infoForBrowser = infoForBrowserMap.get(browser);
    333      if (infoForBrowser === undefined) {
    334        throw new Error("Could not find a profile for this tab.");
    335      }
    336      const { profileCaptureResult } = infoForBrowser;
    337      switch (profileCaptureResult.type) {
    338        case "SUCCESS":
    339          return profileCaptureResult.profile;
    340        case "ERROR":
    341          throw profileCaptureResult.error;
    342        default: {
    343          const { UnhandledCaseError } = lazy.Errors();
    344          throw new UnhandledCaseError(
    345            profileCaptureResult,
    346            "profileCaptureResult"
    347          );
    348        }
    349      }
    350    }
    351    case "GET_SYMBOL_TABLE": {
    352      const { debugName, breakpadId } = request;
    353      const symbolicationService = getSymbolicationServiceForBrowser(browser);
    354      if (!symbolicationService) {
    355        throw new Error("No symbolication service has been found for this tab");
    356      }
    357      return symbolicationService.getSymbolTable(debugName, breakpadId);
    358    }
    359    case "QUERY_SYMBOLICATION_API": {
    360      const { path, requestJson } = request;
    361      const symbolicationService = getSymbolicationServiceForBrowser(browser);
    362      if (!symbolicationService) {
    363        throw new Error("No symbolication service has been found for this tab");
    364      }
    365      return symbolicationService.querySymbolicationApi(path, requestJson);
    366    }
    367    case "GET_EXTERNAL_POWER_TRACKS": {
    368      const { startTime, endTime } = request;
    369      const externalPowerUrl = Services.prefs.getCharPref(
    370        "devtools.performance.recording.power.external-url",
    371        ""
    372      );
    373      if (externalPowerUrl) {
    374        const response = await fetch(
    375          `${externalPowerUrl}?start=${startTime}&end=${endTime}`
    376        );
    377        return response.json();
    378      }
    379      return [];
    380    }
    381    case "GET_EXTERNAL_MARKERS": {
    382      const { startTime, endTime } = request;
    383      const externalMarkersUrl = Services.prefs.getCharPref(
    384        "devtools.performance.recording.markers.external-url",
    385        ""
    386      );
    387      if (externalMarkersUrl) {
    388        const response = await fetch(
    389          `${externalMarkersUrl}?start=${startTime}&end=${endTime}`
    390        );
    391        return response.json();
    392      }
    393      return [];
    394    }
    395    case "GET_PAGE_FAVICONS": {
    396      const { pageUrls } = request;
    397      return getPageFavicons(pageUrls);
    398    }
    399    case "OPEN_SCRIPT_IN_DEBUGGER": {
    400      // This webchannel message type is added with version 5.
    401      const { tabId, scriptUrl, line, column } = request;
    402      const { openScriptInDebugger } = lazy.BrowserModule();
    403      return openScriptInDebugger(tabId, scriptUrl, line, column);
    404    }
    405    case "GET_JS_SOURCES": {
    406      const { sourceUuids } = request;
    407      if (!Array.isArray(sourceUuids)) {
    408        throw new Error("sourceUuids must be an array");
    409      }
    410 
    411      const infoForBrowser = infoForBrowserMap.get(browser);
    412      if (infoForBrowser === undefined) {
    413        throw new Error("No JS source data found for this tab");
    414      }
    415 
    416      const jsSources = infoForBrowser.jsSources;
    417      if (jsSources === null) {
    418        return sourceUuids.map(() => ({
    419          error: "Source not found in the browser",
    420        }));
    421      }
    422 
    423      return sourceUuids.map(uuid => {
    424        const sourceText = jsSources[uuid];
    425        if (!sourceText) {
    426          return { error: "Source not found in the browser" };
    427        }
    428 
    429        return { sourceText };
    430      });
    431    }
    432    default: {
    433      console.error(
    434        "An unknown message type was received by the profiler's WebChannel handler.",
    435        request
    436      );
    437      const { UnhandledCaseError } = lazy.Errors();
    438      throw new UnhandledCaseError(request, "WebChannel request");
    439    }
    440  }
    441 }
    442 
    443 /**
    444 * Get the symbolicationService for the capture that opened this browser's
    445 * tab, or a fallback service for browsers from tabs opened by the user.
    446 *
    447 * @param {MockedExports.Browser} browser
    448 * @return {SymbolicationService | null}
    449 */
    450 function getSymbolicationServiceForBrowser(browser) {
    451  // We try to serve symbolication requests that come from tabs that we
    452  // opened when a profile was captured, and for tabs that the user opened
    453  // independently, for example because the user wants to load an existing
    454  // profile from a file.
    455  const infoForBrowser = infoForBrowserMap.get(browser);
    456  if (infoForBrowser !== undefined) {
    457    // We opened this tab when a profile was captured. Use the symbolication
    458    // service for that capture.
    459    return infoForBrowser.symbolicationService;
    460  }
    461 
    462  // For the "foreign" tabs, we provide a fallback symbolication service so that
    463  // we can find symbols for any libraries that are loaded in this process. This
    464  // means that symbolication will work if the existing file has been captured
    465  // from the same build.
    466  const { createLocalSymbolicationService } = lazy.PerfSymbolication();
    467  return createLocalSymbolicationService(
    468    Services.profiler.sharedLibraries,
    469    lazy.PrefsPresets().getObjdirPrefValue()
    470  );
    471 }
    472 
    473 /**
    474 * This handler handles any messages coming from the WebChannel from profiler.firefox.com.
    475 *
    476 * @param {ProfilerWebChannel} channel
    477 * @param {string} id
    478 * @param {any} message
    479 * @param {MockedExports.WebChannelTarget} target
    480 */
    481 export async function handleWebChannelMessage(channel, id, message, target) {
    482  if (typeof message !== "object" || typeof message.type !== "string") {
    483    console.error(
    484      "An malformed message was received by the profiler's WebChannel handler.",
    485      message
    486    );
    487    return;
    488  }
    489  const messageFromFrontend = /** @type {MessageFromFrontend} */ (message);
    490  const { requestId } = messageFromFrontend;
    491 
    492  try {
    493    const response = await getResponseForMessage(
    494      messageFromFrontend,
    495      target.browser
    496    );
    497    channel.send(
    498      {
    499        type: "SUCCESS_RESPONSE",
    500        requestId,
    501        response,
    502      },
    503      target
    504    );
    505  } catch (error) {
    506    let errorMessage;
    507    if (error instanceof Error) {
    508      errorMessage = `${error.name}: ${error.message}`;
    509    } else {
    510      errorMessage = `${error}`;
    511    }
    512    channel.send(
    513      {
    514        type: "ERROR_RESPONSE",
    515        requestId,
    516        error: errorMessage,
    517      },
    518      target
    519    );
    520  }
    521 }
    522 
    523 /**
    524 * @param {MockedExports.Browser} browser - The tab's browser.
    525 * @param {ProfileCaptureResult} profileCaptureResult - The Gecko profile.
    526 * @param {SymbolicationService | null} symbolicationService - An object which implements the
    527 *   SymbolicationService interface, whose getSymbolTable method will be invoked
    528 *   when profiler.firefox.com sends GET_SYMBOL_TABLE WebChannel messages to us. This
    529 *   method should obtain a symbol table for the requested binary and resolve the
    530 *   returned promise with it.
    531 * @param {JSSources | null} jsSources - JS sources from the profile collection.
    532 */
    533 export function registerProfileCaptureForBrowser(
    534  browser,
    535  profileCaptureResult,
    536  symbolicationService,
    537  jsSources
    538 ) {
    539  infoForBrowserMap.set(browser, {
    540    profileCaptureResult,
    541    symbolicationService,
    542    jsSources,
    543  });
    544 }
    545 
    546 /**
    547 * Get page favicons data and return them.
    548 *
    549 * @param {Array<string>} pageUrls
    550 *
    551 * @returns {Promise<Array<ProfilerFaviconData | null>>} favicon data as binary array.
    552 */
    553 async function getPageFavicons(pageUrls) {
    554  if (!pageUrls || pageUrls.length === 0) {
    555    // Return early if the pages are not provided.
    556    return [];
    557  }
    558 
    559  // Get the data of favicons and return them.
    560  const { favicons, toURI } = lazy.PlacesUtils();
    561 
    562  const promises = pageUrls.map(pageUrl =>
    563    favicons
    564      .getFaviconForPage(toURI(pageUrl), /* preferredWidth = */ 32)
    565      .then(favicon => {
    566        // Check if data is found in the database and return it if so.
    567        if (favicon.rawData.length) {
    568          return {
    569            // PlacesUtils returns a number array for the data. Converting it to
    570            // the Uint8Array here to send it to the tab more efficiently.
    571            data: new Uint8Array(favicon.rawData).buffer,
    572            mimeType: favicon.mimeType,
    573          };
    574        }
    575 
    576        return null;
    577      })
    578      .catch(() => {
    579        // Couldn't find a favicon for this page, return null explicitly.
    580        return null;
    581      })
    582  );
    583 
    584  return Promise.all(promises);
    585 }