Bug 1617246 - Always consult objdirs for symbols. r=gregtatum

This moves some symbolication code out of browser.js and into a new module symbolication.jsm.js.

It also threads the pageContext parameter through to getSymbolsFromThisBrowser so that it can
respect the correct objdir pref. This part is probably more complicated than necessary.

Differential Revision: https://phabricator.services.mozilla.com/D63914
This commit is contained in:
Markus Stange 2020-05-11 19:52:53 +00:00
parent 5c31341df6
commit 74da9ffadf
8 changed files with 240 additions and 164 deletions

View File

@ -49,6 +49,8 @@ declare namespace MockedExports {
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://devtools/client/performance-new/symbolication.jsm.js":
typeof import("resource://devtools/client/performance-new/symbolication.jsm.js");
"resource:///modules/PanelMultiView.jsm":
typeof import("resource:///modules/PanelMultiView.jsm");
}
@ -311,6 +313,11 @@ declare module "resource://devtools/client/performance-new/popup/background.jsm.
export = Background
}
declare module "resource://devtools/client/performance-new/symbolication.jsm.js" {
import * as PerfSymbolication from "devtools/client/performance-new/symbolication.jsm.js";
export = PerfSymbolication
}
declare module "resource:///modules/CustomizableUI.jsm" {
export = MockedExports.CustomizableUIJSM;
}

View File

@ -104,7 +104,10 @@ async function gInit(perfFront, pageContext, openRemoteDevTools) {
// The popup doesn't need to support remote symbol tables from the debuggee.
// Only get the symbols from this browser.
getSymbolTableGetter: () => getSymbolsFromThisBrowser,
getSymbolTableGetter: () => {
return (debugName, breakpadId) =>
getSymbolsFromThisBrowser(pageContext, debugName, breakpadId);
},
pageContext,
openRemoteDevTools,
})

View File

