Bug 1504101 - Add UI to the performance pane that lets the user pick an objdir for local builds. r=julienw

The picked objdirs are stored in a preference named devtools.performance.recording.objdirs
as a JSON-ified array of string paths.

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

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Markus Stange 2019-02-07 19:43:47 +00:00
Родитель 9f8c3a5124
Коммит ecc072dac5
10 изменённых файлов: 312 добавлений и 5 удалений

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

@ -9,6 +9,7 @@ const SYMBOL_TABLE_REQUEST_EVENT = "devtools:perf-html-request-symbol-table";
const SYMBOL_TABLE_RESPONSE_EVENT = "devtools:perf-html-reply-symbol-table";
const UI_BASE_URL_PREF = "devtools.performance.recording.ui-base-url";
const UI_BASE_URL_DEFAULT = "https://perf-html.io";
const OBJDIRS_PREF = "devtools.performance.recording.objdirs";
/**
* This file contains all of the privileged browser-specific functionality. This helps
@ -94,6 +95,31 @@ async function _getArrayOfStringsPref(preferenceFront, prefName, defaultValue) {
return defaultValue;
}
/**
* Similar to _getArrayOfStringsPref, but gets the pref from the host browser
* instance, *not* from the debuggee.
* Don't trust that the user has stored the correct value in preferences, or that it
* 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
*/
async function _getArrayOfStringsHostPref(prefName, defaultValue) {
let array;
try {
const text = Services.prefs.getStringPref(prefName, JSON.stringify(defaultValue));
array = JSON.parse(text);
} catch (error) {
return defaultValue;
}
if (Array.isArray(array) && array.every(feature => typeof feature === "string")) {
return array;
}
return defaultValue;
}
/**
* Attempt to get a int preference value from the debuggee.
*
@ -120,7 +146,7 @@ async function _getIntPref(preferenceFront, prefName, defaultValue) {
* of the object and how it gets defined.
*/
async function getRecordingPreferences(preferenceFront, defaultSettings = {}) {
const [ entries, interval, features, threads ] = await Promise.all([
const [ entries, interval, features, threads, objdirs ] = await Promise.all([
_getIntPref(
preferenceFront,
`devtools.performance.recording.entries`,
@ -141,16 +167,21 @@ async function getRecordingPreferences(preferenceFront, defaultSettings = {}) {
`devtools.performance.recording.threads`,
defaultSettings.threads
),
_getArrayOfStringsHostPref(
OBJDIRS_PREF,
defaultSettings.objdirs
),
]);
// The pref stores the value in usec.
const newInterval = interval / 1000;
return { entries, interval: newInterval, features, threads };
return { entries, interval: newInterval, features, threads, objdirs };
}
/**
* Take the recording settings, as defined by the getRecordingSettings selector, and
* persist them to preferences.
* persist them to preferences. Some of these prefs get persisted on the debuggee,
* and some of them on the host browser instance.
*
* @param {PreferenceFront} preferenceFront
* @param {object} defaultSettings See the getRecordingSettings selector for the shape
@ -175,6 +206,10 @@ async function setRecordingPreferences(preferenceFront, settings) {
`devtools.performance.recording.threads`,
JSON.stringify(settings.threads)
),
Services.prefs.setCharPref(
OBJDIRS_PREF,
JSON.stringify(settings.objdirs)
),
]);
}

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

@ -0,0 +1,90 @@
/* 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/. */
"use strict";
const { PureComponent } = require("devtools/client/shared/vendor/react");
const { div, input, select, option } = require("devtools/client/shared/vendor/react-dom-factories");
const { withCommonPathPrefixRemoved } = require("devtools/client/performance-new/utils");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
// A list of directories with add and remove buttons.
// Looks like this:
//
// +---------------------------------------------+
// | code/obj-m-android-opt |
// | code/obj-m-android-debug |
// | test/obj-m-test |
// | |
// +---------------------------------------------+
//
// [+] [-]
class DirectoryPicker extends PureComponent {
static get propTypes() {
return {
dirs: PropTypes.array.isRequired,
onAdd: PropTypes.func.isRequired,
onRemove: PropTypes.func.isRequired,
};
}
constructor(props) {
super(props);
this._listBox = null;
this._takeListBoxRef = this._takeListBoxRef.bind(this);
this._handleAddButtonClick = this._handleAddButtonClick.bind(this);
this._handleRemoveButtonClick = this._handleRemoveButtonClick.bind(this);
}
_takeListBoxRef(element) {
this._listBox = element;
}
_handleAddButtonClick() {
this.props.onAdd();
}
_handleRemoveButtonClick() {
if (this._listBox && this._listBox.selectedIndex !== -1) {
this.props.onRemove(this._listBox.selectedIndex);
}
}
render() {
const { dirs } = this.props;
const truncatedDirs = withCommonPathPrefixRemoved(dirs);
return [
select(
{
className: "perf-settings-dir-list",
size: "4",
ref: this._takeListBoxRef,
},
dirs.map((fullPath, i) => option(
{
className: "pref-settings-dir-list-item",
title: fullPath,
},
truncatedDirs[i]
))
),
div(
{ className: "perf-settings-dir-list-button-group" },
input({
type: "button",
value: "+",
title: "Add a directory",
onClick: this._handleAddButtonClick,
}),
input({
type: "button",
value: "-",
title: "Remove the selected directory from the list",
onClick: this._handleRemoveButtonClick,
}),
),
];
}
}
module.exports = DirectoryPicker;

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

@ -3,13 +3,17 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { PureComponent, createFactory } = require("devtools/client/shared/vendor/react");
const { div, details, summary, label, input, span, h2, section } = require("devtools/client/shared/vendor/react-dom-factories");
const { div, details, summary, label, input, span, h2, section, p } = require("devtools/client/shared/vendor/react-dom-factories");
const Range = createFactory(require("devtools/client/performance-new/components/Range"));
const DirectoryPicker = createFactory(require("devtools/client/performance-new/components/DirectoryPicker"));
const { makeExponentialScale, formatFileSize, calculateOverhead } = require("devtools/client/performance-new/utils");
const { connect } = require("devtools/client/shared/vendor/react-redux");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const actions = require("devtools/client/performance-new/store/actions");
const selectors = require("devtools/client/performance-new/store/selectors");
const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "FilePicker",
"@mozilla.org/filepicker;1", "nsIFilePicker");
// sizeof(double) + sizeof(char)
// http://searchfox.org/mozilla-central/rev/e8835f52eff29772a57dca7bcc86a9a312a23729/tools/profiler/core/ProfileEntry.h#73
@ -167,12 +171,14 @@ class Settings extends PureComponent {
features: PropTypes.array.isRequired,
threads: PropTypes.array.isRequired,
threadsString: PropTypes.string.isRequired,
objdirs: PropTypes.array.isRequired,
// DispatchProps
changeInterval: PropTypes.func.isRequired,
changeEntries: PropTypes.func.isRequired,
changeFeatures: PropTypes.func.isRequired,
changeThreads: PropTypes.func.isRequired,
changeObjdirs: PropTypes.func.isRequired,
};
}
@ -185,6 +191,8 @@ class Settings extends PureComponent {
this._handleThreadCheckboxChange = this._handleThreadCheckboxChange.bind(this);
this._handleFeaturesCheckboxChange = this._handleFeaturesCheckboxChange.bind(this);
this._handleAddObjdir = this._handleAddObjdir.bind(this);
this._handleRemoveObjdir = this._handleRemoveObjdir.bind(this);
this._setThreadTextFromInput = this._setThreadTextFromInput.bind(this);
this._handleThreadTextCleanup = this._handleThreadTextCleanup.bind(this);
this._renderThreadsColumns = this._renderThreadsColumns.bind(this);
@ -246,6 +254,27 @@ class Settings extends PureComponent {
}
}
_handleAddObjdir() {
const { objdirs, changeObjdirs } = this.props;
FilePicker.init(window, "Pick build directory", FilePicker.modeGetFolder);
FilePicker.open(rv => {
if (rv == FilePicker.returnOK) {
const path = FilePicker.file.path;
if (path && !objdirs.includes(path)) {
const newObjdirs = [...objdirs, path];
changeObjdirs(newObjdirs);
}
}
});
}
_handleRemoveObjdir(index) {
const { objdirs, changeObjdirs } = this.props;
const newObjdirs = [...objdirs];
newObjdirs.splice(index, 1);
changeObjdirs(newObjdirs);
}
_setThreadTextFromInput(event) {
this.setState({ temporaryThreadText: event.target.value });
}
@ -371,6 +400,35 @@ class Settings extends PureComponent {
);
}
_renderLocalBuildSection() {
const { objdirs } = this.props;
return details(
{ className: "perf-settings-details" },
summary(
{
className: "perf-settings-summary",
id: "perf-settings-local-build-summary",
},
"Local build:"
),
div(
{ className: "perf-settings-details-contents" },
div(
{ className: "perf-settings-details-contents-slider" },
p(null,
`If you're profiling a build that you have compiled yourself, on this
machine, please add your build's objdir to the list below so that
it can be used to look up symbol information.`),
DirectoryPicker({
dirs: objdirs,
onAdd: this._handleAddObjdir,
onRemove: this._handleRemoveObjdir,
}),
)
)
);
}
render() {
return section(
{ className: "perf-settings" },
@ -400,7 +458,8 @@ class Settings extends PureComponent {
onChange: this.props.changeEntries,
}),
this._renderThreads(),
this._renderFeatures()
this._renderFeatures(),
this._renderLocalBuildSection()
);
}
}
@ -445,6 +504,7 @@ function mapStateToProps(state) {
features: selectors.getFeatures(state),
threads: selectors.getThreads(state),
threadsString: selectors.getThreadsString(state),
objdirs: selectors.getObjdirs(state),
};
}
@ -453,6 +513,7 @@ const mapDispatchToProps = {
changeEntries: actions.changeEntries,
changeFeatures: actions.changeFeatures,
changeThreads: actions.changeThreads,
changeObjdirs: actions.changeObjdirs,
};
module.exports = connect(mapStateToProps, mapDispatchToProps)(Settings);

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

