зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
9f8c3a5124
Коммит
ecc072dac5
|
@ -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;
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче