summaryrefslogtreecommitdiffstats
path: root/devtools/client/performance/components
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/performance/components')
-rw-r--r--devtools/client/performance/components/jit-optimizations-item.js175
-rw-r--r--devtools/client/performance/components/jit-optimizations.js248
-rw-r--r--devtools/client/performance/components/moz.build19
-rw-r--r--devtools/client/performance/components/recording-button.js37
-rw-r--r--devtools/client/performance/components/recording-controls.js54
-rw-r--r--devtools/client/performance/components/recording-list-item.js49
-rw-r--r--devtools/client/performance/components/recording-list.js23
-rw-r--r--devtools/client/performance/components/test/chrome.ini5
-rw-r--r--devtools/client/performance/components/test/head.js187
-rw-r--r--devtools/client/performance/components/test/test_jit_optimizations_01.html70
-rw-r--r--devtools/client/performance/components/waterfall-header.js69
-rw-r--r--devtools/client/performance/components/waterfall-tree-row.js107
-rw-r--r--devtools/client/performance/components/waterfall-tree.js167
-rw-r--r--devtools/client/performance/components/waterfall.js36
14 files changed, 1246 insertions, 0 deletions
diff --git a/devtools/client/performance/components/jit-optimizations-item.js b/devtools/client/performance/components/jit-optimizations-item.js
new file mode 100644
index 000000000..e5c77ef02
--- /dev/null
+++ b/devtools/client/performance/components/jit-optimizations-item.js
@@ -0,0 +1,175 @@
+/* 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 { LocalizationHelper } = require("devtools/shared/l10n");
+const STRINGS_URI = "devtools/client/locales/jit-optimizations.properties";
+const L10N = new LocalizationHelper(STRINGS_URI);
+
+const {PluralForm} = require("devtools/shared/plural-form");
+const { DOM: dom, PropTypes, createClass, createFactory } = require("devtools/client/shared/vendor/react");
+const Frame = createFactory(require("devtools/client/shared/components/frame"));
+const PROPNAME_MAX_LENGTH = 4;
+// If TREE_ROW_HEIGHT changes, be sure to change `var(--jit-tree-row-height)`
+// in `devtools/client/themes/jit-optimizations.css`
+const TREE_ROW_HEIGHT = 14;
+
+const OPTIMIZATION_ITEM_TYPES = ["site", "attempts", "types", "attempt", "type",
+ "observedtype"];
+
+/* eslint-disable no-unused-vars */
+/**
+ * TODO - Re-enable this eslint rule. The JIT tool is a work in progress, and isn't fully
+ * integrated as of yet.
+ */
+const {
+ JITOptimizations, hasSuccessfulOutcome, isSuccessfulOutcome
+} = require("devtools/client/performance/modules/logic/jit");
+const OPTIMIZATION_FAILURE = L10N.getStr("jit.optimizationFailure");
+const JIT_SAMPLES = L10N.getStr("jit.samples");
+const JIT_TYPES = L10N.getStr("jit.types");
+const JIT_ATTEMPTS = L10N.getStr("jit.attempts");
+/* eslint-enable no-unused-vars */
+
+const JITOptimizationsItem = createClass({
+ displayName: "JITOptimizationsItem",
+
+ propTypes: {
+ onViewSourceInDebugger: PropTypes.func.isRequired,
+ frameData: PropTypes.object.isRequired,
+ type: PropTypes.oneOf(OPTIMIZATION_ITEM_TYPES).isRequired,
+ },
+
+ _renderSite({ item: site, onViewSourceInDebugger, frameData }) {
+ let attempts = site.data.attempts;
+ let lastStrategy = attempts[attempts.length - 1].strategy;
+ let propString = "";
+ let propertyName = site.data.propertyName;
+
+ // Display property name if it exists
+ if (propertyName) {
+ if (propertyName.length > PROPNAME_MAX_LENGTH) {
+ propString = ` (.${propertyName.substr(0, PROPNAME_MAX_LENGTH)}…)`;
+ } else {
+ propString = ` (.${propertyName})`;
+ }
+ }
+
+ let sampleString = PluralForm.get(site.samples, JIT_SAMPLES)
+ .replace("#1", site.samples);
+ let text = dom.span(
+ { className: "optimization-site-title" },
+ `${lastStrategy}${propString} – (${sampleString})`
+ );
+ let frame = Frame({
+ onClick: () => onViewSourceInDebugger(frameData.url, site.data.line),
+ frame: {
+ source: frameData.url,
+ line: +site.data.line,
+ column: site.data.column,
+ }
+ });
+ let children = [text, frame];
+
+ if (!hasSuccessfulOutcome(site)) {
+ children.unshift(dom.span({ className: "opt-icon warning" }));
+ }
+
+ return dom.span({ className: "optimization-site" }, ...children);
+ },
+
+ _renderAttempts({ item: attempts }) {
+ return dom.span({ className: "optimization-attempts" },
+ `${JIT_ATTEMPTS} (${attempts.length})`
+ );
+ },
+
+ _renderTypes({ item: types }) {
+ return dom.span({ className: "optimization-types" },
+ `${JIT_TYPES} (${types.length})`
+ );
+ },
+
+ _renderAttempt({ item: attempt }) {
+ let success = isSuccessfulOutcome(attempt.outcome);
+ let { strategy, outcome } = attempt;
+ return dom.span({ className: "optimization-attempt" },
+ dom.span({ className: "optimization-strategy" }, strategy),
+ " → ",
+ dom.span({ className: `optimization-outcome ${success ? "success" : "failure"}` },
+ outcome)
+ );
+ },
+
+ _renderType({ item: type }) {
+ return dom.span({ className: "optimization-ion-type" },
+ `${type.site}:${type.mirType}`);
+ },
+
+ _renderObservedType({ onViewSourceInDebugger, item: type }) {
+ let children = [
+ dom.span({ className: "optimization-observed-type-keyed" },
+ `${type.keyedBy}${type.name ? ` → ${type.name}` : ""}`)
+ ];
+
+ // If we have a line and location, make a link to the debugger
+ if (type.location && type.line) {
+ children.push(
+ Frame({
+ onClick: () => onViewSourceInDebugger(type.location, type.line),
+ frame: {
+ source: type.location,
+ line: type.line,
+ column: type.column,
+ }
+ })
+ );
+ // Otherwise if we just have a location, it's probably just a memory location.
+ } else if (type.location) {
+ children.push(`@${type.location}`);
+ }
+
+ return dom.span({ className: "optimization-observed-type" }, ...children);
+ },
+
+ render() {
+ /* eslint-disable no-unused-vars */
+ /**
+ * TODO - Re-enable this eslint rule. The JIT tool is a work in progress, and these
+ * undefined variables may represent intended functionality.
+ */
+ let {
+ depth,
+ arrow,
+ type,
+ // TODO - The following are currently unused.
+ item,
+ focused,
+ frameData,
+ onViewSourceInDebugger,
+ } = this.props;
+ /* eslint-enable no-unused-vars */
+
+ let content;
+ switch (type) {
+ case "site": content = this._renderSite(this.props); break;
+ case "attempts": content = this._renderAttempts(this.props); break;
+ case "types": content = this._renderTypes(this.props); break;
+ case "attempt": content = this._renderAttempt(this.props); break;
+ case "type": content = this._renderType(this.props); break;
+ case "observedtype": content = this._renderObservedType(this.props); break;
+ }
+
+ return dom.div(
+ {
+ className: `optimization-tree-item optimization-tree-item-${type}`,
+ style: { marginInlineStart: depth * TREE_ROW_HEIGHT }
+ },
+ arrow,
+ content
+ );
+ },
+});
+
+module.exports = JITOptimizationsItem;
diff --git a/devtools/client/performance/components/jit-optimizations.js b/devtools/client/performance/components/jit-optimizations.js
new file mode 100644
index 000000000..c189aa1ce
--- /dev/null
+++ b/devtools/client/performance/components/jit-optimizations.js
@@ -0,0 +1,248 @@
+/* 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 { LocalizationHelper } = require("devtools/shared/l10n");
+const STRINGS_URI = "devtools/client/locales/jit-optimizations.properties";
+const L10N = new LocalizationHelper(STRINGS_URI);
+
+const { assert } = require("devtools/shared/DevToolsUtils");
+const { DOM: dom, createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
+const Tree = createFactory(require("../../shared/components/tree"));
+const OptimizationsItem = createFactory(require("./jit-optimizations-item"));
+const FrameView = createFactory(require("../../shared/components/frame"));
+const JIT_TITLE = L10N.getStr("jit.title");
+// If TREE_ROW_HEIGHT changes, be sure to change `var(--jit-tree-row-height)`
+// in `devtools/client/themes/jit-optimizations.css`
+const TREE_ROW_HEIGHT = 14;
+
+/* eslint-disable no-unused-vars */
+/**
+ * TODO - Re-enable this eslint rule. The JIT tool is a work in progress, and isn't fully
+ * integrated as of yet, and this may represent intended functionality.
+ */
+const onClickTooltipString = frame =>
+ L10N.getFormatStr("viewsourceindebugger",
+ `${frame.source}:${frame.line}:${frame.column}`);
+/* eslint-enable no-unused-vars */
+
+const optimizationAttemptModel = {
+ id: PropTypes.number.isRequired,
+ strategy: PropTypes.string.isRequired,
+ outcome: PropTypes.string.isRequired,
+};
+
+const optimizationObservedTypeModel = {
+ keyedBy: PropTypes.string.isRequired,
+ name: PropTypes.string,
+ location: PropTypes.string,
+ line: PropTypes.string,
+};
+
+const optimizationIonTypeModel = {
+ id: PropTypes.number.isRequired,
+ typeset: PropTypes.arrayOf(optimizationObservedTypeModel),
+ site: PropTypes.number.isRequired,
+ mirType: PropTypes.number.isRequired,
+};
+
+const optimizationSiteModel = {
+ id: PropTypes.number.isRequired,
+ propertyName: PropTypes.string,
+ line: PropTypes.number.isRequired,
+ column: PropTypes.number.isRequired,
+ data: PropTypes.shape({
+ attempts: PropTypes.arrayOf(optimizationAttemptModel).isRequired,
+ types: PropTypes.arrayOf(optimizationIonTypeModel).isRequired,
+ }).isRequired,
+};
+
+const JITOptimizations = createClass({
+ displayName: "JITOptimizations",
+
+ propTypes: {
+ onViewSourceInDebugger: PropTypes.func.isRequired,
+ frameData: PropTypes.object.isRequired,
+ optimizationSites: PropTypes.arrayOf(optimizationSiteModel).isRequired,
+ autoExpandDepth: PropTypes.number,
+ },
+
+ getDefaultProps() {
+ return {
+ autoExpandDepth: 0
+ };
+ },
+
+ getInitialState() {
+ return {
+ expanded: new Set()
+ };
+ },
+
+ /**
+ * Frame data generated from `frameNode.getInfo()`, or an empty
+ * object, as well as a handler for clicking on the frame component.
+ *
+ * @param {?Object} .frameData
+ * @param {Function} .onViewSourceInDebugger
+ * @return {ReactElement}
+ */
+ _createHeader: function ({ frameData, onViewSourceInDebugger }) {
+ let { isMetaCategory, url, line } = frameData;
+ let name = isMetaCategory ? frameData.categoryData.label :
+ frameData.functionName || "";
+
+ // Simulate `SavedFrame`s interface
+ let frame = { source: url, line: +line, functionDisplayName: name };
+
+ // Neither Meta Category nodes, or the lack of a selected frame node,
+ // renders out a frame source, like "file.js:123"; so just use
+ // an empty span.
+ let frameComponent;
+ if (isMetaCategory || !name) {
+ frameComponent = dom.span();
+ } else {
+ frameComponent = FrameView({
+ frame,
+ onClick: () => onViewSourceInDebugger(frame),
+ });
+ }
+
+ return dom.div({ className: "optimization-header" },
+ dom.span({ className: "header-title" }, JIT_TITLE),
+ dom.span({ className: "header-function-name" }, name),
+ frameComponent
+ );
+ },
+
+ _createTree(props) {
+ let {
+ autoExpandDepth,
+ frameData,
+ onViewSourceInDebugger,
+ optimizationSites: sites
+ } = this.props;
+
+ let getSite = id => sites.find(site => site.id === id);
+ let getIonTypeForObserved = type => {
+ return getSite(type.id).data.types
+ .find(iontype => (iontype.typeset || [])
+ .indexOf(type) !== -1);
+ };
+ let isSite = site => getSite(site.id) === site;
+ let isAttempts = attempts => getSite(attempts.id).data.attempts === attempts;
+ let isAttempt = attempt => getSite(attempt.id).data.attempts.indexOf(attempt) !== -1;
+ let isTypes = types => getSite(types.id).data.types === types;
+ let isType = type => getSite(type.id).data.types.indexOf(type) !== -1;
+ let isObservedType = type => getIonTypeForObserved(type);
+
+ let getRowType = node => {
+ if (isSite(node)) {
+ return "site";
+ }
+ if (isAttempts(node)) {
+ return "attempts";
+ }
+ if (isTypes(node)) {
+ return "types";
+ }
+ if (isAttempt(node)) {
+ return "attempt";
+ }
+ if (isType(node)) {
+ return "type";
+ }
+ if (isObservedType(node)) {
+ return "observedtype";
+ }
+ return null;
+ };
+
+ // Creates a unique key for each node in the
+ // optimizations data
+ let getKey = node => {
+ let site = getSite(node.id);
+ if (isSite(node)) {
+ return node.id;
+ } else if (isAttempts(node)) {
+ return `${node.id}-A`;
+ } else if (isTypes(node)) {
+ return `${node.id}-T`;
+ } else if (isType(node)) {
+ return `${node.id}-T-${site.data.types.indexOf(node)}`;
+ } else if (isAttempt(node)) {
+ return `${node.id}-A-${site.data.attempts.indexOf(node)}`;
+ } else if (isObservedType(node)) {
+ let iontype = getIonTypeForObserved(node);
+ return `${getKey(iontype)}-O-${iontype.typeset.indexOf(node)}`;
+ }
+ return "";
+ };
+
+ return Tree({
+ autoExpandDepth,
+ getParent: node => {
+ let site = getSite(node.id);
+ let parent;
+ if (isAttempts(node) || isTypes(node)) {
+ parent = site;
+ } else if (isType(node)) {
+ parent = site.data.types;
+ } else if (isAttempt(node)) {
+ parent = site.data.attempts;
+ } else if (isObservedType(node)) {
+ parent = getIonTypeForObserved(node);
+ }
+ assert(parent, "Could not find a parent for optimization data node");
+
+ return parent;
+ },
+ getChildren: node => {
+ if (isSite(node)) {
+ return [node.data.types, node.data.attempts];
+ } else if (isAttempts(node) || isTypes(node)) {
+ return node;
+ } else if (isType(node)) {
+ return node.typeset || [];
+ }
+ return [];
+ },
+ isExpanded: node => this.state.expanded.has(node),
+ onExpand: node => this.setState(state => {
+ let expanded = new Set(state.expanded);
+ expanded.add(node);
+ return { expanded };
+ }),
+ onCollapse: node => this.setState(state => {
+ let expanded = new Set(state.expanded);
+ expanded.delete(node);
+ return { expanded };
+ }),
+ onFocus: function () {},
+ getKey,
+ getRoots: () => sites || [],
+ itemHeight: TREE_ROW_HEIGHT,
+ renderItem: (item, depth, focused, arrow, expanded) =>
+ new OptimizationsItem({
+ onViewSourceInDebugger,
+ item,
+ depth,
+ focused,
+ arrow,
+ expanded,
+ type: getRowType(item),
+ frameData,
+ }),
+ });
+ },
+
+ render() {
+ let header = this._createHeader(this.props);
+ let tree = this._createTree(this.props);
+
+ return dom.div({}, header, tree);
+ }
+});
+
+module.exports = JITOptimizations;
diff --git a/devtools/client/performance/components/moz.build b/devtools/client/performance/components/moz.build
new file mode 100644
index 000000000..55de59215
--- /dev/null
+++ b/devtools/client/performance/components/moz.build
@@ -0,0 +1,19 @@
+# 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(
+ 'jit-optimizations-item.js',
+ 'jit-optimizations.js',
+ 'recording-button.js',
+ 'recording-controls.js',
+ 'recording-list-item.js',
+ 'recording-list.js',
+ 'waterfall-header.js',
+ 'waterfall-tree-row.js',
+ 'waterfall-tree.js',
+ 'waterfall.js',
+)
+
+MOCHITEST_CHROME_MANIFESTS += ['test/chrome.ini']
diff --git a/devtools/client/performance/components/recording-button.js b/devtools/client/performance/components/recording-button.js
new file mode 100644
index 000000000..877fd0e2b
--- /dev/null
+++ b/devtools/client/performance/components/recording-button.js
@@ -0,0 +1,37 @@
+/* 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 {L10N} = require("devtools/client/performance/modules/global");
+const {DOM, createClass} = require("devtools/client/shared/vendor/react");
+const {button} = DOM;
+
+module.exports = createClass({
+ displayName: "Recording Button",
+
+ render() {
+ let {
+ onRecordButtonClick,
+ isRecording,
+ isLocked
+ } = this.props;
+
+ let classList = ["devtools-button", "record-button"];
+
+ if (isRecording) {
+ classList.push("checked");
+ }
+
+ return button(
+ {
+ className: classList.join(" "),
+ onClick: onRecordButtonClick,
+ "data-standalone": "true",
+ "data-text-only": "true",
+ disabled: isLocked
+ },
+ isRecording ? L10N.getStr("recordings.stop") : L10N.getStr("recordings.start")
+ );
+ }
+});
diff --git a/devtools/client/performance/components/recording-controls.js b/devtools/client/performance/components/recording-controls.js
new file mode 100644
index 000000000..88f788ef3
--- /dev/null
+++ b/devtools/client/performance/components/recording-controls.js
@@ -0,0 +1,54 @@
+/* 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 {L10N} = require("devtools/client/performance/modules/global");
+const {DOM, createClass} = require("devtools/client/shared/vendor/react");
+const {div, button} = DOM;
+
+module.exports = createClass({
+ displayName: "Recording Controls",
+
+ render() {
+ let {
+ onClearButtonClick,
+ onRecordButtonClick,
+ onImportButtonClick,
+ isRecording,
+ isLocked
+ } = this.props;
+
+ let recordButtonClassList = ["devtools-button", "record-button"];
+
+ if (isRecording) {
+ recordButtonClassList.push("checked");
+ }
+
+ return (
+ div({ className: "devtools-toolbar" },
+ div({ className: "toolbar-group" },
+ button({
+ id: "clear-button",
+ className: "devtools-button",
+ title: L10N.getStr("recordings.clear.tooltip"),
+ onClick: onClearButtonClick
+ }),
+ button({
+ id: "main-record-button",
+ className: recordButtonClassList.join(" "),
+ disabled: isLocked,
+ title: L10N.getStr("recordings.start.tooltip"),
+ onClick: onRecordButtonClick
+ }),
+ button({
+ id: "import-button",
+ className: "devtools-button",
+ title: L10N.getStr("recordings.import.tooltip"),
+ onClick: onImportButtonClick
+ })
+ )
+ )
+ );
+ }
+});
diff --git a/devtools/client/performance/components/recording-list-item.js b/devtools/client/performance/components/recording-list-item.js
new file mode 100644
index 000000000..37efec90d
--- /dev/null
+++ b/devtools/client/performance/components/recording-list-item.js
@@ -0,0 +1,49 @@
+/* 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 {DOM, createClass} = require("devtools/client/shared/vendor/react");
+const {div, li, span, button} = DOM;
+const {L10N} = require("devtools/client/performance/modules/global");
+
+module.exports = createClass({
+ displayName: "Recording List Item",
+
+ render() {
+ const {
+ label,
+ duration,
+ onSelect,
+ onSave,
+ isLoading,
+ isSelected,
+ isRecording
+ } = this.props;
+
+ const className = `recording-list-item ${isSelected ? "selected" : ""}`;
+
+ let durationText;
+ if (isLoading) {
+ durationText = L10N.getStr("recordingsList.loadingLabel");
+ } else if (isRecording) {
+ durationText = L10N.getStr("recordingsList.recordingLabel");
+ } else {
+ durationText = L10N.getFormatStr("recordingsList.durationLabel", duration);
+ }
+
+ return (
+ li({ className, onClick: onSelect },
+ div({ className: "recording-list-item-label" },
+ label
+ ),
+ div({ className: "recording-list-item-footer" },
+ span({ className: "recording-list-item-duration" }, durationText),
+ button({ className: "recording-list-item-save", onClick: onSave },
+ L10N.getStr("recordingsList.saveLabel")
+ )
+ )
+ )
+ );
+ }
+});
diff --git a/devtools/client/performance/components/recording-list.js b/devtools/client/performance/components/recording-list.js
new file mode 100644
index 000000000..1df7f2b71
--- /dev/null
+++ b/devtools/client/performance/components/recording-list.js
@@ -0,0 +1,23 @@
+/* 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 {DOM, createClass} = require("devtools/client/shared/vendor/react");
+const {L10N} = require("devtools/client/performance/modules/global");
+const {ul, div} = DOM;
+
+module.exports = createClass({
+ displayName: "Recording List",
+
+ render() {
+ const {
+ items,
+ itemComponent: Item,
+ } = this.props;
+
+ return items.length > 0
+ ? ul({ className: "recording-list" }, ...items.map(Item))
+ : div({ className: "recording-list-empty" }, L10N.getStr("noRecordingsText"));
+ }
+});
diff --git a/devtools/client/performance/components/test/chrome.ini b/devtools/client/performance/components/test/chrome.ini
new file mode 100644
index 000000000..5ba24a9af
--- /dev/null
+++ b/devtools/client/performance/components/test/chrome.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+support-files =
+ head.js
+
+[test_jit_optimizations_01.html]
diff --git a/devtools/client/performance/components/test/head.js b/devtools/client/performance/components/test/head.js
new file mode 100644
index 000000000..be8184160
--- /dev/null
+++ b/devtools/client/performance/components/test/head.js
@@ -0,0 +1,187 @@
+/* Any copyright is dedicated to the Public Domain.
+ yield new Promise(function(){});
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* global window, document, SimpleTest, requestAnimationFrame, is, ok */
+/* exported Cc, Ci, Cu, Cr, Assert, Task, TargetFactory, Toolbox, browserRequire,
+ forceRender, setProps, dumpn, checkOptimizationHeader, checkOptimizationTree */
+let { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+let { require } = Cu.import("resource://gre/modules/devtools/shared/Loader.jsm", {});
+let { Assert } = require("resource://testing-common/Assert.jsm");
+let { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
+let defer = require("devtools/shared/defer");
+let flags = require("devtools/shared/flags");
+let { Task } = require("devtools/shared/task");
+let { TargetFactory } = require("devtools/client/framework/target");
+let { Toolbox } = require("devtools/client/framework/toolbox");
+
+flags.testing = true;
+let { require: browserRequire } = BrowserLoader({
+ baseURI: "resource://devtools/client/performance/",
+ window
+});
+
+let $ = (selector, scope = document) => scope.querySelector(selector);
+let $$ = (selector, scope = document) => scope.querySelectorAll(selector);
+
+function forceRender(comp) {
+ return setState(comp, {})
+ .then(() => setState(comp, {}));
+}
+
+// All tests are asynchronous.
+SimpleTest.waitForExplicitFinish();
+
+function onNextAnimationFrame(fn) {
+ return () =>
+ requestAnimationFrame(() =>
+ requestAnimationFrame(fn));
+}
+
+function setState(component, newState) {
+ let deferred = defer();
+ component.setState(newState, onNextAnimationFrame(deferred.resolve));
+ return deferred.promise;
+}
+
+function setProps(component, newState) {
+ let deferred = defer();
+ component.setProps(newState, onNextAnimationFrame(deferred.resolve));
+ return deferred.promise;
+}
+
+function dumpn(msg) {
+ dump(`PERFORMANCE-COMPONENT-TEST: ${msg}\n`);
+}
+
+/**
+ * Default opts data for testing. First site has a simple IonType,
+ * and an IonType with an ObservedType, and a successful outcome.
+ * Second site does not have a successful outcome.
+ */
+let OPTS_DATA_GENERAL = [{
+ id: 1,
+ propertyName: "my property name",
+ line: 100,
+ column: 200,
+ samples: 90,
+ data: {
+ attempts: [
+ { id: 1, strategy: "GetElem_TypedObject", outcome: "AccessNotTypedObject" },
+ { id: 1, strategy: "GetElem_Dense", outcome: "AccessNotDense" },
+ { id: 1, strategy: "GetElem_TypedStatic", outcome: "Disabled" },
+ { id: 1, strategy: "GetElem_TypedArray", outcome: "GenericSuccess" },
+ ],
+ types: [{
+ id: 1,
+ site: "Receiver",
+ mirType: "Object",
+ typeset: [{
+ id: 1,
+ keyedBy: "constructor",
+ name: "MyView",
+ location: "http://internet.com/file.js",
+ line: "123",
+ }]
+ }, {
+ id: 1,
+ typeset: void 0,
+ site: "Index",
+ mirType: "Int32",
+ }]
+ }
+}, {
+ id: 2,
+ propertyName: void 0,
+ line: 50,
+ column: 51,
+ samples: 100,
+ data: {
+ attempts: [
+ { id: 2, strategy: "Call_Inline", outcome: "CantInlineBigData" }
+ ],
+ types: [{
+ id: 2,
+ site: "Call_Target",
+ mirType: "Object",
+ typeset: [
+ { id: 2, keyedBy: "primitive" },
+ { id: 2, keyedBy: "constructor", name: "B", location: "http://mypage.com/file.js", line: "2" },
+ { id: 2, keyedBy: "constructor", name: "C", location: "http://mypage.com/file.js", line: "3" },
+ { id: 2, keyedBy: "constructor", name: "D", location: "http://mypage.com/file.js", line: "4" },
+ ],
+ }]
+ }
+}];
+
+OPTS_DATA_GENERAL.forEach(site => {
+ site.data.types.forEach(type => {
+ if (type.typeset) {
+ type.typeset.id = site.id;
+ }
+ });
+ site.data.attempts.id = site.id;
+ site.data.types.id = site.id;
+});
+
+function checkOptimizationHeader(name, file, line) {
+ is($(".optimization-header .header-function-name").textContent, name,
+ "correct optimization header function name");
+ is($(".optimization-header .frame-link-filename").textContent, file,
+ "correct optimization header file name");
+ is($(".optimization-header .frame-link-line").textContent, `:${line}`,
+ "correct optimization header line");
+}
+
+function checkOptimizationTree(rowData) {
+ let rows = $$(".tree .tree-node");
+
+ for (let i = 0; i < rowData.length; i++) {
+ let row = rows[i];
+ let expected = rowData[i];
+
+ switch (expected.type) {
+ case "site":
+ is($(".optimization-site-title", row).textContent,
+ `${expected.strategy} – (${expected.samples} samples)`,
+ `row ${i}th: correct optimization site row`);
+
+ is(!!$(".opt-icon.warning", row), !!expected.failureIcon,
+ `row ${i}th: expected visibility of failure icon for unsuccessful outcomes`);
+ break;
+ case "types":
+ is($(".optimization-types", row).textContent,
+ `Types (${expected.count})`,
+ `row ${i}th: correct types row`);
+ break;
+ case "attempts":
+ is($(".optimization-attempts", row).textContent,
+ `Attempts (${expected.count})`,
+ `row ${i}th: correct attempts row`);
+ break;
+ case "type":
+ is($(".optimization-ion-type", row).textContent,
+ `${expected.site}:${expected.mirType}`,
+ `row ${i}th: correct ion type row`);
+ break;
+ case "observedtype":
+ is($(".optimization-observed-type-keyed", row).textContent,
+ expected.name ?
+ `${expected.keyedBy} → ${expected.name}` :
+ expected.keyedBy,
+ `row ${i}th: correct observed type row`);
+ break;
+ case "attempt":
+ is($(".optimization-strategy", row).textContent, expected.strategy,
+ `row ${i}th: correct attempt row, attempt item`);
+ is($(".optimization-outcome", row).textContent, expected.outcome,
+ `row ${i}th: correct attempt row, outcome item`);
+ ok($(".optimization-outcome", row)
+ .classList.contains(expected.success ? "success" : "failure"),
+ `row ${i}th: correct attempt row, failure/success status`);
+ break;
+ }
+ }
+}
diff --git a/devtools/client/performance/components/test/test_jit_optimizations_01.html b/devtools/client/performance/components/test/test_jit_optimizations_01.html
new file mode 100644
index 000000000..edc9c34cd
--- /dev/null
+++ b/devtools/client/performance/components/test/test_jit_optimizations_01.html
@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the rendering of the JIT Optimizations tree. Tests when jit data has observed types, multiple observed types, multiple sites, a site with a successful strategy, site with no successful strategy.
+-->
+<head>
+ <meta charset="utf-8">
+ <title>JITOptimizations component test</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body style="height: 10000px;">
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+ try {
+ let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ let React = browserRequire("devtools/client/shared/vendor/react");
+ let JITOptimizations = React.createFactory(browserRequire("devtools/client/performance/components/jit-optimizations"));
+ ok(JITOptimizations, "Should get JITOptimizations");
+ let opts;
+
+ opts = ReactDOM.render(JITOptimizations({
+ onViewSourceInDebugger: function(){},
+ frameData: {
+ isMetaCategory: false,
+ url: "http://internet.com/file.js",
+ line: 1,
+ functionName: "myfunc",
+ },
+ optimizationSites: OPTS_DATA_GENERAL,
+ autoExpandDepth: 1000,
+ }), window.document.body);
+ yield forceRender(opts);
+
+ checkOptimizationHeader("myfunc", "file.js", "1");
+
+ checkOptimizationTree([
+ { type: "site", strategy: "GetElem_TypedArray", samples: "90" },
+ { type: "types", count: "2" },
+ { type: "type", site: "Receiver", mirType: "Object" },
+ { type: "observedtype", keyedBy: "constructor", name: "MyView" },
+ { type: "type", site: "Index", mirType: "Int32" },
+ { type: "attempts", count: "4" },
+ { type: "attempt", strategy: "GetElem_TypedObject", outcome: "AccessNotTypedObject" },
+ { type: "attempt", strategy: "GetElem_Dense", outcome: "AccessNotDense" },
+ { type: "attempt", strategy: "GetElem_TypedStatic", outcome: "Disabled" },
+ { type: "attempt", strategy: "GetElem_TypedArray", outcome: "GenericSuccess", success: true },
+ { type: "site", strategy: "Call_Inline", samples: "100", failureIcon: true },
+ { type: "types", count: "1" },
+ { type: "type", site: "Call_Target", mirType: "Object" },
+ { type: "observedtype", keyedBy: "primitive" },
+ { type: "observedtype", keyedBy: "constructor", name: "B" },
+ { type: "observedtype", keyedBy: "constructor", name: "C" },
+ { type: "observedtype", keyedBy: "constructor", name: "D" },
+ { type: "attempts", count: "1" },
+ { type: "attempt", strategy: "Call_Inline", outcome: "CantInlineBigData" },
+ ]);
+
+ } catch(e) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+ } finally {
+ SimpleTest.finish();
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/devtools/client/performance/components/waterfall-header.js b/devtools/client/performance/components/waterfall-header.js
new file mode 100644
index 000000000..f3030091b
--- /dev/null
+++ b/devtools/client/performance/components/waterfall-header.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";
+
+/**
+ * The "waterfall ticks" view, a header for the markers displayed in the waterfall.
+ */
+
+const { DOM: dom, PropTypes } = require("devtools/client/shared/vendor/react");
+const { L10N } = require("../modules/global");
+const { TickUtils } = require("../modules/waterfall-ticks");
+
+// ms
+const WATERFALL_HEADER_TICKS_MULTIPLE = 5;
+// px
+const WATERFALL_HEADER_TICKS_SPACING_MIN = 50;
+// px
+const WATERFALL_HEADER_TEXT_PADDING = 3;
+
+function WaterfallHeader(props) {
+ let { startTime, dataScale, sidebarWidth, waterfallWidth } = props;
+
+ let tickInterval = TickUtils.findOptimalTickInterval({
+ ticksMultiple: WATERFALL_HEADER_TICKS_MULTIPLE,
+ ticksSpacingMin: WATERFALL_HEADER_TICKS_SPACING_MIN,
+ dataScale: dataScale
+ });
+
+ let ticks = [];
+ for (let x = 0; x < waterfallWidth; x += tickInterval) {
+ let left = x + WATERFALL_HEADER_TEXT_PADDING;
+ let time = Math.round(x / dataScale + startTime);
+ let label = L10N.getFormatStr("timeline.tick", time);
+
+ let node = dom.div({
+ className: "plain waterfall-header-tick",
+ style: { transform: `translateX(${left}px)` }
+ }, label);
+ ticks.push(node);
+ }
+
+ return dom.div(
+ { className: "waterfall-header" },
+ dom.div(
+ {
+ className: "waterfall-sidebar theme-sidebar waterfall-header-name",
+ style: { width: sidebarWidth + "px" }
+ },
+ L10N.getStr("timeline.records")
+ ),
+ dom.div(
+ { className: "waterfall-header-ticks waterfall-background-ticks" },
+ ticks
+ )
+ );
+}
+
+WaterfallHeader.displayName = "WaterfallHeader";
+
+WaterfallHeader.propTypes = {
+ startTime: PropTypes.number.isRequired,
+ dataScale: PropTypes.number.isRequired,
+ sidebarWidth: PropTypes.number.isRequired,
+ waterfallWidth: PropTypes.number.isRequired,
+};
+
+module.exports = WaterfallHeader;
diff --git a/devtools/client/performance/components/waterfall-tree-row.js b/devtools/client/performance/components/waterfall-tree-row.js
new file mode 100644
index 000000000..b87750db1
--- /dev/null
+++ b/devtools/client/performance/components/waterfall-tree-row.js
@@ -0,0 +1,107 @@
+/* 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";
+
+/**
+ * A single row (node) in the waterfall tree
+ */
+
+const { DOM: dom, PropTypes } = require("devtools/client/shared/vendor/react");
+const { MarkerBlueprintUtils } = require("../modules/marker-blueprint-utils");
+
+// px
+const LEVEL_INDENT = 10;
+// px
+const ARROW_NODE_OFFSET = -14;
+// px
+const WATERFALL_MARKER_TIMEBAR_WIDTH_MIN = 5;
+
+function buildMarkerSidebar(blueprint, props) {
+ const { marker, level, sidebarWidth } = props;
+
+ let bullet = dom.div({
+ className: `waterfall-marker-bullet marker-color-${blueprint.colorName}`,
+ style: { transform: `translateX(${level * LEVEL_INDENT}px)` },
+ "data-type": marker.name
+ });
+
+ let label = MarkerBlueprintUtils.getMarkerLabel(marker);
+
+ let name = dom.div({
+ className: "plain waterfall-marker-name",
+ style: { transform: `translateX(${level * LEVEL_INDENT}px)` },
+ title: label
+ }, label);
+
+ return dom.div({
+ className: "waterfall-sidebar theme-sidebar",
+ style: { width: sidebarWidth + "px" }
+ }, bullet, name);
+}
+
+function buildMarkerTimebar(blueprint, props) {
+ const { marker, startTime, dataScale, arrow } = props;
+ const offset = (marker.start - startTime) * dataScale + ARROW_NODE_OFFSET;
+ const width = Math.max((marker.end - marker.start) * dataScale,
+ WATERFALL_MARKER_TIMEBAR_WIDTH_MIN);
+
+ let bar = dom.div(
+ {
+ className: "waterfall-marker-wrap",
+ style: { transform: `translateX(${offset}px)` }
+ },
+ arrow,
+ dom.div({
+ className: `waterfall-marker-bar marker-color-${blueprint.colorName}`,
+ style: { width: `${width}px` },
+ "data-type": marker.name
+ })
+ );
+
+ return dom.div(
+ { className: "waterfall-marker waterfall-background-ticks" },
+ bar
+ );
+}
+
+function WaterfallTreeRow(props) {
+ const { marker, focused } = props;
+ const blueprint = MarkerBlueprintUtils.getBlueprintFor(marker);
+
+ let attrs = {
+ className: "waterfall-tree-item" + (focused ? " focused" : ""),
+ "data-otmt": marker.isOffMainThread
+ };
+
+ // Don't render an expando-arrow for leaf nodes.
+ let submarkers = marker.submarkers;
+ let hasDescendants = submarkers && submarkers.length > 0;
+ if (hasDescendants) {
+ attrs["data-expandable"] = "";
+ } else {
+ attrs["data-invisible"] = "";
+ }
+
+ return dom.div(
+ attrs,
+ buildMarkerSidebar(blueprint, props),
+ buildMarkerTimebar(blueprint, props)
+ );
+}
+
+WaterfallTreeRow.displayName = "WaterfallTreeRow";
+
+WaterfallTreeRow.propTypes = {
+ marker: PropTypes.object.isRequired,
+ level: PropTypes.number.isRequired,
+ arrow: PropTypes.element.isRequired,
+ expanded: PropTypes.bool.isRequired,
+ focused: PropTypes.bool.isRequired,
+ startTime: PropTypes.number.isRequired,
+ dataScale: PropTypes.number.isRequired,
+ sidebarWidth: PropTypes.number.isRequired,
+};
+
+module.exports = WaterfallTreeRow;
diff --git a/devtools/client/performance/components/waterfall-tree.js b/devtools/client/performance/components/waterfall-tree.js
new file mode 100644
index 000000000..031c4facf
--- /dev/null
+++ b/devtools/client/performance/components/waterfall-tree.js
@@ -0,0 +1,167 @@
+/* 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 { createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
+const Tree = createFactory(require("devtools/client/shared/components/tree"));
+const WaterfallTreeRow = createFactory(require("./waterfall-tree-row"));
+
+// px - keep in sync with var(--waterfall-tree-row-height) in performance.css
+const WATERFALL_TREE_ROW_HEIGHT = 15;
+
+/**
+ * Checks if a given marker is in the specified time range.
+ *
+ * @param object e
+ * The marker containing the { start, end } timestamps.
+ * @param number start
+ * The earliest allowed time.
+ * @param number end
+ * The latest allowed time.
+ * @return boolean
+ * True if the marker fits inside the specified time range.
+ */
+function isMarkerInRange(e, start, end) {
+ let mStart = e.start | 0;
+ let mEnd = e.end | 0;
+
+ return (
+ // bounds inside
+ (mStart >= start && mEnd <= end) ||
+ // bounds outside
+ (mStart < start && mEnd > end) ||
+ // overlap start
+ (mStart < start && mEnd >= start && mEnd <= end) ||
+ // overlap end
+ (mEnd > end && mStart >= start && mStart <= end)
+ );
+}
+
+const WaterfallTree = createClass({
+ displayName: "WaterfallTree",
+
+ propTypes: {
+ marker: PropTypes.object.isRequired,
+ startTime: PropTypes.number.isRequired,
+ endTime: PropTypes.number.isRequired,
+ dataScale: PropTypes.number.isRequired,
+ sidebarWidth: PropTypes.number.isRequired,
+ waterfallWidth: PropTypes.number.isRequired,
+ onFocus: PropTypes.func,
+ },
+
+ getInitialState() {
+ return {
+ focused: null,
+ expanded: new Set()
+ };
+ },
+
+ _getRoots(node) {
+ let roots = this.props.marker.submarkers || [];
+ return roots.filter(this._filter);
+ },
+
+ /**
+ * Find the parent node of 'node' with a depth-first search of the marker tree
+ */
+ _getParent(node) {
+ function findParent(marker) {
+ if (marker.submarkers) {
+ for (let submarker of marker.submarkers) {
+ if (submarker === node) {
+ return marker;
+ }
+
+ let parent = findParent(submarker);
+ if (parent) {
+ return parent;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ let rootMarker = this.props.marker;
+ let parent = findParent(rootMarker);
+
+ // We are interested only in parent markers that are rendered,
+ // which rootMarker is not. Return null if the parent is rootMarker.
+ return parent !== rootMarker ? parent : null;
+ },
+
+ _getChildren(node) {
+ let submarkers = node.submarkers || [];
+ return submarkers.filter(this._filter);
+ },
+
+ _getKey(node) {
+ return `marker-${node.index}`;
+ },
+
+ _isExpanded(node) {
+ return this.state.expanded.has(node);
+ },
+
+ _onExpand(node) {
+ this.setState(state => {
+ let expanded = new Set(state.expanded);
+ expanded.add(node);
+ return { expanded };
+ });
+ },
+
+ _onCollapse(node) {
+ this.setState(state => {
+ let expanded = new Set(state.expanded);
+ expanded.delete(node);
+ return { expanded };
+ });
+ },
+
+ _onFocus(node) {
+ this.setState({ focused: node });
+ if (this.props.onFocus) {
+ this.props.onFocus(node);
+ }
+ },
+
+ _filter(node) {
+ let { startTime, endTime } = this.props;
+ return isMarkerInRange(node, startTime, endTime);
+ },
+
+ _renderItem(marker, level, focused, arrow, expanded) {
+ let { startTime, dataScale, sidebarWidth } = this.props;
+ return WaterfallTreeRow({
+ marker,
+ level,
+ arrow,
+ expanded,
+ focused,
+ startTime,
+ dataScale,
+ sidebarWidth
+ });
+ },
+
+ render() {
+ return Tree({
+ getRoots: this._getRoots,
+ getParent: this._getParent,
+ getChildren: this._getChildren,
+ getKey: this._getKey,
+ isExpanded: this._isExpanded,
+ onExpand: this._onExpand,
+ onCollapse: this._onCollapse,
+ onFocus: this._onFocus,
+ renderItem: this._renderItem,
+ focused: this.state.focused,
+ itemHeight: WATERFALL_TREE_ROW_HEIGHT
+ });
+ }
+});
+
+module.exports = WaterfallTree;
diff --git a/devtools/client/performance/components/waterfall.js b/devtools/client/performance/components/waterfall.js
new file mode 100644
index 000000000..067033874
--- /dev/null
+++ b/devtools/client/performance/components/waterfall.js
@@ -0,0 +1,36 @@
+/* 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";
+
+/**
+ * This file contains the "waterfall" view, essentially a detailed list
+ * of all the markers in the timeline data.
+ */
+
+const { DOM: dom, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
+const WaterfallHeader = createFactory(require("./waterfall-header"));
+const WaterfallTree = createFactory(require("./waterfall-tree"));
+
+function Waterfall(props) {
+ return dom.div(
+ { className: "waterfall-markers" },
+ WaterfallHeader(props),
+ WaterfallTree(props)
+ );
+}
+
+Waterfall.displayName = "Waterfall";
+
+Waterfall.propTypes = {
+ marker: PropTypes.object.isRequired,
+ startTime: PropTypes.number.isRequired,
+ endTime: PropTypes.number.isRequired,
+ dataScale: PropTypes.number.isRequired,
+ sidebarWidth: PropTypes.number.isRequired,
+ waterfallWidth: PropTypes.number.isRequired,
+ onFocus: PropTypes.func,
+ onBlur: PropTypes.func,
+};
+
+module.exports = Waterfall;