@ -31,6 +31,10 @@ const lazy = createLazyLoaders({
OS: () => ChromeUtils.import("resource://gre/modules/osfile.jsm"),
ProfilerGetSymbols: () =>
ChromeUtils.import("resource://gre/modules/ProfilerGetSymbols.jsm"),
PerfSymbolication: () =>
ChromeUtils.import(
"resource://devtools/client/performance-new/symbolication.jsm.js"
),
});
const TRANSFER_EVENT = "devtools:perf-html-transfer-profile";
@ -178,102 +182,8 @@ 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,
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),
];
}
/**
* @param {string} path
* @returns {Promise<boolean>}
*/
async function doesFileExistAtPath(path) {
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;
}
}
/**
* 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 {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<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 } = 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 } = 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.
}
}
}
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.
* Return a function `getSymbolTable` that calls getSymbolTableMultiModal with the
* right arguments.
*
* @param {MinimallyTypedGeckoProfile} profile - The raw profie (not gzipped).
* @param {() => string[]} getObjdirs - A function that returns an array of objdir paths
@ -285,48 +195,15 @@ function createMultiModalGetSymbolTableFn(profile, getObjdirs, perfFront) {
const libraryGetter = createLibraryMap(profile);
return async function getSymbolTable(debugName, breakpadId) {
const result = libraryGetter(debugName, breakpadId);
if (!result) {
const lib = libraryGetter(debugName, breakpadId);
if (!lib) {
throw new Error(
`Could not find the library for "${debugName}", "${breakpadId}".`
);
}
const { name, path, debugPath } = result;
if (await doesFileExistAtPath(path)) {
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).
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 = getObjdirs();
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 objdirs = getObjdirs();
const { getSymbolTableMultiModal } = lazy.PerfSymbolication();
return getSymbolTableMultiModal(lib, objdirs, perfFront);
};
}

View File

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

View File

@ -27,6 +27,7 @@ const AppConstants = ChromeUtils.import(
* @typedef {import("../@types/perf").RecordingStateFromPreferences} RecordingStateFromPreferences
* @typedef {import("../@types/perf").PopupBackgroundFeatures} PopupBackgroundFeatures
* @typedef {import("../@types/perf").SymbolTableAsTuple} SymbolTableAsTuple
* @typedef {import("../@types/perf").Library} Library
* @typedef {import("../@types/perf").PerformancePref} PerformancePref
* @typedef {import("../@types/perf").ProfilerWebChannel} ProfilerWebChannel
* @typedef {import("../@types/perf").MessageFromFrontend} MessageFromFrontend
@ -69,10 +70,12 @@ const lazy = createLazyLoaders({
require("devtools/shared/performance-new/recording-utils"),
CustomizableUI: () =>
ChromeUtils.import("resource:///modules/CustomizableUI.jsm"),
PerfSymbolication: () =>
ChromeUtils.import(
"resource://devtools/client/performance-new/symbolication.jsm.js"
),
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"
@ -133,27 +136,25 @@ const presets = {
/**
* This Map caches the symbols from the shared libraries.
* @type {Map<string, { path: string, debugPath: string }>}
* @type {Map<string, Library>}
*/
const symbolCache = new Map();
/**
* @param {PageContext} pageContext
* @param {string} debugName
* @param {string} breakpadId
*/
async function getSymbolsFromThisBrowser(debugName, breakpadId) {
async function getSymbolsFromThisBrowser(pageContext, debugName, breakpadId) {
if (symbolCache.size === 0) {
// Prime the symbols cache.
for (const lib of Services.profiler.sharedLibraries) {
symbolCache.set(`${lib.debugName}/${lib.breakpadId}`, {
path: lib.path,
debugPath: lib.debugPath,
});
symbolCache.set(`${lib.debugName}/${lib.breakpadId}`, lib);
}
}
const cachedLibInfo = symbolCache.get(`${debugName}/${breakpadId}`);
if (!cachedLibInfo) {
const cachedLib = symbolCache.get(`${debugName}/${breakpadId}`);
if (!cachedLib) {
throw new Error(
`The library ${debugName} ${breakpadId} is not in the ` +
"Services.profiler.sharedLibraries list, so the local path for it is not known " +
@ -164,27 +165,19 @@ async function getSymbolsFromThisBrowser(debugName, breakpadId) {
);
}
const { path, debugPath } = cachedLibInfo;
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 } = lazy.ProfilerGetSymbols();
return ProfilerGetSymbols.getSymbolTable(path, debugPath, breakpadId);
const lib = cachedLib;
const objdirs = getObjdirPrefValue(pageContext);
const { getSymbolTableMultiModal } = lazy.PerfSymbolication();
return getSymbolTableMultiModal(lib, objdirs);
}
/**
* This function is called directly by devtools/startup/DevToolsStartup.jsm when
* using the shortcut keys to capture a profile.
* @type {() => Promise<void>}
* @param {PageContext} pageContext
* @return {Promise<void>}
*/
async function captureProfile() {
async function captureProfile(pageContext) {
if (!Services.profiler.IsActive()) {
// The profiler is not active, ignore this shortcut.
return;
@ -203,7 +196,9 @@ async function captureProfile() {
);
const receiveProfile = lazy.BrowserModule().receiveProfile;
receiveProfile(profile, getSymbolsFromThisBrowser);
receiveProfile(profile, (debugName, breakpadId) => {
return getSymbolsFromThisBrowser(pageContext, debugName, breakpadId);
});
Services.profiler.StopProfiler();
}
@ -313,6 +308,15 @@ function getPrefPostfix(pageContext) {
}
}
/**
* @param {PageContext} pageContext
* @returns {string[]}
*/
function getObjdirPrefValue(pageContext) {
const postfix = getPrefPostfix(pageContext);
return _getArrayOfStringsHostPref(OBJDIRS_PREF + postfix);
}
/**
* @param {PageContext} pageContext
* @param {string[]} supportedFeatures
@ -323,7 +327,7 @@ function getRecordingPreferences(pageContext, supportedFeatures) {
// If you add a new preference here, please do not forget to update
// `revertRecordingPreferences` as well.
const objdirs = _getArrayOfStringsHostPref(OBJDIRS_PREF + postfix);
const objdirs = getObjdirPrefValue(pageContext);
const presetName = Services.prefs.getCharPref(PRESET_PREF + postfix);
// First try to get the values from a preset.

View File

@ -281,7 +281,7 @@ function addPopupEventHandlers(state, elements, view) {
});
addHandler(elements.stopAndCapture, "click", () => {
captureProfile();
captureProfile("aboutprofiling");
view.hidePopup();
});

View File

@ -0,0 +1,184 @@
/* 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";
const { createLazyLoaders } = ChromeUtils.import(
"resource://devtools/client/performance-new/typescript-lazy-load.jsm.js"
);
/**
* @typedef {import("./@types/perf").Library} Library
* @typedef {import("./@types/perf").PerfFront} PerfFront
* @typedef {import("./@types/perf").SymbolTableAsTuple} SymbolTableAsTuple
*/
const lazy = createLazyLoaders({
OS: () => ChromeUtils.import("resource://gre/modules/osfile.jsm"),
ProfilerGetSymbols: () =>
ChromeUtils.import("resource://gre/modules/ProfilerGetSymbols.jsm"),
});
/**
* @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,
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),
];
}
/**
* @param {string} path
* @returns {Promise<boolean>}
*/
async function doesFileExistAtPath(path) {
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;
}
}
/**
* 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 {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<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 } = 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 } = 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.
}
}
}
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.
*
* It's also built to handle symbolication requests for both Gecko libraries and
* system libraries.
*
* @param {Library} lib - The library to get symbols for.
* @param {string[]} objdirs - An array of objdir paths on the host machine that
* should be searched for relevant build artifacts.
* @param {PerfFront | undefined} perfFront - The perfFront for a remote debugging
* connection, or undefined when profiling this browser.
* @return {Promise<SymbolTableAsTuple>}
*/
async function getSymbolTableMultiModal(lib, objdirs, perfFront = undefined) {
const { name, debugName, path, debugPath, breakpadId } = lib;
try {
// First, try to find a binary with a matching file name and breakpadId
// in one of the manually specified objdirs. If the profiled build was
// compiled locally, and matches the build artifacts in the objdir, then
// this gives the best results because the objdir build always has full
// symbol information.
// This only works if the binary is one of the Gecko binaries and not
// a system library.
return await getSymbolTableFromLocalBinary(objdirs, name, breakpadId);
} catch (errorWhenCheckingObjdirs) {
// Couldn't find a matching build in one of the objdirs. Search elsewhere.
if (await doesFileExistAtPath(path)) {
const { ProfilerGetSymbols } = lazy.ProfilerGetSymbols();
// This profile was probably 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);
}
// No file exists at the path on this machine, 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).
if (!perfFront) {
// No remote connection - we really needed the file at path.
throw new Error(
`Could not obtain symbols for the library ${debugName} ${breakpadId} ` +
`because there was no file at the given path "${path}". Furthermore, ` +
`looking for symbols in the given objdirs failed: ${errorWhenCheckingObjdirs.message}`
);
}
// 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, the "debuggee" is never a Windows machine, which is why we don't
// need to pass the library's debugPath. (path and debugPath are always the
// same on non-Windows.)
return getSymbolTableFromDebuggee(perfFront, path, breakpadId);
}
}
// Provide an exports object for the JSM to be properly read by TypeScript.
/** @type {any} */ (this).module = {};
module.exports = {
getSymbolTableFromDebuggee,
getSymbolTableFromLocalBinary,
getSymbolTableMultiModal,
};
// Object.keys() confuses the linting which expects a static array expression.
// eslint-disable-next-line
var EXPORTED_SYMBOLS = Object.keys(module.exports);

View File

@ -857,7 +857,7 @@ DevToolsStartup.prototype = {
return;
}
case "profilerCapture": {
ProfilerPopupBackground.captureProfile();
ProfilerPopupBackground.captureProfile("aboutprofiling");
return;
}
}