Bug 1586757 - Add types to everything but the components; r=julienw

Differential Revision: https://phabricator.services.mozilla.com/D47913

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Greg Tatum 2019-10-08 20:43:16 +00:00
Родитель edef14c2cb
Коммит 0b3c8bde66
13 изменённых файлов: 701 добавлений и 136 удалений

17
devtools/client/performance-new/@types/frame-script.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,17 @@
/* 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/. */
/**
* This file contains the globals for the Gecko Profiler frame script environment.
*/
interface ContentWindow {
wrappedJSObject: {
connectToGeckoProfiler?: (
interface: import("./perf").GeckoProfilerFrameScriptInterface
) => void;
};
}
declare var content: ContentWindow;

183
devtools/client/performance-new/@types/gecko.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,183 @@
/* 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-TODO - Needs typing.
*
* This file contains type stubs for loading things from Gecko. All of these
* types should be used in the correct places eventually.
*/
/**
* Namespace anything that has its types mocked out here. These definitions are
* only "good enough" to get the type checking to pass in this directory.
* Eventually some more structured solution should be found. This namespace is
* global and makes sure that all the definitions inside do not clash with
* naming.
*/
declare namespace MockedExports {
interface ChromeUtils {
/**
* Use a JSDoc import declaration to pull in the correct type.
*/
import: (path: string) => any;
createObjectIn: (content: ContentWindow) => object;
exportFunction: (fn: Function, scope: object, options?: object) => void;
cloneInto: (value: any, scope: object, options?: object) => void;
}
interface MessageManager {
loadFrameScript(url: string, flag: boolean): void;
sendAsyncMessage: (event: string, data: any) => void;
addMessageListener: (event: string, listener: (event: any) => void) => void;
}
interface Browser {
addWebTab: (url: string, options: any) => BrowserTab;
contentPrincipal: any;
selectedTab: BrowserTab;
messageManager: MessageManager;
}
interface BrowserTab {
linkedBrowser: Browser;
}
interface ChromeWindow {
gBrowser: Browser;
}
type GetPref<T> = (prefName: string, defaultValue?: T) => T;
type SetPref<T> = (prefName: string, value?: T) => T;
type Services = {
prefs: {
clearUserPref: (prefName: string) => void;
getStringPref: GetPref<string>;
setStringPref: SetPref<string>;
getCharPref: GetPref<string>;
setCharPref: SetPref<string>;
getIntPref: GetPref<number>;
setIntPref: SetPref<number>;
getBoolPref: GetPref<boolean>;
setBoolPref: SetPref<boolean>;
addObserver: any;
};
profiler: any;
platform: string;
obs: {
addObserver: (observer: object, type: string) => void;
removeObserver: (observer: object, type: string) => void;
};
wm: {
getMostRecentWindow: (name: string) => ChromeWindow;
};
focus: {
activeWindow: ChromeWindow;
};
scriptSecurityManager: any;
};
const ServicesJSM: {
Services: Services;
};
const EventEmitter: {
decorate: (target: object) => void;
};
const ProfilerGetSymbolsJSM: {
ProfilerGetSymbols: {
getSymbolTable: (
path: string,
debugPath: string,
breakpadId: string
) => any;
};
};
const AppConstantsJSM: {
AppConstants: {
platform: string;
};
};
const osfileJSM: {
OS: {
Path: {
split: (
path: string
) => {
absolute: boolean;
components: string[];
winDrive?: string;
};
join: (...pathParts: string[]) => string;
};
File: {
stat: (path: string) => Promise<{ isDir: boolean }>;
Error: any;
};
};
};
const Services: Services;
}
declare module "devtools/shared/event-emitter2" {
export = MockedExports.EventEmitter;
}
declare module "resource://gre/modules/Services.jsm" {
export = MockedExports.ServicesJSM;
}
declare module "Services" {
export = MockedExports.Services;
}
declare module "resource://gre/modules/osfile.jsm" {
export = MockedExports.osfileJSM;
}
declare module "resource://gre/modules/AppConstants.jsm" {
export = MockedExports.AppConstantsJSM;
}
declare module "resource://gre/modules/ProfilerGetSymbols.jsm" {
export = MockedExports.ProfilerGetSymbolsJSM;
}
declare var ChromeUtils: MockedExports.ChromeUtils;
declare var Cu: MockedExports.ChromeUtils;
/**
* This is a variant on the normal Document, as it contains chrome-specific properties.
*/
declare interface ChromeDocument extends Document {
/**
* Create a XUL element of a specific type. Right now this function
* only refines iframes, but more tags could be added.
*/
createXULElement: ((type: "iframe") => XULIframeElement) &
((type: string) => XULElement);
}
/**
* This is a variant on the HTMLElement, as it contains chrome-specific properties.
*/
declare interface ChromeHTMLElement extends HTMLElement {
ownerDocument: ChromeDocument;
}
declare interface XULElement extends HTMLElement {
ownerDocument: ChromeDocument;
}
declare interface XULIframeElement extends XULElement {
contentWindow: ChromeWindow;
src: string;
}
declare interface ChromeWindow extends Window {}

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

