Bug 1607801 - Create a TypeScript friendly lazy loading mechanism; r=ochameau
authorGreg Tatum <gtatum@mozilla.com>
Tue, 28 Apr 2020 17:51:09 +0000
changeset 526538 de5aad1dc90e32d9483d5324fd808bfa92086410
parent 526537 8dfe8cb5a8311dc6062c8d9a421dbbf65bc5050e
child 526539 050c45257373eb0ea2a18cfbfb3a17a5c821f62f
push id37358
push useropoprus@mozilla.com
push dateWed, 29 Apr 2020 03:05:14 +0000
treeherdermozilla-central@6bb8423186c1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersochameau
bugs1607801
milestone77.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1607801 - Create a TypeScript friendly lazy loading mechanism; r=ochameau This patch changes the lazy loading mechanism to explicitly create a "lazy" object that can be used to load modules. It makes it so that TypeScript can understand what is going on with the lazy loading. I couldn't find a solution to make the Object.define mechanism work for the global object. I briefly considered using the Object.define() method on the returned "lazy" object, as this could be typed correctly, but I felt magically accessing properties was less clear compared to calling a function that has the side effect of maybe loading a module for the first time. Differential Revision: https://phabricator.services.mozilla.com/D59208
devtools/client/performance-new/@types/gecko.d.ts
devtools/client/performance-new/browser.js
devtools/client/performance-new/moz.build
devtools/client/performance-new/popup/background.jsm.js
devtools/client/performance-new/popup/menu-button.jsm.js
devtools/client/performance-new/popup/panel.jsm.js
devtools/client/performance-new/typescript-lazy-load.jsm.js
--- a/devtools/client/performance-new/@types/gecko.d.ts
+++ b/devtools/client/performance-new/@types/gecko.d.ts
@@ -40,16 +40,18 @@ declare namespace MockedExports {
       typeof import("resource:///modules/CustomizableWidgets.jsm");
     "resource://devtools/shared/Loader.jsm":
       typeof import("resource://devtools/shared/Loader.jsm");
     "resource://devtools/client/performance-new/popup/background.jsm.js":
       typeof import("resource://devtools/client/performance-new/popup/background.jsm.js");
     "resource://devtools/client/shared/browser-loader.js": any;
     "resource://devtools/client/performance-new/popup/menu-button.jsm.js":
       typeof import("devtools/client/performance-new/popup/menu-button.jsm.js");
+    "resource://devtools/client/performance-new/typescript-lazy-load.jsm.js":
+      typeof import("devtools/client/performance-new/typescript-lazy-load.jsm.js");
     "resource://devtools/client/performance-new/popup/panel.jsm.js":
       typeof import("devtools/client/performance-new/popup/panel.jsm.js");
     "resource:///modules/PanelMultiView.jsm":
       typeof import("resource:///modules/PanelMultiView.jsm");
   }
 
   interface ChromeUtils {
     /**
@@ -60,16 +62,17 @@ declare namespace MockedExports {
      *  of type
      *
      * Then add the file path to the KnownModules above.
      */
     import: <S extends keyof KnownModules>(module: S) => KnownModules[S];
     createObjectIn: (content: ContentWindow) => object;
     exportFunction: (fn: Function, scope: object, options?: object) => void;
     cloneInto: (value: any, scope: object, options?: object) => void;
+    defineModuleGetter: (target: any, variable: string, path: string) => void;
   }
 
   interface MessageManager {
     loadFrameScript(url: string, flag: boolean): void;
     sendAsyncMessage: (event: string, data: any) => void;
     addMessageListener: (event: string, listener: (event: any) => void) => void;
   }
 
@@ -278,16 +281,20 @@ declare module "resource://gre/modules/S
 declare module "Services" {
   export = MockedExports.Services;
 }
 
 declare module "chrome" {
   export = MockedExports.chrome;
 }
 
+declare module "ChromeUtils" {
+  export = ChromeUtils;
+}
+
 declare module "resource://gre/modules/osfile.jsm" {
   export = MockedExports.osfileJSM;
 }
 
 declare module "resource://gre/modules/AppConstants.jsm" {
   export = MockedExports.AppConstantsJSM;
 }
 
--- a/devtools/client/performance-new/browser.js
+++ b/devtools/client/performance-new/browser.js
@@ -15,47 +15,28 @@
  * @typedef {import("./@types/perf").PerformancePref} PerformancePref
  * @typedef {import("./@types/perf").RecordingStateFromPreferences} RecordingStateFromPreferences
  * @typedef {import("./@types/perf").RestartBrowserWithEnvironmentVariable} RestartBrowserWithEnvironmentVariable
  * @typedef {import("./@types/perf").GetEnvironmentVariable} GetEnvironmentVariable
  * @typedef {import("./@types/perf").GetActiveBrowsingContextID} GetActiveBrowsingContextID
  * @typedef {import("./@types/perf").MinimallyTypedGeckoProfile} MinimallyTypedGeckoProfile
  */
 