@ -5,6 +5,7 @@
DevToolsModules(
'Description.js',
'DirectoryPicker.js',
'Perf.js',
'Range.js',
'RecordingButton.js',

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

@ -92,6 +92,15 @@ exports.changeThreads = threads => _dispatchAndUpdatePreferences({
threads,
});
/**
* Updates the recording settings for the objdirs.
* @param {array} objdirs
*/
exports.changeObjdirs = objdirs => _dispatchAndUpdatePreferences({
type: "CHANGE_OBJDIRS",
objdirs,
});
/**
* Receive the values to intialize the store. See the reducer for what values
* are expected.

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

@ -113,6 +113,21 @@ function threads(state = ["GeckoMain", "Compositor"], action) {
}
}
/**
* The current objdirs list.
* @param {array of strings} state
*/
function objdirs(state = [], action) {
switch (action.type) {
case "CHANGE_OBJDIRS":
return action.objdirs;
case "INITIALIZE_STORE":
return action.recordingSettingsFromPreferences.objdirs;
default:
return state;
}
}
/**
* These are all the values used to initialize the profiler. They should never change
* once added to the store.
@ -145,5 +160,6 @@ module.exports = combineReducers({
entries,
features,
threads,
objdirs,
initializedValues,
});

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

@ -11,6 +11,7 @@ const getEntries = state => state.entries;
const getFeatures = state => state.features;
const getThreads = state => state.threads;
const getThreadsString = state => getThreads(state).join(",");
const getObjdirs = state => state.objdirs;
const getRecordingSettings = state => {
return {
@ -18,6 +19,7 @@ const getRecordingSettings = state => {
interval: getInterval(state),
features: getFeatures(state),
threads: getThreads(state),
objdirs: getObjdirs(state),
};
};
@ -43,6 +45,7 @@ module.exports = {
getFeatures,
getThreads,
getThreadsString,
getObjdirs,
getRecordingSettings,
getInitializedValues,
getPerfFront,

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

@ -3,6 +3,8 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { OS } = require("resource://gre/modules/osfile.jsm");
const recordingState = {
// The initial state before we've queried the PerfActor
NOT_YET_KNOWN: "not-yet-known",
@ -174,10 +176,68 @@ function calculateOverhead(interval, bufferSize, features) {
);
}
/**
* Given an array of absolute paths on the file system, return an array that
* doesn't contain the common prefix of the paths; in other words, if all paths
* share a common ancestor directory, cut off the path to that ancestor
* directory and only leave the path components that differ.
* This makes some lists look a little nicer. For example, this turns the list
* ["/Users/foo/code/obj-m-android-opt", "/Users/foo/code/obj-m-android-debug"]
* into the list ["obj-m-android-opt", "obj-m-android-debug"].
* @param {array of string} pathArray The array of absolute paths.
* @returns {array of string} A new array with the described adjustment.
*/
function withCommonPathPrefixRemoved(pathArray) {
if (pathArray.length === 0) {
return [];
}
const splitPaths = pathArray.map(path => OS.Path.split(path));
if (!splitPaths.every(sp => sp.absolute)) {
// We're expecting all paths to be absolute, so this is an unexpected case,
// return the original array.
return pathArray;
}
const [firstSplitPath, ...otherSplitPaths] = splitPaths;
if ("winDrive" in firstSplitPath) {
const winDrive = firstSplitPath.winDrive;
if (!otherSplitPaths.every(sp => sp.winDrive === winDrive)) {
return pathArray;
}
} else if (otherSplitPaths.some(sp => ("winDrive" in sp))) {
// Inconsistent winDrive property presence, bail out.
return pathArray;
}
// At this point we're either not on Windows or all paths are on the same
// winDrive. And all paths are absolute.
// Find the common prefix. Start by assuming the entire path except for the
// last folder is shared.
const prefix = firstSplitPath.components.slice(0, -1);
for (const sp of otherSplitPaths) {
prefix.length = Math.min(prefix.length, sp.components.length - 1);
for (let i = 0; i < prefix.length; i++) {
if (prefix[i] !== sp.components[i]) {
prefix.length = i;
break;
}
}
}
if (prefix.length === 0 || (prefix.length === 1 && prefix[0] === "")) {
// There is no shared prefix.
// We treat a prefix of [""] as "no prefix", too: Absolute paths on
// non-Windows start with a slash, so OS.Path.split(path) always returns an
// array whose first element is the empty string on those platforms.
// Stripping off a prefix of [""] from the split paths would simply remove
// the leading slash from the un-split paths, which is not useful.
return pathArray;
}
return splitPaths.map(sp => OS.Path.join(...sp.components.slice(prefix.length)));
}
module.exports = {
formatFileSize,
makeExponentialScale,
scaleRangeWithClamping,
calculateOverhead,
recordingState,
withCommonPathPrefixRemoved,
};

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

@ -155,6 +155,11 @@ pref("devtools.performance.ui.experimental", false);
// This isn't exposed directly to the user.
pref("devtools.performance.recording.ui-base-url", "https://perf-html.io");
// A JSON array of strings, where each string is a file path to an objdir on
// the host machine. This is used in order to look up symbol information from
// build artifacts of local builds.
pref("devtools.performance.recording.objdirs", "[]");
// The default cache UI setting
pref("devtools.cache.disabled", false);

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

@ -218,3 +218,30 @@
.perf-settings-subtext {
font-weight: bold;
}
.perf-settings-dir-list {
box-sizing: border-box;
width: 100%;
height: 100px;
border: 1px solid var(--grey-30);
padding: 0;
overflow-y: auto;
background-color: white;
}
.pref-settings-dir-list-item {
padding: 3px 5px;
}
.pref-settings-dir-list-item::before {
content: url(chrome://devtools/skin/images/folder.svg);
display: inline-block;
width: 12px;
height: 12px;
margin-inline-end: 4px;
vertical-align: -2px;
}
.perf-settings-dir-list-button-group {
padding: 4px 2px;
}