@ -1,12 +1,17 @@
/* 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/. */
/**
* This file contains the shared types for the performance-new client.
*/
import {
Reducer as ReduxReducer,
Store as ReduxStore,
} from "devtools/client/shared/vendor/redux";
export interface PanelWindow extends Window {
export interface PanelWindow {
gToolbox?: any;
gTarget?: any;
gInit(perfFront: any, preferenceFront: any): void;
@ -14,7 +19,7 @@ export interface PanelWindow extends Window {
}
/**
* TODO
* TS-TODO - Stub.
*/
export interface Target {
// TODO
@ -22,14 +27,14 @@ export interface Target {
}
/**
* TODO
* TS-TODO - Stub.
*/
export interface Toolbox {
target: Target;
}
/**
* TODO
* TS-TODO - Stub.
*/
export interface PerfFront {
startProfiler: any;
@ -42,9 +47,17 @@ export interface PerfFront {
}
/**
* TODO
* TS-TODO - Stub
*/
export interface PreferenceFront {}
export interface PreferenceFront {
clearUserPref: (prefName: string) => Promise<void>;
getStringPref: (prefName: string) => Promise<string>;
setStringPref: (prefName: string, value: string) => Promise<void>;
getCharPref: (prefName: string) => Promise<string>;
setCharPref: (prefName: string, value: string) => Promise<void>;
getIntPref: (prefName: string) => Promise<number>;
setIntPref: (prefName: string, value: number) => Promise<void>;
}
export type RecordingState =
// The initial state before we've queried the PerfActor
@ -122,7 +135,15 @@ export type ReceiveProfile = (
getSymbolTableCallback: GetSymbolTableCallback
) => void;
export type SetRecordingPreferences = (settings: Object) => void;
export type SetRecordingPreferences = (settings: object) => void;
/**
* This interface is injected into profiler.firefox.com
*/
interface GeckoProfilerFrameScriptInterface {
getProfile: () => Promise<object>;
getSymbolTable: GetSymbolTableCallback;
}
export interface RecordingStateFromPreferences {
entries: number;
@ -148,7 +169,7 @@ export interface InitializedValues {
// or inside of devtools.
isPopup: boolean;
// The popup and devtools panel use different codepaths for getting symbol tables.
getSymbolTableGetter: (profile: Object) => GetSymbolTableCallback;
getSymbolTableGetter: (profile: object) => GetSymbolTableCallback;
}
/**
@ -195,5 +216,27 @@ export type Action =
setRecordingPreferences: SetRecordingPreferences;
isPopup: boolean;
recordingSettingsFromPreferences: RecordingStateFromPreferences;
getSymbolTableGetter: (profile: Object) => GetSymbolTableCallback;
getSymbolTableGetter: (profile: object) => GetSymbolTableCallback;
};
export type PopupBackgroundFeatures = { [feature: string]: boolean };
/**
* The state of the profiler popup.
*/
export interface PopupBackgroundState {
isRunning: boolean;
settingsOpen: boolean;
features: PopupBackgroundFeatures;
buffersize: number;
windowLength: number;
interval: number;
threads: string;
}
// TS-TODO - Stub
export interface ContentFrameMessageManager {
addMessageListener: (event: string, listener: (event: any) => void) => void;
addEventListener: (event: string, listener: (event: any) => void) => void;
sendAsyncMessage: (name: string, data: any) => void;
}

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

@ -1,16 +1,47 @@
/* 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";
// The "loader" is globally available, much like "require".
loader.lazyRequireGetter(this, "Services");
loader.lazyRequireGetter(this, "OS", "resource://gre/modules/osfile.jsm", true);
loader.lazyRequireGetter(
this,
"ProfilerGetSymbols",
"resource://gre/modules/ProfilerGetSymbols.jsm",
true
/**
* @typedef {import("./@types/perf").Action} Action
* @typedef {import("./@types/perf").Library} Library
* @typedef {import("./@types/perf").PerfFront} PerfFront
* @typedef {import("./@types/perf").SymbolTableAsTuple} SymbolTableAsTuple
* @typedef {import("./@types/perf").RecordingState} RecordingState
* @typedef {import("./@types/perf").GetSymbolTableCallback} GetSymbolTableCallback
* @typedef {import("./@types/perf").PreferenceFront} PreferenceFront
*/
/**
* 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 lazyOS = requireLazy(() => require("resource://gre/modules/osfile.jsm"));
const lazyProfilerGetSymbols = requireLazy(() =>
require("resource://gre/modules/ProfilerGetSymbols.jsm")
);
const TRANSFER_EVENT = "devtools:perf-html-transfer-profile";
@ -35,13 +66,14 @@ const OBJDIRS_PREF = "devtools.performance.recording.objdirs";
* into a new browser tab, and injects the profile via a frame script.
*
* @param {object} profile - The Gecko profile.
* @param {function} getSymbolTableCallback - A callback function with the signature
* @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();
// Find the most recently used window, as the DevTools client could be in a variety
// of hosts.
const win = Services.wm.getMostRecentWindow("navigator:browser");
@ -104,7 +136,7 @@ function receiveProfile(profile, getSymbolTableCallback) {
* function always returns a valid array of strings.
* @param {PreferenceFront} preferenceFront
* @param {string} prefName
* @param {array of string} defaultValue
* @param {string[]} defaultValue
*/
async function _getArrayOfStringsPref(preferenceFront, prefName, defaultValue) {
let array;
@ -132,9 +164,10 @@ async function _getArrayOfStringsPref(preferenceFront, prefName, defaultValue) {
* even exists. Gracefully handle malformed data or missing data. Ensure that this
* function always returns a valid array of strings.
* @param {string} prefName
* @param {array of string} defaultValue
* @param {string[]} defaultValue
*/
async function _getArrayOfStringsHostPref(prefName, defaultValue) {
const { Services } = lazyServices();
let array;
try {
const text = Services.prefs.getStringPref(
@ -220,10 +253,11 @@ async function getRecordingPreferencesFromDebuggee(
* and some of them on the host browser instance.
*
* @param {PreferenceFront} preferenceFront
* @param {object} defaultSettings See the getRecordingSettings selector for the shape
* @param {object} settings See the getRecordingSettings selector for the shape
* of the object and how it gets defined.
*/
async function setRecordingPreferencesOnDebuggee(preferenceFront, settings) {
const { Services } = lazyServices();
await Promise.all([
preferenceFront.setIntPref(
`devtools.performance.recording.entries`,
@ -273,9 +307,14 @@ async function setRecordingPreferencesOnDebuggee(preferenceFront, settings) {
* profile has been passed to the UI.
*
* @param {object} profile - The profile JSON object
* @returns {(debugName: string, breakpadId: string) => Library | undefined}
*/
function createLibraryMap(profile) {
const map = new Map();
/**
* @param {object} processProfile
*/
function fillMapForProcessRecursive(processProfile) {
for (const lib of processProfile.libs) {
const { debugName, breakpadId } = lib;
@ -294,6 +333,12 @@ function createLibraryMap(profile) {
};
}
/**
* @param {PerfFront} perfFront
* @param {string} path
* @param {string} breakpadId
* @returns {Promise<SymbolTableAsTuple>}
*/
async function getSymbolTableFromDebuggee(perfFront, path, breakpadId) {
const [addresses, index, buffer] = await perfFront.getSymbolTable(
path,
@ -308,7 +353,12 @@ async function getSymbolTableFromDebuggee(perfFront, path, breakpadId) {
];
}
/**
* @param {string} path
* @returns {Promise<boolean>}
*/
async function doesFileExistAtPath(path) {
const { OS } = lazyOS();
try {
const result = await OS.File.stat(path);
return !result.isDir;
@ -332,15 +382,16 @@ async function doesFileExistAtPath(path) {
* An objdir, or "object directory", is a directory on the host machine that's
* used to store build artifacts ("object files") from the compilation process.
*
* @param {array of string} objdirs An array of objdir paths on the host machine
* @param {string[]} objdirs An array of objdir paths on the host machine
* 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} The symbol table of the first encountered binary with a
* @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 candidatePaths = [];
for (const objdirPath of objdirs) {
// Binaries are usually expected to exist at objdir/dist/bin/filename.
@ -354,6 +405,7 @@ async function getSymbolTableFromLocalBinary(objdirs, filename, breakpadId) {
for (const path of candidatePaths) {
if (await doesFileExistAtPath(path)) {
const { ProfilerGetSymbols } = lazyProfilerGetSymbols();
try {
return await ProfilerGetSymbols.getSymbolTable(path, path, breakpadId);
} catch (e) {
@ -379,18 +431,25 @@ async function getSymbolTableFromLocalBinary(objdirs, filename, breakpadId) {
* The profiler popup uses a more simplified version of this function as
* it's dealing with a simpler situation.
*
* @param {Profile} profile - The raw profie (not gzipped).
* @param {array of string} objdirs - An array of objdir paths on the host machine
* @param {object} profile - The raw profie (not gzipped).
* @param {string[]} objdirs - An array of objdir paths on the host machine
* that should be searched for relevant build artifacts.
* @param {PerfFront} perfFront
* @return {Function}
* @return {GetSymbolTableCallback}
*/
function createMultiModalGetSymbolTableFn(profile, objdirs, perfFront) {
const libraryGetter = createLibraryMap(profile);
return async function getSymbolTable(debugName, breakpadId) {
const { name, path, debugPath } = libraryGetter(debugName, breakpadId);
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();
// 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.

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

@ -1,8 +1,15 @@
/* 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
/// <reference path="./@types/frame-script.d.ts" />
/* global content */
"use strict";
/* global addMessageListener, addEventListener, content, sendAsyncMessage */
/**
* @typedef {import("./@types/perf").GetSymbolTableCallback} GetSymbolTableCallback
* @typedef {import("./@types/perf").ContentFrameMessageManager} ContentFrameMessageManager
*/
/**
* This frame script injects itself into profiler.firefox.com and injects the profile
@ -13,19 +20,33 @@ 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 {null | Object} */
let gProfile = null;
const symbolReplyPromiseMap = new Map();
addMessageListener(TRANSFER_EVENT, e => {
/**
* TypeScript wants to use the DOM library definition, which conflicts with our
* own definitions for the frame message manager. Instead, coerce the `this`
* variable into the proper interface.
*
* @type {ContentFrameMessageManager}
*/
let frameScript;
{
const any = /** @type {any} */ (this);
frameScript = any;
}
frameScript.addMessageListener(TRANSFER_EVENT, e => {
gProfile = e.data;
// Eagerly try and see if the framescript was evaluated after perf loaded its scripts.
connectToPage();
// If not try again at DOMContentLoaded which should be called after the script
// tag was synchronously loaded in.
addEventListener("DOMContentLoaded", connectToPage);
frameScript.addEventListener("DOMContentLoaded", connectToPage);
});
addMessageListener(SYMBOL_TABLE_RESPONSE_EVENT, e => {
frameScript.addMessageListener(SYMBOL_TABLE_RESPONSE_EVENT, e => {
const { debugName, breakpadId, status, result, error } = e.data;
const promiseKey = [debugName, breakpadId].join(":");
const { resolve, reject } = symbolReplyPromiseMap.get(promiseKey);
@ -55,9 +76,13 @@ function connectToPage() {
}
}
/** @type {GetSymbolTableCallback} */
function getSymbolTable(debugName, breakpadId) {
return new Promise((resolve, reject) => {
sendAsyncMessage(SYMBOL_TABLE_REQUEST_EVENT, { debugName, breakpadId });
frameScript.sendAsyncMessage(SYMBOL_TABLE_REQUEST_EVENT, {
debugName,
breakpadId,
});
symbolReplyPromiseMap.set([debugName, breakpadId].join(":"), {
resolve,
reject,
@ -72,11 +97,23 @@ function getSymbolTable(debugName, breakpadId) {
/**
* Create a promise that can be used in the page.
*
* @template T
* @param {(resolve: Function, reject: Function) => Promise<T>} fun
* @param {object} contentGlobal
* @returns Promise<T>
*/
function createPromiseInPage(fun, contentGlobal) {
/**
* Use the any type here, as this is pretty dynamic, and probably not worth typing.
* @param {any} resolve
* @param {any} reject
*/
function funThatClonesObjects(resolve, reject) {
return fun(
/** @type {(result: any) => any} */
result => resolve(Cu.cloneInto(result, contentGlobal)),
/** @type {(result: any) => any} */
error => reject(Cu.cloneInto(error, contentGlobal))
);
}
@ -88,9 +125,13 @@ function createPromiseInPage(fun, contentGlobal) {
/**
* Returns a function that calls the original function and tries to make the
* return value available to the page.
* @param {Function} fun
* @param {object} contentGlobal
* @return {Function}
*/
function wrapFunction(fun, contentGlobal) {
return function() {
// @ts-ignore - Ignore the use of `this`.
const result = fun.apply(this, arguments);
if (typeof result === "object") {
if ("then" in result && typeof result.then === "function") {
@ -110,12 +151,18 @@ function wrapFunction(fun, contentGlobal) {
* Pass a simple object containing values that are objects or functions.
* The objects or functions are wrapped in such a way that they can be
* consumed by the page.
* @template T
* @param {T} obj
* @param {object} contentGlobal
* @return {T}
*/
function makeAccessibleToPage(obj, contentGlobal) {
/** @type {any} - This value is probably too dynamic to type. */
const result = Cu.createObjectIn(contentGlobal);
for (const field in obj) {
switch (typeof obj[field]) {
case "function":
// @ts-ignore - Ignore the obj[field] call. This code is too dynamic.
Cu.exportFunction(wrapFunction(obj[field], contentGlobal), result, {
defineAs: field,
});

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

@ -1,9 +1,14 @@
/* 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
/* exported gInit, gDestroy, loader */
"use strict";
/* exported gInit, gDestroy, loader */
/**
* @typedef {import("./@types/perf").PerfFront} PerfFront
* @typedef {import("./@types/perf").PreferenceFront} PreferenceFront
*/
const { BrowserLoader } = ChromeUtils.import(
"resource://devtools/client/shared/browser-loader.js"
@ -36,8 +41,8 @@ const {
/**
* Initialize the panel by creating a redux store, and render the root component.
*
* @param perfFront - The Perf actor's front. Used to start and stop recordings.
* @param preferenceFront - Used to get the recording preferences from the device.
* @param {PerfFront} perfFront - The Perf actor's front. Used to start and stop recordings.
* @param {PreferenceFront} preferenceFront - Used to get the recording preferences from the device.
*/
async function gInit(perfFront, preferenceFront) {
const store = createStore(reducers);
@ -67,12 +72,14 @@ async function gInit(perfFront, preferenceFront) {
// Configure the getSymbolTable function for the DevTools workflow.
// See createMultiModalGetSymbolTableFn for more information.
getSymbolTableGetter: profile =>
createMultiModalGetSymbolTableFn(
profile,
selectors.getPerfFront(store.getState()),
selectors.getObjdirs(store.getState())
),
getSymbolTableGetter:
/** @type {(profile: Object) => GetSymbolTableCallback} */
profile =>
createMultiModalGetSymbolTableFn(
profile,
selectors.getPerfFront(store.getState()),
selectors.getObjdirs(store.getState())
),
})
);

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

@ -1,11 +1,9 @@
/* 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";
// @ts-ignore - No support yet for lazyRequireGetter.
loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
/**
* This file contains the PerformancePanel, which uses a common API for DevTools to
* start and load everything. This will call `gInit` from the initializer.js file,
@ -14,9 +12,9 @@ loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
*/
/**
* @typedef {import("./types").PanelWindow} PanelWindow
* @typedef {import("./types").Toolbox} Toolbox
* @typedef {import("./types").Target} Target
* @typedef {import("./@types/perf").PanelWindow} PanelWindow
* @typedef {import("./@types/perf").Toolbox} Toolbox
* @typedef {import("./@types/perf").Target} Target
*/
class PerformancePanel {
@ -28,7 +26,7 @@ class PerformancePanel {
this.panelWin = iframeWindow;
this.toolbox = toolbox;
// @ts-ignore - No support yet for lazyRequireGetter.
const EventEmitter = require("devtools/shared/event-emitter");
EventEmitter.decorate(this);
}

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

@ -1,6 +1,7 @@
/* 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";
/**
@ -10,29 +11,64 @@
* 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.f
// The following are not lazily loaded as they are needed during initialization.
/** @type {import("resource://gre/modules/Services.jsm")} */
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
/** @type {import("resource://gre/modules/AppConstants.jsm")} */
const { AppConstants } = ChromeUtils.import(
"resource://gre/modules/AppConstants.jsm"
);
const { loader } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
/**
* @typedef {import("../@types/perf").RecordingStateFromPreferences} RecordingStateFromPreferences
* @typedef {import("../@types/perf").PopupBackgroundFeatures} PopupBackgroundFeatures
* @typedef {import("../@types/perf").SymbolTableAsTuple} SymbolTableAsTuple
*/
// 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.
ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
ChromeUtils.defineModuleGetter(
this,
"ProfilerGetSymbols",
"resource://gre/modules/ProfilerGetSymbols.jsm"
/**
* 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(() =>
/** @type {import("resource://gre/modules/osfile.jsm")} */
(ChromeUtils.import("resource://gre/modules/osfile.jsm"))
);
loader.lazyRequireGetter(
this,
"receiveProfile",
"devtools/client/performance-new/browser",
true
const lazyProfilerGetSymbols = requireLazy(() =>
/** @type {import("resource://gre/modules/ProfilerGetSymbols.jsm")} */
(ChromeUtils.import("resource://gre/modules/ProfilerGetSymbols.jsm"))
);
const lazyReceiveProfile = requireLazy(() => {
const { require } = ChromeUtils.import(
"resource://devtools/shared/Loader.jsm"
);
/** @type {import("devtools/client/performance-new/browser")} */
const browserModule = require("devtools/client/performance-new/browser");
return browserModule.receiveProfile;
});
// This pref contains the JSON serialization of the popup's profiler state with
// a string key based off of the debug name and breakpad id.
const PROFILER_STATE_PREF = "devtools.performance.popup";
@ -42,15 +78,35 @@ const DEFAULT_BUFFER_SIZE = 10000000; // 90MB
const DEFAULT_THREADS = "GeckoMain,Compositor";
const DEFAULT_STACKWALK_FEATURE = true;
// This Map caches the symbols from the shared libraries.
/**
* This Map caches the symbols from the shared libraries.
* @type {Map<string, { path: string, debugPath: string }>}
*/
const symbolCache = new Map();
/**
* @typedef {{
* path: string,
* debugName: string,
* debugPath: string,
* breakpadId: string
* }} Libs
*
* @type {(libs: Libs[]) => void}
*/
const primeSymbolStore = libs => {
for (const { path, debugName, debugPath, breakpadId } of libs) {
symbolCache.set(`${debugName}/${breakpadId}`, { path, debugPath });
}
};
/**
* @typedef {import("../@types/perf").PopupBackgroundState} PopupBackgroundState
*/
/**
* @type {PopupBackgroundState}
*/
const state = initializeState();
const forTestsOnly = {
@ -66,6 +122,9 @@ const forTestsOnly = {
},
};
/**
* @param {Partial<PopupBackgroundState>} newState
*/
function adjustState(newState) {
// Deep clone the object, since this can be called through popup.xhtml,
// which can be unloaded thus leaving this object dead.
@ -80,6 +139,10 @@ function adjustState(newState) {
}
}
/**
* TS-TODO - Fix any.
* @type {(debugName: string, breakpadId: string) => Promise<SymbolTableAsTuple>}
*/
async function getSymbolsFromThisBrowser(debugName, breakpadId) {
if (symbolCache.size === 0) {
primeSymbolStore(Services.profiler.sharedLibraries);
@ -98,6 +161,7 @@ async function getSymbolsFromThisBrowser(debugName, breakpadId) {
}
const { path, debugPath } = cachedLibInfo;
const { OS } = lazyOS();
if (!OS.Path.split(path).absolute) {
throw new Error(
"Services.profiler.sharedLibraries did not contain an absolute path for " +
@ -106,9 +170,13 @@ async function getSymbolsFromThisBrowser(debugName, breakpadId) {
);
}
const { ProfilerGetSymbols } = lazyProfilerGetSymbols();
return ProfilerGetSymbols.getSymbolTable(path, debugPath, breakpadId);
}
/**
* @type {() => Promise<void>}
*/
async function captureProfile() {
if (!state.isRunning) {
// The profiler is not active, ignore this shortcut.
@ -120,11 +188,14 @@ async function captureProfile() {
const profile = await Services.profiler
.getProfileDataAsGzippedArrayBuffer()
.catch(e => {
console.error(e);
return {};
});
.catch(
/** @type {(e: any) => {}} */ e => {
console.error(e);
return {};
}
);
const receiveProfile = lazyReceiveProfile();
receiveProfile(profile, getSymbolsFromThisBrowser);
Services.profiler.StopProfiler();
@ -133,6 +204,9 @@ async function captureProfile() {
/**
* Not all features are supported on every version of Firefox. Get the list of checked
* features, add a few defaults, and filter for what is actually supported.
* @param {PopupBackgroundFeatures} features
* @param {string[]} threads
* @returns {string[]}
*/
function getEnabledFeatures(features, threads) {
const enabledFeatures = Object.keys(features).filter(f => features[f]);
@ -160,10 +234,16 @@ function startProfiler() {
);
}
/**
* @type {() => void}
*/
function stopProfiler() {
Services.profiler.StopProfiler();
}
/**
* @type {() => void}
*/
function toggleProfiler() {
if (state.isRunning) {
stopProfiler();
@ -172,6 +252,9 @@ function toggleProfiler() {
}
}
/**
* @type {() => void}
*/
function restartProfiler() {
stopProfiler();
startProfiler();
@ -181,6 +264,11 @@ function restartProfiler() {
const isRunningObserver = {
_observers: new Set(),
/**
* @param {string} subject
* @param {string} topic
* @param {unknown} data
*/
observe(subject, topic, data) {
switch (topic) {
case "profiler-started":
@ -204,6 +292,9 @@ const isRunningObserver = {
Services.obs.removeObserver(this, "profiler-stopped");
},
/**
* @param {(isActive: boolean) => any} observer
*/
addObserver(observer) {
if (this._observers.size === 0) {
this._startListening();
@ -214,6 +305,9 @@ const isRunningObserver = {
Promise.resolve(Services.profiler.IsActive()).then(observer);
},
/**
* @param {(isActive: boolean) => any} observer
*/
removeObserver(observer) {
if (this._observers.delete(observer) && this._observers.size === 0) {
this._stopListening();
@ -221,6 +315,9 @@ const isRunningObserver = {
},
};
/**
* @returns {PopupBackgroundState | null}
*/
function getStoredStateOrNull() {
// Pull out the stored state from preferences, it is a raw string.
const storedStateString = Services.prefs.getStringPref(
@ -242,7 +339,11 @@ function getStoredStateOrNull() {
}
return null;
}
/**
* @param {string} prefName
* @param {string[]} defaultValue
* @return {string[]}
*/
function _getArrayOfStringsPref(prefName, defaultValue) {
let array;
try {
@ -262,6 +363,11 @@ function _getArrayOfStringsPref(prefName, defaultValue) {
return defaultValue;
}
/**
* @param {string} prefName
* @param {string[]} defaultValue
* @return {string[]}
*/
function _getArrayOfStringsHostPref(prefName, defaultValue) {
let array;
try {
@ -284,7 +390,11 @@ function _getArrayOfStringsHostPref(prefName, defaultValue) {
return defaultValue;
}
function getRecordingPreferencesFromBrowser(defaultSettings = {}) {
/**
* @param {RecordingStateFromPreferences} defaultSettings
* @return {RecordingStateFromPreferences}
*/
function getRecordingPreferencesFromBrowser(defaultSettings) {
const [entries, interval, features, threads, objdirs] = [
Services.prefs.getIntPref(
`devtools.performance.recording.entries`,
@ -313,6 +423,9 @@ function getRecordingPreferencesFromBrowser(defaultSettings = {}) {
return { entries, interval: newInterval, features, threads, objdirs };
}
/**
* @param {RecordingStateFromPreferences} settings
*/
function setRecordingPreferencesOnBrowser(settings) {
Services.prefs.setIntPref(
`devtools.performance.recording.entries`,
@ -337,6 +450,9 @@ function setRecordingPreferencesOnBrowser(settings) {
);
}
/**
* @returns {PopupBackgroundState}
*/
function initializeState() {
const features = {
java: false,
@ -368,18 +484,28 @@ function initializeState() {
// Validate the stored state. It's possible a feature was added or removed
// since the profiler was last run.
for (const key of Object.keys(features)) {
features[key] =
key in storedFeatures ? Boolean(storedFeatures[key]) : features[key];
/** @type {{[key: string]: boolean}} */
const featureAsObjMap = features;
featureAsObjMap[key] =
key in storedFeatures
? Boolean(storedFeatures[key])
: featureAsObjMap[key];
}
}
// This function is created inline to make it easy to validate
// the stored state using the captured storedState value.
/**
* This function is created inline to make it easy to validate
* the stored state using the captured storedState value.
* @template Value
* @type {(key: string, type: string, defaultValue: Value) => Value}
*/
function validateStoredState(key, type, defaultValue) {
if (!storedState) {
return defaultValue;
}
const storedValue = storedState[key];
/** @type {object} */
const storedStateAsObjMap = storedState;
const storedValue = storedStateAsObjMap[key];
return typeof storedValue === type ? storedValue : defaultValue;
}

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

@ -1,9 +1,14 @@
/* 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").PerfFront} PerfFront
* @typedef {import("../@types/perf").PreferenceFront} PreferenceFront
*/
/**
* This file initializes the profiler popup UI. It is in charge of initializing
* the browser specific environment, and then passing those requirements into
@ -25,7 +30,7 @@ const { require } = BrowserLoader({
});
/**
* The background.jsm manages the profiler state, and can be loaded multiple time
* The background.jsm.js manages the profiler state, and can be loaded multiple time
* for various components. This pop-up needs a copy, and it is also used by the
* profiler shortcuts. In order to do this, the background code needs to live in a
* JSM module, that can be shared with the DevTools keyboard shortcut manager.
@ -102,8 +107,9 @@ async function gInit() {
function resizeWindow() {
window.requestAnimationFrame(() => {
if (window.gResizePopup) {
window.gResizePopup(document.body.clientHeight);
const { gResizePopup } = /** @type {any} */ (window);
if (gResizePopup) {
gResizePopup(document.body.clientHeight);
}
});
}

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

@ -1,6 +1,7 @@
/* 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";
/**
@ -8,20 +9,37 @@
* Care should be taken to keep it minimal as it can be run with browser initialization.
*/
ChromeUtils.defineModuleGetter(
this,
"Services",
"resource://gre/modules/Services.jsm"
/**
* 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(() =>
/** @type {import("resource://gre/modules/Services.jsm")} */
(ChromeUtils.import("resource://gre/modules/Services.jsm"))
);
ChromeUtils.defineModuleGetter(
this,
"CustomizableUI",
"resource:///modules/CustomizableUI.jsm"
const lazyCustomizableUI = requireLazy(() =>
/** @type {import("resource:///modules/CustomizableUI.jsm")} */
ChromeUtils.import("resource:///modules/CustomizableUI.jsm")
);
ChromeUtils.defineModuleGetter(
this,
"CustomizableWidgets",
"resource:///modules/CustomizableWidgets.jsm"
const lazyCustomizableWidgets = requireLazy(() =>
/** @type {import("resource:///modules/CustomizableWidgets.jsm")} */
ChromeUtils.import("resource:///modules/CustomizableWidgets.jsm")
);
// The profiler's menu button and its popup can be enabled/disabled by the user.
@ -31,26 +49,40 @@ ChromeUtils.defineModuleGetter(
const BUTTON_ENABLED_PREF = "devtools.performance.popup.enabled";
const WIDGET_ID = "profiler-button";
/**
* @return {boolean}
*/
function isEnabled() {
const { Services } = lazyServices();
return Services.prefs.getBoolPref(BUTTON_ENABLED_PREF, false);
}
/**
* @param {HTMLDocument} document
* @param {boolean} isChecked
* @return {void}
*/
function setMenuItemChecked(document, isChecked) {
const menuItem = document.querySelector("#menu_toggleProfilerButtonMenu");
if (!menuItem) {
return;
}
menuItem.setAttribute("checked", isChecked);
menuItem.setAttribute("checked", isChecked.toString());
}
/**
* Toggle the menu button, and initialize the widget if needed.
*
* @param {Object} document - The browser's document.
* @param {object} document - The browser's document.
* @return {void}
*/
function toggle(document) {
const { CustomizableUI } = lazyCustomizableUI();
const { Services } = lazyServices();
const toggledValue = !isEnabled();
Services.prefs.setBoolPref(BUTTON_ENABLED_PREF, toggledValue);
if (toggledValue) {
initialize();
CustomizableUI.addWidgetToArea(WIDGET_ID, CustomizableUI.AREA_NAVBAR);
@ -65,70 +97,108 @@ function toggle(document) {
}
}
// This function takes the button element, and returns a function that's used to
// update the profiler button whenever the profiler activation status changed.
const updateButtonColorForElement = buttonElement => () => {
const isRunning = Services.profiler.IsActive();
/**
* This function takes the button element, and returns a function that's used to
* update the profiler button whenever the profiler activation status changed.
*
* @param {HTMLElement} buttonElement
* @returns {() => void}
*/
function updateButtonColorForElement(buttonElement) {
return () => {
const { Services } = lazyServices();
const isRunning = Services.profiler.IsActive();
// Use photon blue-60 when active.
buttonElement.style.fill = isRunning ? "#0060df" : "";
};
// Use photon blue-60 when active.
buttonElement.style.fill = isRunning ? "#0060df" : "";
};
}
/**
* This function creates the widget definition for the CustomizableUI. It should
* only be run if the profiler button is enabled.
* @return {void}
*/
function initialize() {
const { CustomizableUI } = lazyCustomizableUI();
const { CustomizableWidgets } = lazyCustomizableWidgets();
const { Services } = lazyServices();
const widget = CustomizableUI.getWidget(WIDGET_ID);
if (widget && widget.provider == CustomizableUI.PROVIDER_API) {
// This widget has already been created.
return;
}
let observer;
/** @typedef {() => void} Observer */
/** @type {null | Observer} */
let observer = null;
const item = {
id: WIDGET_ID,
type: "view",
viewId: "PanelUI-profiler",
tooltiptext: "profiler-button.tooltiptext",
onViewShowing: event => {
const panelview = event.target;
const document = panelview.ownerDocument;
// Create an iframe and append it to the panelview.
const iframe = document.createXULElement("iframe");
iframe.id = "PanelUI-profilerIframe";
iframe.className = "PanelUI-developer-iframe";
iframe.src =
"chrome://devtools/content/performance-new/popup/popup.xhtml";
onViewShowing:
/**
* @type {(event: {
* target: ChromeHTMLElement | XULElement,
* detail: {
* addBlocker: (blocker: Promise<void>) => void
* }
* }) => void}
*/
event => {
const panelview = event.target;
const document = panelview.ownerDocument;
if (!document) {
throw new Error(
"Expected to find a document on the panelview element."
);
}
panelview.appendChild(iframe);
// Create an iframe and append it to the panelview.
const iframe = document.createXULElement("iframe");
iframe.id = "PanelUI-profilerIframe";
iframe.className = "PanelUI-developer-iframe";
iframe.src =
"chrome://devtools/content/performance-new/popup/popup.xhtml";
// Provide a mechanism for the iframe to close the popup.
iframe.contentWindow.gClosePopup = () => {
CustomizableUI.hidePanelForNode(iframe);
};
panelview.appendChild(iframe);
/** @type {any} - Cast to an any since we're assigning values to the window object. */
const contentWindow = iframe.contentWindow;
// Provide a mechanism for the iframe to resize the popup.
iframe.contentWindow.gResizePopup = height => {
iframe.style.height = `${Math.min(600, height)}px`;
};
// Provide a mechanism for the iframe to close the popup.
contentWindow.gClosePopup = () => {
CustomizableUI.hidePanelForNode(iframe);
};
// The popup has an annoying rendering "blip" when first rendering the react
// components. This adds a blocker until the content is ready to show.
event.detail.addBlocker(
new Promise(resolve => {
iframe.contentWindow.gReportReady = () => {
// Delete the function gReportReady so we don't leave any dangling
// references between windows.
delete iframe.contentWindow.gReportReady;
// Now resolve this promise to open the window.
resolve();
};
})
);
},
// Provide a mechanism for the iframe to resize the popup.
/** @type {(height: number) => void} */
contentWindow.gResizePopup = height => {
iframe.style.height = `${Math.min(600, height)}px`;
};
// The popup has an annoying rendering "blip" when first rendering the react
// components. This adds a blocker until the content is ready to show.
event.detail.addBlocker(
new Promise(resolve => {
contentWindow.gReportReady = () => {
// Delete the function gReportReady so we don't leave any dangling
// references between windows.
delete contentWindow.gReportReady;
// Now resolve this promise to open the window.
resolve();
};
})
);
},
/**
* @type {(event: { target: ChromeHTMLElement | XULElement }) => void}
*/
onViewHiding(event) {
const document = event.target.ownerDocument;
@ -141,9 +211,13 @@ function initialize() {
// Remove the iframe so it doesn't leak.
iframe.remove();
},
/** @type {(document: HTMLDocument) => void} */
onBeforeCreated: document => {
setMenuItemChecked(document, true);
},
/** @type {(document: HTMLElement) => void} */
onCreated: buttonElement => {
observer = updateButtonColorForElement(buttonElement);
Services.obs.addObserver(observer, "profiler-started");
@ -153,12 +227,16 @@ function initialize() {
// already running at startup.
observer();
},
onDestroyed: () => {
Services.obs.removeObserver(observer, "profiler-started");
Services.obs.removeObserver(observer, "profiler-stopped");
observer = null;
if (observer) {
Services.obs.removeObserver(observer, "profiler-started");
Services.obs.removeObserver(observer, "profiler-stopped");
observer = null;
}
},
};
CustomizableUI.createWidget(item);
CustomizableWidgets.push(item);
}

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

@ -93,7 +93,7 @@ exports.changeEntries = entries =>
/**
* Updates the recording settings for the features.
* @param {Object} features
* @param {object} features
* @return {ThunkAction<void>}
*/
exports.changeFeatures = features =>

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

@ -178,7 +178,7 @@ function setProfilerFrontendUrl(url) {
* test harness. This function runs in a loop every requestAnimationFrame, and
* checks for a sucess title. In addition, an "initialTitle" and "errorTitle"
* can be specified for nicer test output.
* @param {Object}
* @param {object}
* {
* initialTitle: string,
* successTitle: string,

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

@ -13,6 +13,7 @@
"target": "esnext",
"lib": ["esnext", "dom"]
},
"files": ["./@types/gecko.d.ts"],
// Add a @ts-check comment to a JS file to start type checking it.
"include": ["./**/*.js", "./@types/*"]
"include": ["./**/*.js"]
}