--- a/devtools/client/performance-new/components/Perf.js
+++ b/devtools/client/performance-new/components/Perf.js
@@ -1,16 +1,18 @@
/* 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, button } = require("devtools/client/shared/vendor/react-dom-factories");
+const { PureComponent, createFactory } = require("devtools/client/shared/vendor/react");
+const { div, button, p, span, img } = require("devtools/client/shared/vendor/react-dom-factories");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const PerfSettings = createFactory(require("devtools/client/performance-new/components/PerfSettings.js"));
+const { openLink } = require("devtools/client/shared/link");
/**
* The recordingState is one of the following:
**/
// The initial state before we've queried the PerfActor
const NOT_YET_KNOWN = "not-yet-known";
// The profiler is available, we haven't started recording yet.
@@ -27,16 +29,17 @@ const RECORDING = "recording";
// Some other code with access to the profiler started it.
const OTHER_IS_RECORDING = "other-is-recording";
// Profiling is not available when in private browsing mode.
const LOCKED_BY_PRIVATE_BROWSING = "locked-by-private-browsing";
class Perf extends PureComponent {
static get propTypes() {
return {
+ toolbox: PropTypes.object.isRequired,
perfFront: PropTypes.object.isRequired,
receiveProfile: PropTypes.func.isRequired
};
}
constructor(props) {
super(props);
this.state = {
@@ -47,16 +50,18 @@ class Perf extends PureComponent {
};
this.startRecording = this.startRecording.bind(this);
this.getProfileAndStopProfiler = this.getProfileAndStopProfiler.bind(this);
this.stopProfilerAndDiscardProfile = this.stopProfilerAndDiscardProfile.bind(this);
this.handleProfilerStarting = this.handleProfilerStarting.bind(this);
this.handleProfilerStopping = this.handleProfilerStopping.bind(this);
this.handlePrivateBrowsingStarting = this.handlePrivateBrowsingStarting.bind(this);
this.handlePrivateBrowsingEnding = this.handlePrivateBrowsingEnding.bind(this);
+ this.settingsComponentCreated = this.settingsComponentCreated.bind(this);
+ this.handleLinkClick = this.handleLinkClick.bind(this);
}
componentDidMount() {
const { perfFront } = this.props;
// Ask for the initial state of the profiler.
Promise.all([
perfFront.isActive(),
@@ -109,16 +114,25 @@ class Perf extends PureComponent {
this.props.perfFront.stopProfilerAndDiscardProfile();
break;
default:
throw new Error("Unhandled recording state.");
}
}
+ /**
+ * Store a reference to the settings component. This gives the <Perf> component
+ * access to the `.getRecordingSettings()` method. At this time the recording panel
+ * is not doing much state management, so this avoid the overhead of redux.
+ */
+ settingsComponentCreated(settings) {
+ this.settings = settings;
+ }
+
getRecordingStateForTesting() {
return this.state.recordingState;
}
handleProfilerStarting() {
switch (this.state.recordingState) {
case NOT_YET_KNOWN:
// We couldn't have started it yet, so it must have been someone
@@ -230,66 +244,74 @@ class Perf extends PureComponent {
// be the only logical state to go into.
this.setState({
recordingState: AVAILABLE_TO_RECORD,
recordingUnexpectedlyStopped: false
});
}
startRecording() {
+ const settings = this.settings;
+ if (!settings) {
+ console.error("Expected the PerfSettings panel to be rendered and available.");
+ return;
+ }
this.setState({
recordingState: REQUEST_TO_START_RECORDING,
// Reset this error state since it's no longer valid.
recordingUnexpectedlyStopped: false,
});
- this.props.perfFront.startProfiler();
+ this.props.perfFront.startProfiler(
+ // Pull out the recording settings from the child component. This approach avoids
+ // using Redux as a state manager.
+ settings.getRecordingSettings()
+ );
}
async getProfileAndStopProfiler() {
this.setState({ recordingState: REQUEST_TO_GET_PROFILE_AND_STOP_PROFILER });
const profile = await this.props.perfFront.getProfileAndStopProfiler();
this.setState({ recordingState: AVAILABLE_TO_RECORD });
console.log("getProfileAndStopProfiler");
this.props.receiveProfile(profile);
}
stopProfilerAndDiscardProfile() {
this.setState({ recordingState: REQUEST_TO_STOP_PROFILER });
this.props.perfFront.stopProfilerAndDiscardProfile();
}
- render() {
+ renderButton() {
const { recordingState, isSupportedPlatform } = this.state;
- // Handle the cases of platform support.
- switch (isSupportedPlatform) {
- case null:
- // We don't know yet if this is a supported platform, wait for a response.
- return null;
- case false:
- return renderButton({
- label: "Start recording",
- disabled: true,
- additionalMessage: "Your platform is not supported. The Gecko Profiler only " +
- "supports Tier-1 platforms."
- });
- case true:
- // Continue on and render the panel.
- break;
+ if (!isSupportedPlatform) {
+ return renderButton({
+ label: "Start recording",
+ disabled: true,
+ additionalMessage: "Your platform is not supported. The Gecko Profiler only " +
+ "supports Tier-1 platforms."
+ });
}
// TODO - L10N all of the messages. Bug 1418056
switch (recordingState) {
case NOT_YET_KNOWN:
return null;
case AVAILABLE_TO_RECORD:
return renderButton({
onClick: this.startRecording,
- label: "Start recording",
+ label: span(
+ null,
+ img({
+ className: "perf-button-image",
+ src: "chrome://devtools/skin/images/tool-profiler.svg"
+ }),
+ "Start recording",
+ ),
additionalMessage: this.state.recordingUnexpectedlyStopped
? div(null, "The recording was stopped by another tool.")
: null
});
case REQUEST_TO_STOP_PROFILER:
return renderButton({
label: "Stopping the recording",
@@ -324,26 +346,78 @@ class Perf extends PureComponent {
additionalMessage: `The profiler is disabled when Private Browsing is enabled.
Close all Private Windows to re-enable the profiler`
});
default:
throw new Error("Unhandled recording state");
}
}
+
+ handleLinkClick(event) {
+ openLink(event.target.value, this.props.toolbox);
+ }
+
+ render() {
+ const { isSupportedPlatform } = this.state;
+
+ if (isSupportedPlatform === null) {
+ // We don't know yet if this is a supported platform, wait for a response.
+ return null;
+ }
+
+ return div(
+ { className: "perf" },
+ this.renderButton(),
+ PerfSettings({ ref: this.settingsComponentCreated }),
+ div(
+ { className: "perf-description" },
+ p(null,
+ "This new recording panel is a bit different from the existing " +
+ "performance panel. It records the entire browser, and then opens up " +
+ "and shares the profile with ",
+ button(
+ // Implement links as buttons to avoid any risk of loading the link in the
+ // the panel.
+ {
+ className: "perf-external-link",
+ value: "https://perf-html.io",
+ onClick: this.handleLinkClick
+ },
+ "perf-html.io"
+ ),
+ ", a Mozilla performance analysis tool."
+ ),
+ p(null,
+ "This is still a prototype. Join along or file bugs at: ",
+ button(
+ // Implement links as buttons to avoid any risk of loading the link in the
+ // the panel.
+ {
+ className: "perf-external-link",
+ value: "https://github.com/devtools-html/perf.html",
+ onClick: this.handleLinkClick
+ },
+ "github.com/devtools-html/perf.html"
+ ),
+ "."
+ )
+ ),
+ );
+ }
}
module.exports = Perf;
function renderButton(props) {
const { disabled, label, onClick, additionalMessage } = props;
const nbsp = "\u00A0";
return div(
- { className: "perf" },
+ { className: "perf-button-container" },
div({ className: "perf-additional-message" }, additionalMessage || nbsp),
div(
null,
button(
{
className: "devtools-button perf-button",
"data-standalone": true,
disabled,
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/components/PerfSettings.js
@@ -0,0 +1,409 @@
+/* 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, 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 Range = createFactory(require("devtools/client/performance-new/components/Range"));
+const { makeExponentialScale, formatFileSize, calculateOverhead } = require("devtools/client/performance-new/utils");
+
+// sizeof(double) + sizeof(char)
+// http://searchfox.org/mozilla-central/rev/e8835f52eff29772a57dca7bcc86a9a312a23729/tools/profiler/core/ProfileEntry.h#73
+const PROFILE_ENTRY_SIZE = 9;
+
+const NOTCHES = Array(22).fill("discrete-level-notch");
+
+const threadColumns = [
+ [
+ {
+ name: "GeckoMain",
+ title: "The main processes for both the parent process, and content processes"
+ },
+ {
+ name: "Compositor",
+ title: "Composites together different painted elements on the page."
+ },
+ {
+ name: "DOM Worker",
+ title: "This handle both web workers and service workers"
+ },
+ {
+ name: "Renderer",
+ title: "When WebRender is enabled, the thread that executes OpenGL calls"
+ },
+ ],
+ [
+ {
+ name: "RenderBackend",
+ title: "The WebRender RenderBackend thread"
+ },
+ {
+ name: "PaintWorker",
+ title: "When off-main-thread painting is enabled, the thread on which " +
+ "painting happens"
+ },
+ {
+ name: "StyleThread",
+ title: "Style computation is split into multiple threads"
+ },
+ {
+ name: "Socket Thread",
+ title: "The thread where networking code runs any blocking socket calls"
+ },
+ ],
+ [
+ {
+ name: "StreamTrans",
+ title: "TODO"
+ },
+ {
+ name: "ImgDecoder",
+ title: "Image decoding threads"
+ },
+ {
+ name: "DNS Resolver",
+ title: "DNS resolution happens on this thread"
+ },
+ ]
+];
+
+const featureCheckboxes = [
+ {
+ name: "Native Stacks",
+ value: "stackwalk",
+ title: "Record native stacks (C++ and Rust). This is not available on all platforms.",
+ recommended: true
+ },
+ {
+ name: "JavaScript",
+ value: "js",
+ title: "Record JavaScript stack information, and interleave it with native stacks.",
+ recommended: true
+ },
+ {
+ name: "Java",
+ value: "java",
+ title: "Profile Java code (Android only)."
+ },
+ {
+ name: "Native Leaf Stack",
+ value: "leaf",
+ title: "Record the native memory address of the leaf-most stack. This could be " +
+ "useful on platforms that do not support stack walking."
+ },
+ {
+ name: "Main Thread IO",
+ value: "mainthreadio",
+ title: "Record main thread I/O markers."
+ },
+ {
+ name: "Memory",
+ value: "memory",
+ title: "Add memory measurements to the samples, this includes resident set size " +
+ "(RSS) and unique set size (USS)."
+ },
+ {
+ name: "Privacy",
+ value: "privacy",
+ title: "Remove some potentially user-identifiable information."
+ },
+ {
+ name: "JIT Optimizations",
+ value: "trackopts",
+ title: "Track JIT optimizations in the JS engine."
+ },
+ {
+ name: "TaskTracer",
+ value: "tasktracer",
+ title: "Enable TaskTracer (Experimental, requires custom build.)"
+ },
+];
+
+/**
+ * This component manages the settings for recording a performance profile. In addition
+ * to rendering the UI, it also manages the state of the settings. In order to not
+ * introduce the complexity of adding Redux to a relatively simple UI, this
+ * component expects to be accessed via the `ref`, and then calling
+ * `settings.getRecordingSettings()` to get out the settings. If the recording panel
+ * takes on new responsibilities, then this decision should be revisited.
+ */
+class PerfSettings extends PureComponent {
+ static get propTypes() {
+ return {};
+ }
+
+ constructor(props) {
+ super(props);
+ // Right now the defaults are reset every time the panel is opened. These should
+ // be persisted between sessions. See Bug 1453014.
+ this.state = {
+ interval: 1,
+ entries: 10000000, // 90MB
+ features: {
+ js: true,
+ stackwalk: true,
+ },
+ threads: "GeckoMain,Compositor",
+ threadListFocused: false,
+ };
+ this._handleThreadCheckboxChange = this._handleThreadCheckboxChange.bind(this);
+ this._handleFeaturesCheckboxChange = this._handleFeaturesCheckboxChange.bind(this);
+ this._handleThreadTextChange = this._handleThreadTextChange.bind(this);
+ this._handleThreadTextCleanup = this._handleThreadTextCleanup.bind(this);
+ this._renderThreadsColumns = this._renderThreadsColumns.bind(this);
+ this._onChangeInterval = this._onChangeInterval.bind(this);
+ this._onChangeEntries = this._onChangeEntries.bind(this);
+ this._intervalExponentialScale = makeExponentialScale(0.01, 100);
+ this._entriesExponentialScale = makeExponentialScale(100000, 100000000);
+ }
+
+ getRecordingSettings() {
+ const features = [];
+ for (const [name, isSet] of Object.entries(this.state.features)) {
+ if (isSet) {
+ features.push(name);
+ }
+ }
+ return {
+ entries: this.state.entries,
+ interval: this.state.interval,
+ features,
+ threads: _threadStringToList(this.state.threads)
+ };
+ }
+
+ _renderNotches() {
+ const { interval, entries, features } = this.state;
+ const overhead = calculateOverhead(interval, entries, features);
+ const notchCount = 22;
+ const notches = [];
+ for (let i = 0; i < notchCount; i++) {
+ const active = i <= Math.round(overhead * (NOTCHES.length - 1))
+ ? "active" : "inactive";
+
+ let level = "normal";
+ if (i > 16) {
+ level = "critical";
+ } else if (i > 10) {
+ level = "warning";
+ }
+ notches.push(
+ div({
+ key: i,
+ className:
+ `perf-settings-notch perf-settings-notch-${level} ` +
+ `perf-settings-notch-${active}`
+ })
+ );
+ }
+ return notches;
+ }
+
+ _handleThreadCheckboxChange(event) {
+ const { checked, value } = event.target;
+
+ this.setState(state => {
+ let threadsList = _threadStringToList(state.threads);
+ if (checked) {
+ if (!threadsList.includes(value)) {
+ threadsList.push(value);
+ }
+ } else {
+ threadsList = threadsList.filter(thread => thread !== value);
+ }
+ return { threads: threadsList.join(",") };
+ });
+ }
+
+ _handleFeaturesCheckboxChange(event) {
+ const { checked, value } = event.target;
+
+ this.setState(state => ({
+ features: {...state.features, [value]: checked}
+ }));
+ }
+
+ _handleThreadTextChange(event) {
+ this.setState({ threads: event.target.value });
+ }
+
+ _handleThreadTextCleanup() {
+ this.setState(state => {
+ const threadsList = _threadStringToList(state.threads);
+ return { threads: threadsList.join(",") };
+ });
+ }
+
+ _renderThreadsColumns(threads, index) {
+ return div(
+ { className: "perf-settings-thread-column", key: index },
+ threads.map(({name, title}) => label(
+ {
+ className: "perf-settings-checkbox-label",
+ key: name,
+ title
+ },
+ input({
+ className: "perf-settings-checkbox",
+ type: "checkbox",
+ value: name,
+ checked: this.state.threads.includes(name),
+ onChange: this._handleThreadCheckboxChange
+ }),
+ name
+ ))
+ );
+ }
+
+ _renderThreads() {
+ return details(
+ { className: "perf-settings-details" },
+ summary({ className: "perf-settings-summary" }, "Threads:"),
+ // Contain the overflow of the slide down animation with the first div.
+ div(
+ { className: "perf-settings-details-contents" },
+ // Provide a second <div> element for the contents of the slide down animation.
+ div(
+ { className: "perf-settings-details-contents-slider" },
+ div(
+ { className: "perf-settings-thread-columns" },
+ threadColumns.map(this._renderThreadsColumns),
+ ),
+ div(
+ { className: "perf-settings-row" },
+ label(
+ {
+ className: "perf-settings-text-label",
+ title: "These thread names are a comma separated list that is used to " +
+ "enable profiling of the threads in the profiler. The name needs to " +
+ "be only a partial match of the thread name to be included. It " +
+ "is whitespace sensitive."
+ },
+ div({}, "Add custom threads by name:"),
+ input({
+ className: "perf-settings-text-input",
+ type: "text",
+ value: this.state.threads,
+ onChange: this._handleThreadTextChange,
+ onBlur: this._handleThreadTextCleanup,
+ })
+ )
+ )
+ )
+ )
+ );
+ }
+
+ _renderFeatures() {
+ return details(
+ { className: "perf-settings-details" },
+ summary({ className: "perf-settings-summary" }, "Features:"),
+ div(
+ { className: "perf-settings-details-contents" },
+ div(
+ { className: "perf-settings-details-contents-slider" },
+ featureCheckboxes.map(({name, value, title, recommended}) => label(
+ {
+ className: "perf-settings-checkbox-label perf-settings-feature-label",
+ key: value,
+ },
+ input({
+ className: "perf-settings-checkbox",
+ type: "checkbox",
+ value,
+ checked: this.state.features[value],
+ onChange: this._handleFeaturesCheckboxChange
+ }),
+ div({ className: "perf-settings-feature-name" }, name),
+ div(
+ { className: "perf-settings-feature-title" },
+ title,
+ recommended
+ ? span(
+ { className: "perf-settings-subtext" },
+ " (Recommended on by default.)"
+ )
+ : null
+ )
+ ))
+ )
+ )
+ );
+ }
+
+ _onChangeInterval(interval) {
+ this.setState({ interval });
+ }
+
+ _onChangeEntries(entries) {
+ this.setState({ entries });
+ }
+
+ render() {
+ return section(
+ { className: "perf-settings" },
+ h2({ className: "perf-settings-title" }, "Recording Settings"),
+ div(
+ { className: "perf-settings-row" },
+ label({ className: "perf-settings-label" }, "Overhead:"),
+ div(
+ { className: "perf-settings-value perf-settings-notches" },
+ this._renderNotches()
+ )
+ ),
+ Range({
+ label: "Sampling interval:",
+ value: this.state.interval,
+ id: "perf-range-interval",
+ scale: this._intervalExponentialScale,
+ display: _intervalTextDisplay,
+ onChange: this._onChangeInterval
+ }),
+ Range({
+ label: "Buffer size:",
+ value: this.state.entries,
+ id: "perf-range-entries",
+ scale: this._entriesExponentialScale,
+ display: _entriesTextDisplay,
+ onChange: this._onChangeEntries
+ }),
+ this._renderThreads(),
+ this._renderFeatures()
+ );
+ }
+}
+
+/**
+ * Clean up the thread list string into a list of values.
+ * @param string threads, comma separated values.
+ * @return Array list of thread names
+ */
+function _threadStringToList(threads) {
+ return threads
+ // Split on commas
+ .split(",")
+ // Clean up any extraneous whitespace
+ .map(string => string.trim())
+ // Filter out any blank strings
+ .filter(string => string);
+}
+
+/**
+ * Format the interval number for display.
+ * @param {number} value
+ * @return {string}
+ */
+function _intervalTextDisplay(value) {
+ return `${value} ms`;
+}
+
+/**
+ * Format the entries number for display.
+ * @param {number} value
+ * @return {string}
+ */
+function _entriesTextDisplay(value) {
+ return formatFileSize(value * PROFILE_ENTRY_SIZE);
+}
+
+module.exports = PerfSettings;
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/components/Range.js
@@ -0,0 +1,69 @@
+/* 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 PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const { div, input, label } = require("devtools/client/shared/vendor/react-dom-factories");
+
+class Range extends PureComponent {
+ static get propTypes() {
+ return {
+ value: PropTypes.number.isRequired,
+ label: PropTypes.string.isRequired,
+ id: PropTypes.string.isRequired,
+ scale: PropTypes.object.isRequired,
+ onChange: PropTypes.func.isRequired,
+ display: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.handleInput = this.handleInput.bind(this);
+ }
+
+ handleInput(e) {
+ e.preventDefault();
+ const { scale, onChange } = this.props;
+ const frac = e.target.value / 100;
+ onChange(scale.fromFractionToSingleDigitValue(frac));
+ }
+
+ render() {
+ const { label: labelText, scale, id, value, display } = this.props;
+ return (
+ div(
+ { className: "perf-settings-row" },
+ label(
+ {
+ className: "perf-settings-label",
+ htmlFor: id
+ },
+ labelText
+ ),
+ div(
+ { className: "perf-settings-value" },
+ div(
+ { className: "perf-settings-range-input" },
+ input({
+ type: "range",
+ className: `perf-settings-range-input-el`,
+ min: "0",
+ max: "100",
+ value: scale.fromValueToFraction(value) * 100,
+ onChange: this.handleInput,
+ id,
+ })
+ ),
+ div(
+ { className: `perf-settings-range-value`},
+ display(value)
+ )
+ )
+ )
+ );
+ }
+}
+
+module.exports = Range;
--- a/devtools/client/performance-new/components/moz.build
+++ b/devtools/client/performance-new/components/moz.build
@@ -1,8 +1,10 @@
# vim: set filetype=python:
# 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/.
DevToolsModules(
'Perf.js',
+ 'PerfSettings.js',
+ 'Range.js',
)
--- a/devtools/client/performance-new/initializer.js
+++ b/devtools/client/performance-new/initializer.js
@@ -14,20 +14,22 @@ const { require } = BrowserLoaderModule.
const Perf = require("devtools/client/performance-new/components/Perf");
const Services = require("Services");
const { render, unmountComponentAtNode } = require("devtools/client/shared/vendor/react-dom");
const { createElement } = require("devtools/client/shared/vendor/react");
/**
* Perform a simple initialization on the panel. Hook up event listeners.
*
+ * @param toolbox - The toolbox
* @param perfFront - The Perf actor's front. Used to start and stop recordings.
*/
-function gInit(perfFront) {
+function gInit(toolbox, perfFront) {
const props = {
+ toolbox,
perfFront,
receiveProfile: profile => {
// Open up a new tab and send a message with the profile.
let browser = top.gBrowser;
if (!browser) {
// Current isn't browser window. Looking for the recent browser.
const win = Services.wm.getMostRecentWindow("navigator:browser");
if (!win) {
--- a/devtools/client/performance-new/moz.build
+++ b/devtools/client/performance-new/moz.build
@@ -4,14 +4,15 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DIRS += [
'components',
]
DevToolsModules(
'panel.js',
+ 'utils.js',
)
MOCHITEST_CHROME_MANIFESTS += ['test/chrome/chrome.ini']
with Files('**'):
BUG_COMPONENT = ('Firefox', 'Developer Tools: Performance Tools (Profiler/Timeline)')
--- a/devtools/client/performance-new/panel.js
+++ b/devtools/client/performance-new/panel.js
@@ -30,17 +30,17 @@ class PerformancePanel {
this.panelWin.gToolbox = this.toolbox;
this.panelWin.gTarget = this.target;
const rootForm = await this.target.root;
const perfFront = new PerfFront(this.target.client, rootForm);
this.isReady = true;
this.emit("ready");
- this.panelWin.gInit(perfFront);
+ this.panelWin.gInit(this.toolbox, perfFront);
return this;
}
// DevToolPanel API:
get target() {
return this.toolbox.target;
}
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance-new/utils.js
@@ -0,0 +1,162 @@
+/* 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 UNITS = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
+
+/**
+ * Linearly interpolate between values.
+ * https://en.wikipedia.org/wiki/Linear_interpolation
+ *
+ * @param {number} frac - Value ranged 0 - 1 to interpolate between the range
+ * start and range end.
+ * @param {number} rangeState - The value to start from.
+ * @param {number} rangeEnd - The value to interpolate to.
+ * @returns {number}
+ */
+function lerp(frac, rangeStart, rangeEnd) {
+ return (1 - frac) * rangeStart + frac * rangeEnd;
+}
+
+/**
+ * Make sure a value is clamped between a min and max value.
+ *
+ * @param {number} val - The value to clamp.
+ * @param {number} min - The minimum value.
+ * @returns {number}
+ */
+function clamp(val, min, max) {
+ return Math.max(min, Math.min(max, val));
+}
+
+/**
+ * Formats a file size.
+ * @param {number} num - The number (in bytes) to format.
+ * @returns {string} e.g. "10 B", "100 MB"
+ */
+function formatFileSize(num) {
+ if (!Number.isFinite(num)) {
+ throw new TypeError(`Expected a finite number, got ${typeof num}: ${num}`);
+ }
+
+ const neg = num < 0;
+
+ if (neg) {
+ num = -num;
+ }
+
+ if (num < 1) {
+ return (neg ? "-" : "") + num + " B";
+ }
+
+ const exponent = Math.min(
+ Math.floor(Math.log(num) / Math.log(1000)),
+ UNITS.length - 1
+ );
+ const numStr = Number((num / Math.pow(1000, exponent)).toPrecision(3));
+ const unit = UNITS[exponent];
+
+ return (neg ? "-" : "") + numStr + " " + unit;
+}
+
+/**
+ * Creates numbers that scale exponentially.
+ *
+ * @param {number} rangeStart
+ * @param {number} rangeEnd
+ */
+function makeExponentialScale(rangeStart, rangeEnd) {
+ const startExp = Math.log(rangeStart);
+ const endExp = Math.log(rangeEnd);
+ const fromFractionToValue = frac =>
+ Math.exp((1 - frac) * startExp + frac * endExp);
+ const fromValueToFraction = value =>
+ (Math.log(value) - startExp) / (endExp - startExp);
+ const fromFractionToSingleDigitValue = frac => {
+ return +fromFractionToValue(frac).toPrecision(1);
+ };
+ return {
+ // Takes a number ranged 0-1 and returns it within the range.
+ fromFractionToValue,
+ // Takes a number in the range, and returns a value between 0-1
+ fromValueToFraction,
+ // Takes a number ranged 0-1 and returns a value in the range, but with
+ // a single digit value.
+ fromFractionToSingleDigitValue,
+ };
+}
+
+/**
+ * Scale a source range to a destination range, but clamp it within the
+ * destination range.
+ * @param {number} val - The source range value to map to the destination range,
+ * @param {number} sourceRangeStart,
+ * @param {number} sourceRangeEnd,
+ * @param {number} destRangeStart,
+ * @param {number} destRangeEnd
+ */
+function scaleRangeWithClamping(
+ val,
+ sourceRangeStart,
+ sourceRangeEnd,
+ destRangeStart,
+ destRangeEnd
+) {
+ const frac = clamp(
+ (val - sourceRangeStart) / (sourceRangeEnd - sourceRangeStart),
+ 0,
+ 1
+ );
+ return lerp(frac, destRangeStart, destRangeEnd);
+}
+
+/**
+ * Use some heuristics to guess at the overhead of the recording settings.
+ * @param {number} interval
+ * @param {number} bufferSize
+ * @param {object} features - Map of the feature name to a boolean.
+ */
+function calculateOverhead(interval, bufferSize, features) {
+ const overheadFromSampling =
+ scaleRangeWithClamping(
+ Math.log(interval),
+ Math.log(0.05),
+ Math.log(1),
+ 1,
+ 0
+ ) +
+ scaleRangeWithClamping(
+ Math.log(interval),
+ Math.log(1),
+ Math.log(100),
+ 0.1,
+ 0
+ );
+ const overheadFromBuffersize = scaleRangeWithClamping(
+ Math.log(bufferSize),
+ Math.log(10),
+ Math.log(1000000),
+ 0,
+ 0.1
+ );
+ const overheadFromStackwalk = features.stackwalk ? 0.05 : 0;
+ const overheadFromJavaScrpt = features.js ? 0.05 : 0;
+ const overheadFromTaskTracer = features.tasktracer ? 0.05 : 0;
+ return clamp(
+ overheadFromSampling +
+ overheadFromBuffersize +
+ overheadFromStackwalk +
+ overheadFromJavaScrpt +
+ overheadFromTaskTracer,
+ 0,
+ 1
+ );
+}
+
+module.exports = {
+ formatFileSize,
+ makeExponentialScale,
+ scaleRangeWithClamping,
+ calculateOverhead
+};
--- a/devtools/client/themes/perf.css
+++ b/devtools/client/themes/perf.css
@@ -12,12 +12,203 @@
}
.devtools-button.perf-button {
padding: 5px;
margin: auto;
font-size: 120%;
}
+.perf-button-image {
+ vertical-align: text-top;
+ padding-inline-end: 4px;
+}
+
.perf-additional-message {
margin: 10px;
margin-top: 65px;
}
+
+.perf > * {
+ max-width: 440px;
+}
+
+.perf-description {
+ line-height: 1.4;
+}
+
+.perf-external-link {
+ margin: 0;
+ padding: 0;
+ background: none;
+ border: none;
+ color: var(--blue-60);
+ text-decoration: underline;
+ white-space: nowrap;
+ cursor: pointer;
+}
+
+/* Settings */
+
+.perf-settings {
+ width: 100%;
+ margin: 50px 0 25px;
+}
+
+.perf-settings-title {
+ padding: 5px 10px;
+ margin-bottom: 15px;
+ background-color: var(--grey-10);
+ border: var(--grey-30) 1px solid;
+ font-size: 11px;
+ font-weight: normal;
+}
+
+.perf-settings-row {
+ display: flex;
+ overflow: hidden;
+ line-height: 1.8;
+}
+
+.perf-settings-controls > .tree {
+ height: 100%;
+}
+
+.perf-settings-row.focused {
+ background-color: var(--theme-selection-background);
+ color: var(--theme-selection-color);
+}
+
+.perf-settings-label {
+ height: 30px;
+ min-width: 110px;
+}
+
+.perf-settings-value {
+ display: flex;
+ flex: 1;
+}
+
+.perf-settings-range-input {
+ flex: 1;
+}
+
+.perf-settings-range-input-el {
+ width: 100%;
+}
+
+.perf-settings-range-value {
+ min-width: 70px;
+ text-align: end;
+}
+
+.perf-settings-notches {
+ height: 14px;
+ margin: 5px 0 10px;
+ margin-inline-start: 0.7em;
+ display: flex;
+}
+
+.perf-settings-notch {
+ margin-right: 1px;
+ flex: 1;
+ border: 1px solid rgba(0,0,0,0.2);
+ border-radius: 2px;
+}
+
+.perf-settings-notch-normal.perf-settings-notch-active {
+ border-color: hsl(90, 90%, 40%);
+ background-color: hsla(90, 90%, 40%, 0.5);
+}
+
+.perf-settings-notch-warning.perf-settings-notch-active {
+ border-color: hsl(45, 100%, 49%);
+ background-color: hsla(45, 100%, 49%, 0.5);
+}
+
+.perf-settings-notch-critical.perf-settings-notch-active {
+ border-color: hsl(0, 90%, 40%);
+ background-color: hsla(0, 90%, 40%, 0.5);
+}
+
+.perf-settings-text-input {
+ width: 100%;
+ padding: 4px;
+ box-sizing: border-box;
+}
+
+.perf-settings-text-label {
+ flex: 1;
+}
+
+.perf-settings-details-contents {
+ overflow: hidden;
+}
+
+.perf-settings-details-contents-slider {
+ padding: 10px;
+ margin: 0 0 18px;
+ border: var(--grey-20) 1px solid;
+ background-color: var(--grey-10);
+ opacity: 0;
+ transform: translateY(-100px);
+ transition-duration: 250ms;
+ transition-timing-function: cubic-bezier(.07,.95,0,1);
+ transition-property: transform, opacity;
+}
+
+.perf-settings-details[open] .perf-settings-details-contents-slider {
+ opacity: 1;
+ transform: translateY(0);
+}
+
+.perf-settings-summary {
+ height: 30px;
+ cursor: default;
+ -moz-user-select: none;
+}
+
+.perf-settings-thread-columns {
+ margin-bottom: 20px;
+ display: flex;
+ line-height: 2;
+}
+
+.perf-settings-thread-column {
+ flex: 1;
+}
+
+.perf-settings-checkbox-label {
+ display: block;
+}
+
+.perf-settings-feature-label {
+ margin: 16px 0;
+ display: flex;
+}
+
+.perf-settings-feature-label {
+ margin: 16px 0;
+ display: flex;
+}
+
+.perf-settings-checkbox {
+ align-self: flex-start;
+}
+
+.perf-settings-feature-name {
+ width: 150px;
+ color: var(--blue-60);
+}
+
+.perf-settings-feature-title {
+ flex: 1;
+}
+
+.perf-settings-feature-name {
+ width: 130px;
+ color: var(--blue-60);
+ line-height: 1.6;
+}
+
+.perf-settings-subtext {
+ font-weight: bold;
+}