Bug 1581975 - Factor out the getSymbolsTable browser code from client code; r=julienw

The popup's shortcuts use a different codepath than the popup's buttons.
When using the buttons, the profile was not being captured as a gzipped
profile, and it was using the DevTools' mechanism for getting the symbol
tables. This patch makes the getSymbolTables mechanism configuring in the
recording panel's client.

In addition, browser code made its way into the client. This patch moves
the browser code to all be in browser.js to match the original code
organization for the panel, which was trying to keep browser APIs
out of the React components and Redux store.

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

--HG--
extra : source : 5a3661caf52faaf67b10fcef9e3121d639a17cc3
This commit is contained in:
Greg Tatum 2019-10-04 18:17:43 +00:00
Родитель 038d80fc3b
Коммит 4128a5f6ec
11 изменённых файлов: 264 добавлений и 182 удалений

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

@ -2,7 +2,16 @@
* 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/. */
"use strict";
const Services = require("Services");
// 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
);
const TRANSFER_EVENT = "devtools:perf-html-transfer-profile";
const SYMBOL_TABLE_REQUEST_EVENT = "devtools:perf-html-request-symbol-table";
@ -228,8 +237,189 @@ async function setRecordingPreferencesOnDebuggee(preferenceFront, settings) {
]);
}
/**
* Returns a function getDebugPathFor(debugName, breakpadId) => Library which
* resolves a (debugName, breakpadId) pair to the library's information, which
* contains the absolute paths on the file system where the binary and its
* optional pdb file are stored.
*
* This is needed for the following reason:
* - In order to obtain a symbol table for a system library, we need to know
* the library's absolute path on the file system. On Windows, we
* additionally need to know the absolute path to the library's PDB file,
* which we call the binary's "debugPath".
* - Symbol tables are requested asynchronously, by the profiler UI, after the
* profile itself has been obtained.
* - When the symbol tables are requested, we don't want the profiler UI to
* pass us arbitrary absolute file paths, as an extra defense against
* potential information leaks.
* - Instead, when the UI requests symbol tables, it identifies the library
* with a (debugName, breakpadId) pair. We need to map that pair back to the
* absolute paths.
* - We get the "trusted" paths from the "libs" sections of the profile. We
* trust these paths because we just obtained the profile directly from
* Gecko.
* - This function builds the (debugName, breakpadId) => Library mapping and
* retains it on the returned closure so that it can be consulted after the
* profile has been passed to the UI.
*
* @param {object} profile - The profile JSON object
*/
function createLibraryMap(profile) {
const map = new Map();
function fillMapForProcessRecursive(processProfile) {
for (const lib of processProfile.libs) {
const { debugName, breakpadId } = lib;
const key = [debugName, breakpadId].join(":");
map.set(key, lib);
}
for (const subprocess of processProfile.processes) {
fillMapForProcessRecursive(subprocess);
}
}
fillMapForProcessRecursive(profile);
return function getLibraryFor(debugName, breakpadId) {
const key = [debugName, breakpadId].join(":");
return map.get(key);
};
}
async function getSymbolTableFromDebuggee(perfFront, path, breakpadId) {
const [addresses, index, buffer] = await perfFront.getSymbolTable(
path,
breakpadId
);
// The protocol transmits these arrays as plain JavaScript arrays of
// numbers, but we want to pass them on as typed arrays. Convert them now.
return [
new Uint32Array(addresses),
new Uint32Array(index),
new Uint8Array(buffer),
];
}
async function doesFileExistAtPath(path) {
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;
}
}
/**
* Retrieve a symbol table from a binary on the host machine, by looking up
* relevant build artifacts in the specified objdirs.
* This is needed if the debuggee is a build running on a remote machine that
* was compiled by the developer on *this* machine (the "host machine"). In
* that case, the objdir will contain the compiled binary with full symbol and
* debug information, whereas the binary on the device may not exist in
* uncompressed form or may have been stripped of debug information and some
* symbol information.
* 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
* 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
* 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 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)) {
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.
}
}
}
throw new Error("Could not find any matching binary.");
}
/**
* Profiling through the DevTools remote debugging protocol supports multiple
* different modes. This function is specialized to handle various profiling
* modes such as:
*
* 1) Profiling the same browser on the same machine.
* 2) Profiling a remote browser on the same machine.
* 3) Profiling a remote browser on a different device.
*
* 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
* that should be searched for relevant build artifacts.
* @param {PerfFront} perfFront
* @return {Function}
*/
function createMultiModalGetSymbolTableFn(profile, objdirs, perfFront) {
const libraryGetter = createLibraryMap(profile);
return async function getSymbolTable(debugName, breakpadId) {
const { name, path, debugPath } = libraryGetter(debugName, breakpadId);
if (await doesFileExistAtPath(path)) {
// 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).
try {
// First, try to find a binary with a matching file name and breakpadId
// on the host machine. This works if the profiled build is a developer
// build that has been compiled on this machine, and if the binary is
// one of the Gecko binaries and not a system library.
// The other place where we could obtain symbols is the debuggee device;
// that's handled in the catch branch below.
// We check the host machine first, because if this is a developer
// build, then the objdir files will contain more symbol information
// than the files that get pushed to the device.
return await getSymbolTableFromLocalBinary(objdirs, name, breakpadId);
} catch (e) {
// No matching file was found on the host machine.
// Try to obtain the symbol table on the debuggee. We get into this
// branch in the following cases:
// - Android system libraries
// - Firefox binaries that have no matching equivalent on the host
// machine, for example because the user didn't point us at the
// corresponding objdir, or if the build was compiled somewhere
// else, or if the build on the device is outdated.
// For now, this path is not used on Windows, which is why we don't
// need to pass the library's debugPath.
return getSymbolTableFromDebuggee(perfFront, path, breakpadId);
}
};
}
module.exports = {
receiveProfile,
getRecordingPreferencesFromDebuggee,
setRecordingPreferencesOnDebuggee,
createMultiModalGetSymbolTableFn,
};

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

@ -3,12 +3,12 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/* exported gInit, gDestroy */
/* exported gInit, gDestroy, loader */
const { BrowserLoader } = ChromeUtils.import(
"resource://devtools/client/shared/browser-loader.js"
);
const { require } = BrowserLoader({
const { require, loader } = BrowserLoader({
baseURI: "resource://devtools/client/performance-new/",
window,
});
@ -24,6 +24,7 @@ const {
receiveProfile,
getRecordingPreferencesFromDebuggee,
setRecordingPreferencesOnDebuggee,
createMultiModalGetSymbolTableFn,
} = require("devtools/client/performance-new/browser");
/**
@ -55,6 +56,7 @@ async function gInit(perfFront, preferenceFront) {
preferenceFront,
selectors.getRecordingSettings(store.getState())
),
// Go ahead and hide the implementation details for the component on how the
// preference information is stored
setRecordingPreferences: () =>
@ -62,6 +64,15 @@ async function gInit(perfFront, preferenceFront) {
preferenceFront,
selectors.getRecordingSettings(store.getState())
),
// 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())
),
})
);

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

@ -80,7 +80,7 @@ function adjustState(newState) {
}
}
async function getSymbols(debugName, breakpadId) {
async function getSymbolsFromThisBrowser(debugName, breakpadId) {
if (symbolCache.size === 0) {
primeSymbolStore(Services.profiler.sharedLibraries);
}
@ -125,7 +125,7 @@ async function captureProfile() {
return {};
});
receiveProfile(profile, getSymbols);
receiveProfile(profile, getSymbolsFromThisBrowser);
Services.profiler.StopProfiler();
}
@ -424,4 +424,5 @@ var EXPORTED_SYMBOLS = [
"getRecordingPreferencesFromBrowser",
"setRecordingPreferencesOnBrowser",
"forTestsOnly",
"getSymbolsFromThisBrowser",
];

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

@ -33,6 +33,7 @@ const { require } = BrowserLoader({
const {
getRecordingPreferencesFromBrowser,
setRecordingPreferencesOnBrowser,
getSymbolsFromThisBrowser,
} = ChromeUtils.import(
"resource://devtools/client/performance-new/popup/background.jsm"
);
@ -80,6 +81,9 @@ async function gInit(perfFront, preferenceFront) {
setRecordingPreferencesOnBrowser(
selectors.getRecordingSettings(store.getState())
),
// The popup doesn't need to support remote symbol tables from the debuggee.
// Only get the symbols from this browser.
getSymbolTableGetter: () => getSymbolsFromThisBrowser,
isPopup: true,
})
);

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

@ -165,6 +165,6 @@ function initialize() {
CustomizableWidgets.push(item);
}
const ProfilerMenuButton = { toggle, initialize };
const ProfilerMenuButton = { toggle, initialize, isEnabled };
var EXPORTED_SYMBOLS = ["ProfilerMenuButton"];

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

@ -12,10 +12,6 @@ const {
REQUEST_TO_STOP_PROFILER,
},
} = require("devtools/client/performance-new/utils");
const { OS } = require("resource://gre/modules/osfile.jsm");
const {
ProfilerGetSymbols,
} = require("resource://gre/modules/ProfilerGetSymbols.jsm");
/**
* The recording state manages the current state of the recording panel.
@ -139,127 +135,6 @@ exports.startRecording = () => {
};
};
/**
* Returns a function getDebugPathFor(debugName, breakpadId) => Library which
* resolves a (debugName, breakpadId) pair to the library's information, which
* contains the absolute paths on the file system where the binary and its
* optional pdb file are stored.
*
* This is needed for the following reason:
* - In order to obtain a symbol table for a system library, we need to know
* the library's absolute path on the file system. On Windows, we
* additionally need to know the absolute path to the library's PDB file,
* which we call the binary's "debugPath".
* - Symbol tables are requested asynchronously, by the profiler UI, after the
* profile itself has been obtained.
* - When the symbol tables are requested, we don't want the profiler UI to
* pass us arbitrary absolute file paths, as an extra defense against
* potential information leaks.
* - Instead, when the UI requests symbol tables, it identifies the library
* with a (debugName, breakpadId) pair. We need to map that pair back to the
* absolute paths.
* - We get the "trusted" paths from the "libs" sections of the profile. We
* trust these paths because we just obtained the profile directly from
* Gecko.
* - This function builds the (debugName, breakpadId) => Library mapping and
* retains it on the returned closure so that it can be consulted after the
* profile has been passed to the UI.
*
* @param {object} profile - The profile JSON object
*/
function createLibraryMap(profile) {
const map = new Map();
function fillMapForProcessRecursive(processProfile) {
for (const lib of processProfile.libs) {
const { debugName, breakpadId } = lib;
const key = [debugName, breakpadId].join(":");
map.set(key, lib);
}
for (const subprocess of processProfile.processes) {
fillMapForProcessRecursive(subprocess);
}
}
fillMapForProcessRecursive(profile);
return function getLibraryFor(debugName, breakpadId) {
const key = [debugName, breakpadId].join(":");
return map.get(key);
};
}
async function getSymbolTableFromDebuggee(perfFront, path, breakpadId) {
const [addresses, index, buffer] = await perfFront.getSymbolTable(
path,
breakpadId
);
// The protocol transmits these arrays as plain JavaScript arrays of
// numbers, but we want to pass them on as typed arrays. Convert them now.
return [
new Uint32Array(addresses),
new Uint32Array(index),
new Uint8Array(buffer),
];
}
async function doesFileExistAtPath(path) {
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;
}
}
/**
* Retrieve a symbol table from a binary on the host machine, by looking up
* relevant build artifacts in the specified objdirs.
* This is needed if the debuggee is a build running on a remote machine that
* was compiled by the developer on *this* machine (the "host machine"). In
* that case, the objdir will contain the compiled binary with full symbol and
* debug information, whereas the binary on the device may not exist in
* uncompressed form or may have been stripped of debug information and some
* symbol information.
* 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
* 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
* 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 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)) {
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.
}
}
}
throw new Error("Could not find any matching binary.");
}
/**
* Stops the profiler, and opens the profile in a new window.
* @param {object} window - The current window for the page.
@ -275,46 +150,11 @@ exports.getProfileAndStopProfiler = window => {
window.gClosePopup();
}
const libraryGetter = createLibraryMap(profile);
async function getSymbolTable(debugName, breakpadId) {
const { name, path, debugPath } = libraryGetter(debugName, breakpadId);
if (await doesFileExistAtPath(path)) {
// 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).
try {
// First, try to find a binary with a matching file name and breakpadId
// on the host machine. This works if the profiled build is a developer
// build that has been compiled on this machine, and if the binary is
// one of the Gecko binaries and not a system library.
// The other place where we could obtain symbols is the debuggee device;
// that's handled in the catch branch below.
// We check the host machine first, because if this is a developer
// build, then the objdir files will contain more symbol information
// than the files that get pushed to the device.
const objdirs = selectors.getObjdirs(getState());
return await getSymbolTableFromLocalBinary(objdirs, name, breakpadId);
} catch (e) {
// No matching file was found on the host machine.
// Try to obtain the symbol table on the debuggee. We get into this
// branch in the following cases:
// - Android system libraries
// - Firefox binaries that have no matching equivalent on the host
// machine, for example because the user didn't point us at the
// corresponding objdir, or if the build was compiled somewhere
// else, or if the build on the device is outdated.
// For now, this path is not used on Windows, which is why we don't
// need to pass the library's debugPath.
return getSymbolTableFromDebuggee(perfFront, path, breakpadId);
}
}
const getSymbolTable = selectors.getSymbolTableGetter(getState())(profile);
const receiveProfile = selectors.getReceiveProfileFn(getState());
receiveProfile(profile, getSymbolTable);
selectors.getReceiveProfileFn(getState())(profile, getSymbolTable);
dispatch(changeRecordingState(AVAILABLE_TO_RECORD));
};
};

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

@ -139,6 +139,7 @@ function objdirs(state = [], action) {
* setRecordingPreferences - A function to set the recording settings.
* isPopup - A boolean value that sets lets the UI know if it is in the popup window
* or inside of devtools.
* getSymbolTableGetter - Run this function to get the getSymbolTable function.
* }
*/
function initializedValues(state = null, action) {
@ -149,6 +150,7 @@ function initializedValues(state = null, action) {
receiveProfile: action.receiveProfile,
setRecordingPreferences: action.setRecordingPreferences,
isPopup: Boolean(action.isPopup),
getSymbolTableGetter: action.getSymbolTableGetter,
};
default:
return state;

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

@ -37,6 +37,8 @@ const getReceiveProfileFn = state => getInitializedValues(state).receiveProfile;
const getSetRecordingPreferencesFn = state =>
getInitializedValues(state).setRecordingPreferences;
const getIsPopup = state => getInitializedValues(state).isPopup;
const getSymbolTableGetter = state =>
getInitializedValues(state).getSymbolTableGetter;
module.exports = {
getRecordingState,
@ -54,4 +56,5 @@ module.exports = {
getReceiveProfileFn,
getSetRecordingPreferencesFn,
getIsPopup,
getSymbolTableGetter,
};

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

@ -202,6 +202,8 @@ function createPerfComponent() {
recordingPreferencesCalls.push(settings);
}
const noop = () => {};
function mountComponent() {
store.dispatch(
actions.initializeStore({
@ -211,6 +213,7 @@ function createPerfComponent() {
store.getState()
),
setRecordingPreferences: recordingPreferencesMock,
getSymbolTableGetter: () => noop,
})
);

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

@ -30,7 +30,10 @@ exports.PerfActor = ActorClassWithSpec(perfSpec, {
Actor.prototype.initialize.call(this, conn);
// The "bridge" is the actual implementation of the actor. It is abstracted
// out into its own class so that it can be re-used with the profiler popup.
this.bridge = new ActorReadyGeckoProfilerInterface();
this.bridge = new ActorReadyGeckoProfilerInterface({
// Do not use the gzipped API from the Profiler to capture profiles.
gzipped: false,
});
_bridgeEvents(this, [
"profile-locked-by-private-browsing",

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

@ -31,7 +31,19 @@ const IS_SUPPORTED_PLATFORM = "nsIProfiler" in Ci;
* the Gecko Profiler on the current browser.
*/
class ActorReadyGeckoProfilerInterface {
constructor() {
/**
* @param {Object} options
* @param options.gzipped - This flag controls whether or not to gzip the profile when
* capturing it. The profiler popup wants a gzipped profile in an array buffer, while
* the devtools want the full object. See Bug 1581963 to perhaps provide an API
* to request the gzipped profile. This would then remove this configuration from
* the GeckoProfilerInterface.
*/
constructor(
options = {
gzipped: true,
}
) {
// Only setup the observers on a supported platform.
if (IS_SUPPORTED_PLATFORM) {
this._observer = {
@ -45,6 +57,7 @@ class ActorReadyGeckoProfilerInterface {
);
Services.obs.addObserver(this._observer, "last-pb-context-exited");
}
this.gzipped = options.gzipped;
EventEmitter.decorate(this);
}
@ -128,20 +141,32 @@ class ActorReadyGeckoProfilerInterface {
let profile;
try {
// Attempt to pull out the data.
profile = await Services.profiler.getProfileDataAsync();
if (this.gzipped) {
profile = await Services.profiler.getProfileDataAsGzippedArrayBuffer();
} else {
profile = await Services.profiler.getProfileDataAsync();
// Stop and discard the buffers.
Services.profiler.StopProfiler();
if (Object.keys(profile).length === 0) {
console.error(
"An empty object was received from getProfileDataAsync.getProfileDataAsync(), " +
"meaning that a profile could not successfully be serialized and captured."
);
profile = null;
}
}
} catch (e) {
// If there was any kind of error, bailout with no profile.
return null;
// Explicitly set the profile to null if there as an error.
profile = null;
console.error(
`There was an error fetching a profile (gzipped: ${this.gzipped})`,
e
);
}
// Gecko Profiler errors can return an empty object, return null for this
// case as well.
if (Object.keys(profile).length === 0) {
return null;
}
// Stop and discard the buffers.
Services.profiler.StopProfiler();
// Returns a profile when successful, and null when there is an error.
return profile;
}