-/**
- * TS-TODO
- *
- * This function replaces lazyRequireGetter, and TypeScript can understand it. It's
- * currently duplicated until we have consensus that TypeScript is a good idea.
- *
- * @template T
- * @type {(callback: () => T) => () => T}
- */
-function requireLazy(callback) {
-  /** @type {T | undefined} */
-  let cache;
-  return () => {
-    if (cache === undefined) {
-      cache = callback();
-    }
-    return cache;
-  };
-}
-
-const lazyServices = requireLazy(() =>
-  require("resource://gre/modules/Services.jsm")
+const ChromeUtils = require("ChromeUtils");
+const { createLazyLoaders } = ChromeUtils.import(
+  "resource://devtools/client/performance-new/typescript-lazy-load.jsm.js"
 );
 
-const lazyChrome = requireLazy(() => require("chrome"));
-
-const lazyOS = requireLazy(() => require("resource://gre/modules/osfile.jsm"));
-
-const lazyProfilerGetSymbols = requireLazy(() =>
-  require("resource://gre/modules/ProfilerGetSymbols.jsm")
-);
+const lazy = createLazyLoaders({
+  Chrome: () => require("chrome"),
+  Services: () => require("Services"),
+  OS: () => ChromeUtils.import("resource://gre/modules/osfile.jsm"),
+  ProfilerGetSymbols: () =>
+    ChromeUtils.import("resource://gre/modules/ProfilerGetSymbols.jsm"),
+});
 
 const TRANSFER_EVENT = "devtools:perf-html-transfer-profile";
 const SYMBOL_TABLE_REQUEST_EVENT = "devtools:perf-html-request-symbol-table";
 const SYMBOL_TABLE_RESPONSE_EVENT = "devtools:perf-html-reply-symbol-table";
 
 /** @type {PerformancePref["UIBaseUrl"]} */
 const UI_BASE_URL_PREF = "devtools.performance.recording.ui-base-url";
 /** @type {PerformancePref["UIBaseUrlPathPref"]} */
@@ -79,17 +60,17 @@ const UI_BASE_URL_PATH_DEFAULT = "/from-
  * @param {MinimallyTypedGeckoProfile} profile - The Gecko profile.
  * @param {GetSymbolTableCallback} getSymbolTableCallback - A callback function with the signature
  *   (debugName, breakpadId) => Promise<SymbolTableAsTuple>, which will be invoked
  *   when profiler.firefox.com sends SYMBOL_TABLE_REQUEST_EVENT messages to us. This
  *   function should obtain a symbol table for the requested binary and resolve the
  *   returned promise with it.
  */
 function receiveProfile(profile, getSymbolTableCallback) {
-  const { Services } = lazyServices();
+  const Services = lazy.Services();
   // Find the most recently used window, as the DevTools client could be in a variety
   // of hosts.
   const win = Services.wm.getMostRecentWindow("navigator:browser");
   if (!win) {
     throw new Error("No browser window");
   }
   const browser = win.gBrowser;
   win.focus();
@@ -216,17 +197,17 @@ async function getSymbolTableFromDebugge
   ];
 }
 
 /**
  * @param {string} path
  * @returns {Promise<boolean>}
  */
 async function doesFileExistAtPath(path) {
-  const { OS } = lazyOS();
+  const { OS } = lazy.OS();
   try {
     const result = await OS.File.stat(path);
     return !result.isDir;
   } catch (e) {
     if (e instanceof OS.File.Error && e.becauseNoSuchFile) {
       return false;
     }
     throw e;
@@ -249,31 +230,31 @@ async function doesFileExistAtPath(path)
  *   that should be searched for relevant build artifacts.
  * @param {string} filename The file name of the binary.
  * @param {string} breakpadId The breakpad ID of the binary.
  * @returns {Promise<SymbolTableAsTuple>} The symbol table of the first encountered binary with a
  *   matching breakpad ID, in SymbolTableAsTuple format. An exception is thrown (the
  *   promise is rejected) if nothing was found.
  */
 async function getSymbolTableFromLocalBinary(objdirs, filename, breakpadId) {
-  const { OS } = lazyOS();
+  const { OS } = lazy.OS();
   const candidatePaths = [];
   for (const objdirPath of objdirs) {
     // Binaries are usually expected to exist at objdir/dist/bin/filename.
     candidatePaths.push(OS.Path.join(objdirPath, "dist", "bin", filename));
     // Also search in the "objdir" directory itself (not just in dist/bin).
     // If, for some unforeseen reason, the relevant binary is not inside the
     // objdirs dist/bin/ directory, this provides a way out because it lets the
     // user specify the actual location.
     candidatePaths.push(OS.Path.join(objdirPath, filename));
   }
 
   for (const path of candidatePaths) {
     if (await doesFileExistAtPath(path)) {
-      const { ProfilerGetSymbols } = lazyProfilerGetSymbols();
+      const { ProfilerGetSymbols } = lazy.ProfilerGetSymbols();
       try {
         return await ProfilerGetSymbols.getSymbolTable(path, path, breakpadId);
       } catch (e) {
         // ProfilerGetSymbols.getSymbolTable was unsuccessful. So either the
         // file wasn't parseable or its contents didn't match the specified
         // breakpadId, or some other error occurred.
         // Advance to the next candidate path.
       }
@@ -307,17 +288,17 @@ function createMultiModalGetSymbolTableF
     const result = libraryGetter(debugName, breakpadId);
     if (!result) {
       throw new Error(
         `Could not find the library for "${debugName}", "${breakpadId}".`
       );
     }
     const { name, path, debugPath } = result;
     if (await doesFileExistAtPath(path)) {
-      const { ProfilerGetSymbols } = lazyProfilerGetSymbols();
+      const { ProfilerGetSymbols } = lazy.ProfilerGetSymbols();
       // This profile was obtained from this machine, and not from a
       // different device (e.g. an Android phone). Dump symbols from the file
       // on this machine directly.
       return ProfilerGetSymbols.getSymbolTable(path, debugPath, breakpadId);
     }
     // The file does not exist, which probably indicates that the profile was
     // obtained on a different machine, i.e. the debuggee is truly remote
     // (e.g. on an Android phone).
@@ -350,48 +331,48 @@ function createMultiModalGetSymbolTableF
 }
 
 /**
  * Restarts the browser with a given environment variable set to a value.
  *
  * @type {RestartBrowserWithEnvironmentVariable}
  */
 function restartBrowserWithEnvironmentVariable(envName, value) {
-  const { Services } = lazyServices();
-  const { Cc, Ci } = lazyChrome();
+  const Services = lazy.Services();
+  const { Cc, Ci } = lazy.Chrome();
   const env = Cc["@mozilla.org/process/environment;1"].getService(
     Ci.nsIEnvironment
   );
   env.set(envName, value);
 
   Services.startup.quit(
     Services.startup.eForceQuit | Services.startup.eRestart
   );
 }
 
 /**
  * Gets an environment variable from the browser.
  *
  * @type {GetEnvironmentVariable}
  */
 function getEnvironmentVariable(envName) {
-  const { Cc, Ci } = lazyChrome();
+  const { Cc, Ci } = lazy.Chrome();
   const env = Cc["@mozilla.org/process/environment;1"].getService(
     Ci.nsIEnvironment
   );
   return env.get(envName);
 }
 
 /**
  * @param {Window} window
  * @param {string[]} objdirs
  * @param {(objdirs: string[]) => unknown} changeObjdirs
  */
 function openFilePickerForObjdir(window, objdirs, changeObjdirs) {
-  const { Cc, Ci } = lazyChrome();
+  const { Cc, Ci } = lazy.Chrome();
   const FilePicker = Cc["@mozilla.org/filepicker;1"].createInstance(
     Ci.nsIFilePicker
   );
   FilePicker.init(window, "Pick build directory", FilePicker.modeGetFolder);
   FilePicker.open(rv => {
     if (rv == FilePicker.returnOK) {
       const path = FilePicker.file.path;
       if (path && !objdirs.includes(path)) {
--- a/devtools/client/performance-new/moz.build
+++ b/devtools/client/performance-new/moz.build
@@ -10,16 +10,17 @@ DIRS += [
     'popup',
 ]
 
 DevToolsModules(
     'browser.js',
     'initializer.js',
     'panel.js',
     'preference-management.js',
+    'typescript-lazy-load.jsm.js',
     'utils.js',
 )
 
 BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
 XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell/xpcshell.ini']
 
 with Files('**'):
     BUG_COMPONENT = ('DevTools', 'Performance Tools (Profiler/Timeline)')
--- a/devtools/client/performance-new/popup/background.jsm.js
+++ b/devtools/client/performance-new/popup/background.jsm.js
@@ -9,16 +9,19 @@
  * configuration of the profiler. It is in a JSM so that the logic can be shared
  * with both the popup client, and the keyboard shortcuts. The shortcuts don't need
  * access to any UI, and need to be loaded independent of the popup.
  */
 
 // The following are not lazily loaded as they are needed during initialization.
 
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { createLazyLoaders } = ChromeUtils.import(
+  "resource://devtools/client/performance-new/typescript-lazy-load.jsm.js"
+);
 // For some reason TypeScript was giving me an error when de-structuring AppConstants. I
 // suspect a bug in TypeScript was at play.
 const AppConstants = ChromeUtils.import(
   "resource://gre/modules/AppConstants.jsm"
 ).AppConstants;
 
 /**
  * @typedef {import("../@types/perf").RecordingStateFromPreferences} RecordingStateFromPreferences
@@ -43,92 +46,44 @@ const THREADS_PREF = "devtools.performan
 const OBJDIRS_PREF = "devtools.performance.recording.objdirs";
 /** @type {PerformancePref["Duration"]} */
 const DURATION_PREF = "devtools.performance.recording.duration";
 /** @type {PerformancePref["Preset"]} */
 const PRESET_PREF = "devtools.performance.recording.preset";
 /** @type {PerformancePref["PopupFeatureFlag"]} */
 const POPUP_FEATURE_FLAG_PREF = "devtools.performance.popup.feature-flag";
 
+// Lazily load the require function, when it's needed.
+ChromeUtils.defineModuleGetter(
+  this,
+  "require",
+  "resource://devtools/shared/Loader.jsm"
+);
+
 // The following utilities are lazily loaded as they are not needed when controlling the
 // global state of the profiler, and only are used during specific funcationality like
 // symbolication or capturing a profile.
-
-/**
- * TS-TODO
- *
- * This function replaces lazyRequireGetter, and TypeScript can understand it. It's
- * currently duplicated until we have consensus that TypeScript is a good idea.
- *
- * @template T
- * @type {(callback: () => T) => () => T}
- */
-function requireLazy(callback) {
-  /** @type {T | undefined} */
-  let cache;
-  return () => {
-    if (cache === undefined) {
-      cache = callback();
-    }
-    return cache;
-  };
-}
-
-const lazyOS = requireLazy(() =>
-  ChromeUtils.import("resource://gre/modules/osfile.jsm")
-);
-
-const lazyProfilerGetSymbols = requireLazy(() =>
-  ChromeUtils.import("resource://gre/modules/ProfilerGetSymbols.jsm")
-);
-
-const lazyBrowserModule = requireLazy(() => {
-  const { require } = ChromeUtils.import(
-    "resource://devtools/shared/Loader.jsm"
-  );
-  const browserModule = require("devtools/client/performance-new/browser");
-  return browserModule;
+const lazy = createLazyLoaders({
+  OS: () => ChromeUtils.import("resource://gre/modules/osfile.jsm"),
+  Utils: () => require("devtools/client/performance-new/utils"),
+  BrowserModule: () => require("devtools/client/performance-new/browser"),
+  RecordingUtils: () =>
+    require("devtools/shared/performance-new/recording-utils"),
+  CustomizableUI: () =>
+    ChromeUtils.import("resource:///modules/CustomizableUI.jsm"),
+  PreferenceManagement: () =>
+    require("devtools/client/performance-new/preference-management"),
+  ProfilerGetSymbols: () =>
+    ChromeUtils.import("resource://gre/modules/ProfilerGetSymbols.jsm"),
+  ProfilerMenuButton: () =>
+    ChromeUtils.import(
+      "resource://devtools/client/performance-new/popup/menu-button.jsm.js"
+    ),
 });
 
-const lazyPreferenceManagement = requireLazy(() => {
-  const { require } = ChromeUtils.import(
-    "resource://devtools/shared/Loader.jsm"
-  );
-
-  const preferenceManagementModule = require("devtools/client/performance-new/preference-management");
-  return preferenceManagementModule;
-});
-
-const lazyRecordingUtils = requireLazy(() => {
-  const { require } = ChromeUtils.import(
-    "resource://devtools/shared/Loader.jsm"
-  );
-
-  const recordingUtils = require("devtools/shared/performance-new/recording-utils");
-  return recordingUtils;
-});
-
-const lazyUtils = requireLazy(() => {
-  const { require } = ChromeUtils.import(
-    "resource://devtools/shared/Loader.jsm"
-  );
-  const recordingUtils = require("devtools/client/performance-new/utils");
-  return recordingUtils;
-});
-
-const lazyProfilerMenuButton = requireLazy(() =>
-  ChromeUtils.import(
-    "resource://devtools/client/performance-new/popup/menu-button.jsm.js"
-  )
-);
-
-const lazyCustomizableUI = requireLazy(() =>
-  ChromeUtils.import("resource:///modules/CustomizableUI.jsm")
-);
-
 /** @type {Presets} */
 const presets = {
   "web-developer": {
     label: "Web Developer",
     description:
       "Recommended preset for most web app debugging, with low overhead.",
     entries: 10000000,
     interval: 1,
@@ -205,26 +160,26 @@ async function getSymbolsFromThisBrowser
         "and symbols for it can not be obtained. This usually happens if a content " +
         "process uses a library that's not used in the parent process - " +
         "Services.profiler.sharedLibraries only knows about libraries in the " +
         "parent process."
     );
   }
 
   const { path, debugPath } = cachedLibInfo;
-  const { OS } = lazyOS();
+  const { OS } = lazy.OS();
   if (!OS.Path.split(path).absolute) {
     throw new Error(
       "Services.profiler.sharedLibraries did not contain an absolute path for " +
         `the library ${debugName} ${breakpadId}, so symbols for this library can not ` +
         "be obtained."
     );
   }
 
-  const { ProfilerGetSymbols } = lazyProfilerGetSymbols();
+  const { ProfilerGetSymbols } = lazy.ProfilerGetSymbols();
 
   return ProfilerGetSymbols.getSymbolTable(path, debugPath, breakpadId);
 }
 
 /**
  * This function is called directly by devtools/startup/DevToolsStartup.jsm when
  * using the shortcut keys to capture a profile.
  * @type {() => Promise<void>}
@@ -242,41 +197,41 @@ async function captureProfile() {
     .getProfileDataAsGzippedArrayBuffer()
     .catch(
       /** @type {(e: any) => {}} */ e => {
         console.error(e);
         return {};
       }
     );
 
-  const receiveProfile = lazyBrowserModule().receiveProfile;
+  const receiveProfile = lazy.BrowserModule().receiveProfile;
   receiveProfile(profile, getSymbolsFromThisBrowser);
 
   Services.profiler.StopProfiler();
 }
 
 /**
  * This function is only called by devtools/startup/DevToolsStartup.jsm when
  * starting the profiler using the shortcut keys, through toggleProfiler below.
  * @param {PageContext} pageContext
  */
 function startProfiler(pageContext) {
-  const { translatePreferencesToState } = lazyPreferenceManagement();
+  const { translatePreferencesToState } = lazy.PreferenceManagement();
   const {
     entries,
     interval,
     features,
     threads,
     duration,
   } = translatePreferencesToState(
     getRecordingPreferences(pageContext, Services.profiler.GetFeatures())
   );
 
   // Get the active BrowsingContext ID from browser.
-  const { getActiveBrowsingContextID } = lazyRecordingUtils();
+  const { getActiveBrowsingContextID } = lazy.RecordingUtils();
   const activeBrowsingContextID = getActiveBrowsingContextID();
 
   Services.profiler.StartProfiler(
     entries,
     interval,
     features,
     threads,
     activeBrowsingContextID,
@@ -347,17 +302,17 @@ function getPrefPostfix(pageContext) {
     case "devtools":
     case "aboutprofiling":
       // Don't use any postfix on the prefs.
       return "";
     case "devtools-remote":
     case "aboutprofiling-remote":
       return ".remote";
     default: {
-      const { UnhandledCaseError } = lazyUtils();
+      const { UnhandledCaseError } = lazy.Utils();
       throw new UnhandledCaseError(pageContext, "Page Context");
     }
   }
 }
 
 /**
  * @param {PageContext} pageContext
  * @param {string[]} supportedFeatures
@@ -521,17 +476,17 @@ function handleWebChannelMessage(channel
     return;
   }
   const messageFromFrontend = /** @type {MessageFromFrontend} */ (message);
   const { requestId } = messageFromFrontend;
   switch (messageFromFrontend.type) {
     case "STATUS_QUERY": {
       // The content page wants to know if this channel exists. It does, so respond
       // back to the ping.
-      const { ProfilerMenuButton } = lazyProfilerMenuButton();
+      const { ProfilerMenuButton } = lazy.ProfilerMenuButton();
       channel.send(
         {
           type: "STATUS_RESPONSE",
           menuButtonIsEnabled: ProfilerMenuButton.isInNavbar(),
           requestId,
         },
         target
       );
@@ -544,22 +499,22 @@ function handleWebChannelMessage(channel
           "Could not find the owner document for the current browser while enabling " +
             "the profiler menu button"
         );
       }
       // Ensure the widget is enabled.
       Services.prefs.setBoolPref(POPUP_FEATURE_FLAG_PREF, true);
 
       // Enable the profiler menu button.
-      const { ProfilerMenuButton } = lazyProfilerMenuButton();
+      const { ProfilerMenuButton } = lazy.ProfilerMenuButton();
       ProfilerMenuButton.addToNavbar(ownerDocument);
 
       // Dispatch the change event manually, so that the shortcuts will also be
       // added.
-      const { CustomizableUI } = lazyCustomizableUI();
+      const { CustomizableUI } = lazy.CustomizableUI();
       CustomizableUI.dispatchToolboxEvent("customizationchange");
 
       // Open the popup with a message.
       ProfilerMenuButton.openPopup(ownerDocument);
 
       // Respond back that we've done it.
       channel.send(
         {
--- a/devtools/client/performance-new/popup/menu-button.jsm.js
+++ b/devtools/client/performance-new/popup/menu-button.jsm.js
@@ -1,94 +1,71 @@
 /* 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").PerformancePref} PerformancePref
- */
-
-/**
  * This file controls the enabling and disabling of the menu button for the profiler.
  * Care should be taken to keep it minimal as it can be run with browser initialization.
  */
 
-/**
- * TS-TODO
- *
- * This function replaces lazyRequireGetter, and TypeScript can understand it. It's
- * currently duplicated until we have consensus that TypeScript is a good idea.
- *
- * @template T
- * @type {(callback: () => T) => () => T}
- */
-function requireLazy(callback) {
-  /** @type {T | undefined} */
-  let cache;
-  return () => {
-    if (cache === undefined) {
-      cache = callback();
-    }
-    return cache;
-  };
-}
-
 // Provide an exports object for the JSM to be properly read by TypeScript.
 /** @type {any} */ (this).exports = {};
 
-const lazyServices = requireLazy(() =>
-  ChromeUtils.import("resource://gre/modules/Services.jsm")
-);
-const lazyCustomizableUI = requireLazy(() =>
-  ChromeUtils.import("resource:///modules/CustomizableUI.jsm")
+const { createLazyLoaders } = ChromeUtils.import(
+  "resource://devtools/client/performance-new/typescript-lazy-load.jsm.js"
 );
-const lazyCustomizableWidgets = requireLazy(() =>
-  ChromeUtils.import("resource:///modules/CustomizableWidgets.jsm")
-);
-const lazyPopupPanel = requireLazy(() =>
-  ChromeUtils.import(
-    "resource://devtools/client/performance-new/popup/panel.jsm.js"
-  )
-);
+
+const lazy = createLazyLoaders({
+  Services: () => ChromeUtils.import("resource://gre/modules/Services.jsm"),
+  CustomizableUI: () =>
+    ChromeUtils.import("resource:///modules/CustomizableUI.jsm"),
+  CustomizableWidgets: () =>
+    ChromeUtils.import("resource:///modules/CustomizableWidgets.jsm"),
+  PopupPanel: () =>
+    ChromeUtils.import(
+      "resource://devtools/client/performance-new/popup/panel.jsm.js"
+    ),
+});
 
 const WIDGET_ID = "profiler-button";
 
 /**
  * Add the profiler button to the navbar.
  *
  * @param {ChromeDocument} document  The browser's document.
  * @return {void}
  */
 function addToNavbar(document) {
-  const { CustomizableUI } = lazyCustomizableUI();
+  const { CustomizableUI } = lazy.CustomizableUI();
 
   CustomizableUI.addWidgetToArea(WIDGET_ID, CustomizableUI.AREA_NAVBAR);
 }
 
 /**
  * Remove the widget and place it in the customization palette. This will also
  * disable the shortcuts.
  *
  * @return {void}
  */
 function remove() {
-  const { CustomizableUI } = lazyCustomizableUI();
+  const { CustomizableUI } = lazy.CustomizableUI();
   CustomizableUI.removeWidgetFromArea(WIDGET_ID);
 }
 
 /**
  * See if the profiler menu button is in the navbar, or other active areas. The
  * placement is null when it's inactive in the customization palette.
  *
  * @return {boolean}
  */
 function isInNavbar() {
-  const { CustomizableUI } = lazyCustomizableUI();
+  const { CustomizableUI } = lazy.CustomizableUI();
   return Boolean(CustomizableUI.getPlacementOfWidget("profiler-button"));
 }
 
 /**
  * Opens the popup for the profiler.
  * @param {Document} document
  */
 function openPopup(document) {
@@ -103,19 +80,19 @@ function openPopup(document) {
 
 /**
  * This function creates the widget definition for the CustomizableUI. It should
  * only be run if the profiler button is enabled.
  * @param {(isEnabled: boolean) => void} toggleProfilerKeyShortcuts
  * @return {void}
  */
 function initialize(toggleProfilerKeyShortcuts) {
-  const { CustomizableUI } = lazyCustomizableUI();
-  const { CustomizableWidgets } = lazyCustomizableWidgets();
-  const { Services } = lazyServices();
+  const { CustomizableUI } = lazy.CustomizableUI();
+  const { CustomizableWidgets } = lazy.CustomizableWidgets();
+  const { Services } = lazy.Services();
 
   const widget = CustomizableUI.getWidget(WIDGET_ID);
   if (widget && widget.provider == CustomizableUI.PROVIDER_API) {
     // This widget has already been created.
     return;
   }
 
   const viewId = "PanelUI-profiler";
@@ -138,17 +115,17 @@ function initialize(toggleProfilerKeySho
    */
   function handleCustomizationChange() {
     const isEnabled = isInNavbar();
     toggleProfilerKeyShortcuts(isEnabled);
 
     if (!isEnabled) {
       // The profiler menu button is no longer in the navbar, make sure that the
       // "intro-displayed" preference is reset.
-      /** @type {PerformancePref["PopupIntroDisplayed"]} */
+      /** @type {import("../@types/perf").PerformancePref["PopupIntroDisplayed"]} */
       const popupIntroDisplayedPref =
         "devtools.performance.popup.intro-displayed";
       Services.prefs.setBoolPref(popupIntroDisplayedPref, false);
 
       if (Services.profiler.IsActive()) {
         Services.profiler.StopProfiler();
       }
     }
@@ -174,17 +151,17 @@ function initialize(toggleProfilerKeySho
           // The popup logic is stored in a separate script so it doesn't have
           // to be parsed at browser startup, and will only be lazily loaded
           // when the popup is viewed.
           const {
             selectElementsInPanelview,
             createViewControllers,
             addPopupEventHandlers,
             initializePopup,
-          } = lazyPopupPanel();
+          } = lazy.PopupPanel();
 
           const panelElements = selectElementsInPanelview(event.target);
           const panelView = createViewControllers(panelState, panelElements);
           addPopupEventHandlers(panelState, panelElements, panelView);
           initializePopup(panelState, panelElements, panelView);
         } catch (error) {
           // Surface any errors better in the console.
           console.error(error);
@@ -204,17 +181,17 @@ function initialize(toggleProfilerKeySho
 
     /**
      * Perform any general initialization for this widget. This is called once per
      * browser window.
      *
      * @type {(document: HTMLDocument) => void}
      */
     onBeforeCreated: document => {
-      /** @type {PerformancePref["PopupIntroDisplayed"]} */
+      /** @type {import("../@types/perf").PerformancePref["PopupIntroDisplayed"]} */
       const popupIntroDisplayedPref =
         "devtools.performance.popup.intro-displayed";
 
       // Determine the state of the popup's info being collapsed BEFORE the view
       // is shown, and update the collapsed state. This way the transition animation
       // isn't run.
       panelState.isInfoCollapsed = Services.prefs.getBoolPref(
         popupIntroDisplayedPref
--- a/devtools/client/performance-new/popup/panel.jsm.js
+++ b/devtools/client/performance-new/popup/panel.jsm.js
@@ -14,50 +14,29 @@
  */
 
 /**
  * @typedef {Object} State - The mutable state of the popup.
  * @property {Array<() => void>} cleanup - Functions to cleanup once the view is hidden.
  * @property {boolean} isInfoCollapsed
  */
 
-/**
- * TS-TODO
- *
- * This function replaces lazyRequireGetter, and TypeScript can understand it. It's
- * currently duplicated until we have consensus that TypeScript is a good idea.
- *
- * @template T
- * @type {(callback: () => T) => () => T}
- */
-function requireLazy(callback) {
-  /** @type {T | undefined} */
-  let cache;
-  return () => {
-    if (cache === undefined) {
-      cache = callback();
-    }
-    return cache;
-  };
-}
+const { createLazyLoaders } = ChromeUtils.import(
+  "resource://devtools/client/performance-new/typescript-lazy-load.jsm.js"
+);
 
-// Provide an exports object for the JSM to be properly read by TypeScript.
-/** @type {any} */ (this).module = {};
-
-const lazyServices = requireLazy(() =>
-  ChromeUtils.import("resource://gre/modules/Services.jsm")
-);
-const lazyPanelMultiView = requireLazy(() =>
-  ChromeUtils.import("resource:///modules/PanelMultiView.jsm")
-);
-const lazyBackground = requireLazy(() =>
-  ChromeUtils.import(
-    "resource://devtools/client/performance-new/popup/background.jsm.js"
-  )
-);
+const lazy = createLazyLoaders({
+  Services: () => ChromeUtils.import("resource://gre/modules/Services.jsm"),
+  PanelMultiView: () =>
+    ChromeUtils.import("resource:///modules/PanelMultiView.jsm"),
+  Background: () =>
+    ChromeUtils.import(
+      "resource://devtools/client/performance-new/popup/background.jsm.js"
+    ),
+});
 
 /**
  * This function collects all of the selection of the elements inside of the panel.
  *
  * @param {XULElement} panelview
  */
 function selectElementsInPanelview(panelview) {
   const document = panelview.ownerDocument;
@@ -122,39 +101,39 @@ function createViewControllers(state, el
         const { height } = info.getBoundingClientRect();
         info.style.marginBlockEnd = `-${height}px`;
       } else {
         info.style.marginBlockEnd = "0";
       }
     },
 
     updatePresets() {
-      const { Services } = lazyServices();
-      const { presets, getRecordingPreferences } = lazyBackground();
+      const { Services } = lazy.Services();
+      const { presets, getRecordingPreferences } = lazy.Background();
       const { presetName } = getRecordingPreferences(
         "aboutprofiling",
         Services.profiler.GetFeatures()
       );
       const preset = presets[presetName];
       if (preset) {
         elements.presetDescription.style.display = "block";
         elements.presetCustom.style.display = "none";
         elements.presetDescription.textContent = preset.description;
         elements.presetsMenuList.value = presetName;
       } else {
         elements.presetDescription.style.display = "none";
         elements.presetCustom.style.display = "block";
       }
-      const { PanelMultiView } = lazyPanelMultiView();
+      const { PanelMultiView } = lazy.PanelMultiView();
       // Update the description height sizing.
       PanelMultiView.forNode(elements.panelview).descriptionHeightWorkaround();
     },
 
     updateProfilerActive() {
-      const { Services } = lazyServices();
+      const { Services } = lazy.Services();
       const isProfilerActive = Services.profiler.IsActive();
       elements.inactive.setAttribute(
         "hidden",
         isProfilerActive ? "true" : "false"
       );
       elements.active.setAttribute(
         "hidden",
         isProfilerActive ? "false" : "true"
@@ -165,18 +144,18 @@ function createViewControllers(state, el
     createPresetsList() {
       // Check the DOM if the presets were built or not. We can't cache this value
       // in the `State` object, as the `State` object will be removed if the
       // button is removed from the toolbar, but the DOM changes will still persist.
       if (elements.menupopup.getAttribute("presetsbuilt") === "true") {
         // The presets were already built.
         return;
       }
-      const { Services } = lazyServices();
-      const { presets } = lazyBackground();
+      const { Services } = lazy.Services();
+      const { presets } = lazy.Background();
       const currentPreset = Services.prefs.getCharPref(
         "devtools.performance.recording.preset"
       );
 
       const menuitems = Object.entries(presets).map(([id, preset]) => {
         const menuitem = elements.document.createXULElement("menuitem");
         menuitem.setAttribute("label", preset.label);
         menuitem.setAttribute("value", id);
@@ -225,17 +204,17 @@ function initializePopup(state, elements
     // the size of the container. It needs to wait a second before the bounding box
     // returns an actual size.
     view.updateInfoCollapse();
     view.updateProfilerActive();
     view.updatePresets();
 
     // XUL <description> elements don't vertically size correctly, this is
     // the workaround for it.
-    const { PanelMultiView } = lazyPanelMultiView();
+    const { PanelMultiView } = lazy.PanelMultiView();
     PanelMultiView.forNode(elements.panelview).descriptionHeightWorkaround();
 
     // Now wait for another rAF, and turn the animations back on.
     elements.window.requestAnimationFrame(() => {
       elements.header.setAttribute("animationready", "true");
     });
   });
 }
@@ -250,17 +229,17 @@ function initializePopup(state, elements
  * @param {ViewController} view
  */
 function addPopupEventHandlers(state, elements, view) {
   const {
     changePreset,
     startProfiler,
     stopProfiler,
     captureProfile,
-  } = lazyBackground();
+  } = lazy.Background();
 
   /**
    * Adds a handler that automatically is removed once the panel is hidden.
    *
    * @param {HTMLElement} element
    * @param {string} type
    * @param {(event: Event) => void} handler
    */
@@ -321,25 +300,28 @@ function addPopupEventHandlers(state, el
   });
 
   addHandler(elements.presetsCustomButton, "click", () => {
     elements.window.openTrustedLinkIn("about:profiling", "tab");
     view.hidePopup();
   });
 
   // Update the view when the profiler starts/stops.
-  const { Services } = lazyServices();
+  const { Services } = lazy.Services();
   Services.obs.addObserver(view.updateProfilerActive, "profiler-started");
   Services.obs.addObserver(view.updateProfilerActive, "profiler-stopped");
   state.cleanup.push(() => {
     Services.obs.removeObserver(view.updateProfilerActive, "profiler-started");
     Services.obs.removeObserver(view.updateProfilerActive, "profiler-stopped");
   });
 }
 
+// Provide an exports object for the JSM to be properly read by TypeScript.
+/** @type {any} */ (this).module = {};
+
 module.exports = {
   selectElementsInPanelview,
   createViewControllers,
   addPopupEventHandlers,
   initializePopup,
 };
 
 // Object.keys() confuses the linting which expects a static array expression.
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/typescript-lazy-load.jsm.js
@@ -0,0 +1,54 @@
+/* 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";
+
+/**
+ * TypeScript can't understand the lazyRequireGetter mechanism, due to how it defines
+ * properties as a getter. This function, instead provides lazy loading in a
+ * TypeScript-friendly manner. It applies the lazy load memoization to each property
+ * of the provided object.
+ *
+ * Example usage:
+ *
+ * const lazy = createLazyLoaders({
+ *   moduleA: () => require("module/a"),
+ *   moduleB: () => require("module/b"),
+ * });
+ *
+ * Later:
+ *
+ * const moduleA = lazy.moduleA();
+ * const { objectInModuleB } = lazy.moduleB();
+ *
+ * @template T
+ * @param {T} definition - An object where each property has a function that loads a module.
+ * @returns {T} - The load memoized version of T.
+ */
+function createLazyLoaders(definition) {
+  /** @type {any} */
+  const result = {};
+  for (const [key, callback] of Object.entries(definition)) {
+    /** @type {any} */
+    let cache;
+    result[key] = () => {
+      if (cache === undefined) {
+        cache = callback();
+      }
+      return cache;
+    };
+  }
+  return result;
+}
+
+// Provide an exports object for the JSM to be properly read by TypeScript.
+/** @type {any} */ (this).module = {};
+
+module.exports = {
+  createLazyLoaders,
+};
+
+// Object.keys() confuses the linting which expects a static array expression.
+// eslint-disable-next-line
+var EXPORTED_SYMBOLS = Object.keys(module.exports);