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
This commit is contained in:
Greg Tatum 2020-04-28 17:51:09 +00:00
Родитель 02e0ad2908
Коммит 46fc6e8cf6
7 изменённых файлов: 163 добавлений и 206 удалений

Просмотреть файл

@ -45,6 +45,8 @@ declare namespace MockedExports {
"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":
@ -65,6 +67,7 @@ declare namespace MockedExports {
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 {
@ -283,6 +286,10 @@ declare module "chrome" {
export = MockedExports.chrome;
}
declare module "ChromeUtils" {
export = ChromeUtils;
}
declare module "resource://gre/modules/osfile.jsm" {
export = MockedExports.osfileJSM;
}

Просмотреть файл

@ -20,37 +20,18 @@
* @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";
@ -84,7 +65,7 @@ const UI_BASE_URL_PATH_DEFAULT = "/from-addon";
* 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");
@ -221,7 +202,7 @@ async function getSymbolTableFromDebuggee(perfFront, path, breakpadId) {
* @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;
@ -254,7 +235,7 @@ async function doesFileExistAtPath(path) {
* 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.
@ -268,7 +249,7 @@ async function getSymbolTableFromLocalBinary(objdirs, filename, breakpadId) {
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) {
@ -312,7 +293,7 @@ function createMultiModalGetSymbolTableFn(profile, getObjdirs, perfFront) {
}
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.
@ -355,8 +336,8 @@ function createMultiModalGetSymbolTableFn(profile, getObjdirs, perfFront) {
* @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
);
@ -373,7 +354,7 @@ function restartBrowserWithEnvironmentVariable(envName, value) {
* @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
);
@ -386,7 +367,7 @@ function getEnvironmentVariable(envName) {
* @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
);

Просмотреть файл

@ -15,6 +15,7 @@ DevToolsModules(
'initializer.js',
'panel.js',
'preference-management.js',
'typescript-lazy-load.jsm.js',
'utils.js',
)

Просмотреть файл

@ -14,6 +14,9 @@
// 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(
@ -48,82 +51,34 @@ 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": {
@ -210,7 +165,7 @@ async function getSymbolsFromThisBrowser(debugName, breakpadId) {
}
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 " +
@ -219,7 +174,7 @@ async function getSymbolsFromThisBrowser(debugName, breakpadId) {
);
}
const { ProfilerGetSymbols } = lazyProfilerGetSymbols();
const { ProfilerGetSymbols } = lazy.ProfilerGetSymbols();
return ProfilerGetSymbols.getSymbolTable(path, debugPath, breakpadId);
}
@ -247,7 +202,7 @@ async function captureProfile() {
}
);
const receiveProfile = lazyBrowserModule().receiveProfile;
const receiveProfile = lazy.BrowserModule().receiveProfile;
receiveProfile(profile, getSymbolsFromThisBrowser);
Services.profiler.StopProfiler();
@ -259,7 +214,7 @@ async function captureProfile() {
* @param {PageContext} pageContext
*/
function startProfiler(pageContext) {
const { translatePreferencesToState } = lazyPreferenceManagement();
const { translatePreferencesToState } = lazy.PreferenceManagement();
const {
entries,
interval,
@ -271,7 +226,7 @@ function startProfiler(pageContext) {
);
// Get the active BrowsingContext ID from browser.
const { getActiveBrowsingContextID } = lazyRecordingUtils();
const { getActiveBrowsingContextID } = lazy.RecordingUtils();
const activeBrowsingContextID = getActiveBrowsingContextID();
Services.profiler.StartProfiler(
@ -352,7 +307,7 @@ function getPrefPostfix(pageContext) {
case "aboutprofiling-remote":
return ".remote";
default: {
const { UnhandledCaseError } = lazyUtils();
const { UnhandledCaseError } = lazy.Utils();
throw new UnhandledCaseError(pageContext, "Page Context");
}
}
@ -526,7 +481,7 @@ function handleWebChannelMessage(channel, id, message, target) {
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",
@ -549,12 +504,12 @@ function handleWebChannelMessage(channel, id, message, target) {
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.

Просмотреть файл

@ -4,53 +4,30 @@
// @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 lazyCustomizableWidgets = requireLazy(() =>
ChromeUtils.import("resource:///modules/CustomizableWidgets.jsm")
);
const lazyPopupPanel = requireLazy(() =>
ChromeUtils.import(
"resource://devtools/client/performance-new/popup/panel.jsm.js"
)
const { createLazyLoaders } = ChromeUtils.import(
"resource://devtools/client/performance-new/typescript-lazy-load.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";
/**
@ -60,7 +37,7 @@ const WIDGET_ID = "profiler-button";
* @return {void}
*/
function addToNavbar(document) {
const { CustomizableUI } = lazyCustomizableUI();
const { CustomizableUI } = lazy.CustomizableUI();
CustomizableUI.addWidgetToArea(WIDGET_ID, CustomizableUI.AREA_NAVBAR);
}
@ -72,7 +49,7 @@ function addToNavbar(document) {
* @return {void}
*/
function remove() {
const { CustomizableUI } = lazyCustomizableUI();
const { CustomizableUI } = lazy.CustomizableUI();
CustomizableUI.removeWidgetFromArea(WIDGET_ID);
}
@ -83,7 +60,7 @@ function remove() {
* @return {boolean}
*/
function isInNavbar() {
const { CustomizableUI } = lazyCustomizableUI();
const { CustomizableUI } = lazy.CustomizableUI();
return Boolean(CustomizableUI.getPlacementOfWidget("profiler-button"));
}
@ -108,9 +85,9 @@ function openPopup(document) {
* @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) {
@ -143,7 +120,7 @@ function initialize(toggleProfilerKeyShortcuts) {
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);
@ -179,7 +156,7 @@ function initialize(toggleProfilerKeyShortcuts) {
createViewControllers,
addPopupEventHandlers,
initializePopup,
} = lazyPopupPanel();
} = lazy.PopupPanel();
const panelElements = selectElementsInPanelview(event.target);
const panelView = createViewControllers(panelState, panelElements);
@ -209,7 +186,7 @@ function initialize(toggleProfilerKeyShortcuts) {
* @type {(document: HTMLDocument) => void}
*/
onBeforeCreated: document => {
/** @type {PerformancePref["PopupIntroDisplayed"]} */
/** @type {import("../@types/perf").PerformancePref["PopupIntroDisplayed"]} */
const popupIntroDisplayedPref =
"devtools.performance.popup.intro-displayed";

Просмотреть файл

@ -19,40 +19,19 @@
* @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.
@ -127,8 +106,8 @@ function createViewControllers(state, elements) {
},
updatePresets() {
const { Services } = lazyServices();
const { presets, getRecordingPreferences } = lazyBackground();
const { Services } = lazy.Services();
const { presets, getRecordingPreferences } = lazy.Background();
const { presetName } = getRecordingPreferences(
"aboutprofiling",
Services.profiler.GetFeatures()
@ -143,13 +122,13 @@ function createViewControllers(state, elements) {
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",
@ -170,8 +149,8 @@ function createViewControllers(state, elements) {
// 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"
);
@ -230,7 +209,7 @@ function initializePopup(state, elements, view) {
// 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.
@ -255,7 +234,7 @@ function addPopupEventHandlers(state, elements, view) {
startProfiler,
stopProfiler,
captureProfile,
} = lazyBackground();
} = lazy.Background();
/**
* Adds a handler that automatically is removed once the panel is hidden.
@ -326,7 +305,7 @@ function addPopupEventHandlers(state, elements, view) {
});
// 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(() => {
@ -335,6 +314,9 @@ function addPopupEventHandlers(state, elements, view) {
});
}
// Provide an exports object for the JSM to be properly read by TypeScript.
/** @type {any} */ (this).module = {};
module.exports = {
selectElementsInPanelview,
createViewControllers,

Просмотреть файл

@ -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);