diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /devtools/client/performance/components | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'devtools/client/performance/components')
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; |