summaryrefslogtreecommitdiffstats
path: root/devtools/client/performance
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /devtools/client/performance
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/performance')
-rw-r--r--devtools/client/performance/components/JITOptimizations.js274
-rw-r--r--devtools/client/performance/components/JITOptimizationsItem.js228
-rw-r--r--devtools/client/performance/components/RecordingButton.js45
-rw-r--r--devtools/client/performance/components/RecordingControls.js63
-rw-r--r--devtools/client/performance/components/RecordingList.js32
-rw-r--r--devtools/client/performance/components/RecordingListItem.js65
-rw-r--r--devtools/client/performance/components/Waterfall.js42
-rw-r--r--devtools/client/performance/components/WaterfallHeader.js73
-rw-r--r--devtools/client/performance/components/WaterfallTree.js189
-rw-r--r--devtools/client/performance/components/WaterfallTreeRow.js116
-rw-r--r--devtools/client/performance/components/chrome/chrome.ini5
-rw-r--r--devtools/client/performance/components/chrome/head.js245
-rw-r--r--devtools/client/performance/components/chrome/test_jit_optimizations_01.html70
-rw-r--r--devtools/client/performance/components/moz.build19
-rw-r--r--devtools/client/performance/docs/markers.md171
-rw-r--r--devtools/client/performance/events.js110
-rw-r--r--devtools/client/performance/index.xhtml354
-rw-r--r--devtools/client/performance/initializer.js80
-rw-r--r--devtools/client/performance/modules/categories.js87
-rw-r--r--devtools/client/performance/modules/constants.js11
-rw-r--r--devtools/client/performance/modules/global.js36
-rw-r--r--devtools/client/performance/modules/io.js173
-rw-r--r--devtools/client/performance/modules/logic/frame-utils.js510
-rw-r--r--devtools/client/performance/modules/logic/jit.js350
-rw-r--r--devtools/client/performance/modules/logic/moz.build12
-rw-r--r--devtools/client/performance/modules/logic/telemetry.js106
-rw-r--r--devtools/client/performance/modules/logic/tree-model.js589
-rw-r--r--devtools/client/performance/modules/logic/waterfall-utils.js171
-rw-r--r--devtools/client/performance/modules/marker-blueprint-utils.js110
-rw-r--r--devtools/client/performance/modules/marker-dom-utils.js275
-rw-r--r--devtools/client/performance/modules/marker-formatters.js204
-rw-r--r--devtools/client/performance/modules/markers.js180
-rw-r--r--devtools/client/performance/modules/moz.build22
-rw-r--r--devtools/client/performance/modules/utils.js24
-rw-r--r--devtools/client/performance/modules/waterfall-ticks.js98
-rw-r--r--devtools/client/performance/modules/widgets/graphs.js527
-rw-r--r--devtools/client/performance/modules/widgets/marker-details.js177
-rw-r--r--devtools/client/performance/modules/widgets/markers-overview.js256
-rw-r--r--devtools/client/performance/modules/widgets/moz.build11
-rw-r--r--devtools/client/performance/modules/widgets/tree-view.js461
-rw-r--r--devtools/client/performance/moz.build25
-rw-r--r--devtools/client/performance/panel.js162
-rw-r--r--devtools/client/performance/performance-controller.js542
-rw-r--r--devtools/client/performance/performance-view.js490
-rw-r--r--devtools/client/performance/test/.eslintrc.js6
-rw-r--r--devtools/client/performance/test/browser.ini148
-rw-r--r--devtools/client/performance/test/browser_aaa-run-first-leaktest.js27
-rw-r--r--devtools/client/performance/test/browser_perf-button-states.js105
-rw-r--r--devtools/client/performance/test/browser_perf-calltree-js-categories.js82
-rw-r--r--devtools/client/performance/test/browser_perf-calltree-js-columns.js87
-rw-r--r--devtools/client/performance/test/browser_perf-calltree-js-events.js79
-rw-r--r--devtools/client/performance/test/browser_perf-calltree-memory-columns.js91
-rw-r--r--devtools/client/performance/test/browser_perf-console-record-01.js57
-rw-r--r--devtools/client/performance/test/browser_perf-console-record-02.js104
-rw-r--r--devtools/client/performance/test/browser_perf-console-record-03.js90
-rw-r--r--devtools/client/performance/test/browser_perf-console-record-04.js75
-rw-r--r--devtools/client/performance/test/browser_perf-console-record-05.js131
-rw-r--r--devtools/client/performance/test/browser_perf-console-record-06.js125
-rw-r--r--devtools/client/performance/test/browser_perf-console-record-07.js247
-rw-r--r--devtools/client/performance/test/browser_perf-console-record-08.js311
-rw-r--r--devtools/client/performance/test/browser_perf-console-record-09.js83
-rw-r--r--devtools/client/performance/test/browser_perf-details-01-toggle.js89
-rw-r--r--devtools/client/performance/test/browser_perf-details-02-utility-fun.js75
-rw-r--r--devtools/client/performance/test/browser_perf-details-03-without-allocations.js176
-rw-r--r--devtools/client/performance/test/browser_perf-details-04-toolbar-buttons.js253
-rw-r--r--devtools/client/performance/test/browser_perf-details-05-preserve-view.js68
-rw-r--r--devtools/client/performance/test/browser_perf-details-06-rerender-on-selection.js105
-rw-r--r--devtools/client/performance/test/browser_perf-details-07-bleed-events.js63
-rw-r--r--devtools/client/performance/test/browser_perf-details-render-00-waterfall.js53
-rw-r--r--devtools/client/performance/test/browser_perf-details-render-01-js-calltree.js51
-rw-r--r--devtools/client/performance/test/browser_perf-details-render-02-js-flamegraph.js51
-rw-r--r--devtools/client/performance/test/browser_perf-details-render-03-memory-calltree.js60
-rw-r--r--devtools/client/performance/test/browser_perf-details-render-04-memory-flamegraph.js60
-rw-r--r--devtools/client/performance/test/browser_perf-docload.js58
-rw-r--r--devtools/client/performance/test/browser_perf-fission-switch-target.js58
-rw-r--r--devtools/client/performance/test/browser_perf-gc-snap.js146
-rw-r--r--devtools/client/performance/test/browser_perf-highlighted.js66
-rw-r--r--devtools/client/performance/test/browser_perf-loading-01.js80
-rw-r--r--devtools/client/performance/test/browser_perf-loading-02.js129
-rw-r--r--devtools/client/performance/test/browser_perf-marker-details.js143
-rw-r--r--devtools/client/performance/test/browser_perf-options-01-toggle-throw.js40
-rw-r--r--devtools/client/performance/test/browser_perf-options-02-toggle-throw-alt.js44
-rw-r--r--devtools/client/performance/test/browser_perf-options-03-toggle-meta.js51
-rw-r--r--devtools/client/performance/test/browser_perf-options-enable-framerate-01.js77
-rw-r--r--devtools/client/performance/test/browser_perf-options-enable-framerate-02.js57
-rw-r--r--devtools/client/performance/test/browser_perf-options-enable-memory-01.js96
-rw-r--r--devtools/client/performance/test/browser_perf-options-enable-memory-02.js69
-rw-r--r--devtools/client/performance/test/browser_perf-options-flatten-tree-recursion-01.js101
-rw-r--r--devtools/client/performance/test/browser_perf-options-flatten-tree-recursion-02.js124
-rw-r--r--devtools/client/performance/test/browser_perf-options-invert-call-tree-01.js53
-rw-r--r--devtools/client/performance/test/browser_perf-options-invert-call-tree-02.js59
-rw-r--r--devtools/client/performance/test/browser_perf-options-invert-flame-graph-01.js53
-rw-r--r--devtools/client/performance/test/browser_perf-options-invert-flame-graph-02.js63
-rw-r--r--devtools/client/performance/test/browser_perf-options-propagate-allocations.js56
-rw-r--r--devtools/client/performance/test/browser_perf-options-propagate-profiler.js42
-rw-r--r--devtools/client/performance/test/browser_perf-options-show-idle-blocks-01.js53
-rw-r--r--devtools/client/performance/test/browser_perf-options-show-idle-blocks-02.js62
-rw-r--r--devtools/client/performance/test/browser_perf-options-show-jit-optimizations.js303
-rw-r--r--devtools/client/performance/test/browser_perf-options-show-platform-data-01.js53
-rw-r--r--devtools/client/performance/test/browser_perf-options-show-platform-data-02.js53
-rw-r--r--devtools/client/performance/test/browser_perf-overview-render-01.js42
-rw-r--r--devtools/client/performance/test/browser_perf-overview-render-02.js158
-rw-r--r--devtools/client/performance/test/browser_perf-overview-render-03.js114
-rw-r--r--devtools/client/performance/test/browser_perf-overview-render-04.js106
-rw-r--r--devtools/client/performance/test/browser_perf-overview-selection-01.js97
-rw-r--r--devtools/client/performance/test/browser_perf-overview-selection-02.js102
-rw-r--r--devtools/client/performance/test/browser_perf-overview-selection-03.js122
-rw-r--r--devtools/client/performance/test/browser_perf-overview-time-interval.js96
-rw-r--r--devtools/client/performance/test/browser_perf-private-browsing.js136
-rw-r--r--devtools/client/performance/test/browser_perf-range-changed-render.js104
-rw-r--r--devtools/client/performance/test/browser_perf-recording-notices-01.js55
-rw-r--r--devtools/client/performance/test/browser_perf-recording-notices-02.js80
-rw-r--r--devtools/client/performance/test/browser_perf-recording-notices-03.js208
-rw-r--r--devtools/client/performance/test/browser_perf-recording-notices-04.js90
-rw-r--r--devtools/client/performance/test/browser_perf-recording-notices-05.js54
-rw-r--r--devtools/client/performance/test/browser_perf-recording-selected-01.js65
-rw-r--r--devtools/client/performance/test/browser_perf-recording-selected-02.js84
-rw-r--r--devtools/client/performance/test/browser_perf-recording-selected-03.js57
-rw-r--r--devtools/client/performance/test/browser_perf-recording-selected-04.js80
-rw-r--r--devtools/client/performance/test/browser_perf-recordings-clear-01.js85
-rw-r--r--devtools/client/performance/test/browser_perf-recordings-clear-02.js114
-rw-r--r--devtools/client/performance/test/browser_perf-recordings-io-01.js94
-rw-r--r--devtools/client/performance/test/browser_perf-recordings-io-02.js26
-rw-r--r--devtools/client/performance/test/browser_perf-recordings-io-03.js54
-rw-r--r--devtools/client/performance/test/browser_perf-recordings-io-04.js176
-rw-r--r--devtools/client/performance/test/browser_perf-recordings-io-05.js43
-rw-r--r--devtools/client/performance/test/browser_perf-recordings-io-06.js140
-rw-r--r--devtools/client/performance/test/browser_perf-refresh.js47
-rw-r--r--devtools/client/performance/test/browser_perf-states.js176
-rw-r--r--devtools/client/performance/test/browser_perf-telemetry-01.js86
-rw-r--r--devtools/client/performance/test/browser_perf-telemetry-02.js69
-rw-r--r--devtools/client/performance/test/browser_perf-telemetry-03.js83
-rw-r--r--devtools/client/performance/test/browser_perf-telemetry-04.js87
-rw-r--r--devtools/client/performance/test/browser_perf-theme-toggle.js78
-rw-r--r--devtools/client/performance/test/browser_perf-tree-abstract-01.js205
-rw-r--r--devtools/client/performance/test/browser_perf-tree-abstract-02.js213
-rw-r--r--devtools/client/performance/test/browser_perf-tree-abstract-03.js207
-rw-r--r--devtools/client/performance/test/browser_perf-tree-abstract-04.js44
-rw-r--r--devtools/client/performance/test/browser_perf-tree-abstract-05.js139
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-01.js122
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-02.js287
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-03.js163
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-04.js158
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-05.js55
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-06.js62
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-07.js52
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-08.js155
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-09.js76
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-10.js195
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-11.js154
-rw-r--r--devtools/client/performance/test/browser_perf-ui-recording.js52
-rw-r--r--devtools/client/performance/test/browser_timeline-filters-01.js119
-rw-r--r--devtools/client/performance/test/browser_timeline-filters-02.js48
-rw-r--r--devtools/client/performance/test/browser_timeline-waterfall-background.js53
-rw-r--r--devtools/client/performance/test/browser_timeline-waterfall-generic.js154
-rw-r--r--devtools/client/performance/test/browser_timeline-waterfall-rerender.js76
-rw-r--r--devtools/client/performance/test/browser_timeline-waterfall-sidebar.js77
-rw-r--r--devtools/client/performance/test/browser_timeline-waterfall-workers.js70
-rw-r--r--devtools/client/performance/test/doc_allocs.html26
-rw-r--r--devtools/client/performance/test/doc_innerHTML.html21
-rw-r--r--devtools/client/performance/test/doc_markers.html38
-rw-r--r--devtools/client/performance/test/doc_simple-test.html27
-rw-r--r--devtools/client/performance/test/doc_worker.html29
-rw-r--r--devtools/client/performance/test/head.js117
-rw-r--r--devtools/client/performance/test/helpers/actions.js168
-rw-r--r--devtools/client/performance/test/helpers/dom-utils.js32
-rw-r--r--devtools/client/performance/test/helpers/event-utils.js118
-rw-r--r--devtools/client/performance/test/helpers/input-utils.js86
-rw-r--r--devtools/client/performance/test/helpers/moz.build20
-rw-r--r--devtools/client/performance/test/helpers/panel-utils.js111
-rw-r--r--devtools/client/performance/test/helpers/prefs.js78
-rw-r--r--devtools/client/performance/test/helpers/profiler-mm-utils.js82
-rw-r--r--devtools/client/performance/test/helpers/recording-utils.js54
-rw-r--r--devtools/client/performance/test/helpers/synth-utils.js152
-rw-r--r--devtools/client/performance/test/helpers/tab-utils.js44
-rw-r--r--devtools/client/performance/test/helpers/urls.js9
-rw-r--r--devtools/client/performance/test/helpers/wait-utils.js54
-rw-r--r--devtools/client/performance/test/js_simpleWorker.js6
-rw-r--r--devtools/client/performance/test/moz.build8
-rw-r--r--devtools/client/performance/test/xpcshell/.eslintrc.js6
-rw-r--r--devtools/client/performance/test/xpcshell/head.js44
-rw-r--r--devtools/client/performance/test/xpcshell/test_frame-utils-01.js293
-rw-r--r--devtools/client/performance/test/xpcshell/test_frame-utils-02.js87
-rw-r--r--devtools/client/performance/test/xpcshell/test_jit-graph-data.js238
-rw-r--r--devtools/client/performance/test/xpcshell/test_jit-model-01.js154
-rw-r--r--devtools/client/performance/test/xpcshell/test_jit-model-02.js196
-rw-r--r--devtools/client/performance/test/xpcshell/test_marker-blueprint.js34
-rw-r--r--devtools/client/performance/test/xpcshell/test_marker-utils.js204
-rw-r--r--devtools/client/performance/test/xpcshell/test_perf-utils-allocations-to-samples.js90
-rw-r--r--devtools/client/performance/test/xpcshell/test_profiler-categories.js32
-rw-r--r--devtools/client/performance/test/xpcshell/test_tree-model-01.js255
-rw-r--r--devtools/client/performance/test/xpcshell/test_tree-model-02.js84
-rw-r--r--devtools/client/performance/test/xpcshell/test_tree-model-03.js123
-rw-r--r--devtools/client/performance/test/xpcshell/test_tree-model-04.js126
-rw-r--r--devtools/client/performance/test/xpcshell/test_tree-model-05.js91
-rw-r--r--devtools/client/performance/test/xpcshell/test_tree-model-06.js213
-rw-r--r--devtools/client/performance/test/xpcshell/test_tree-model-07.js133
-rw-r--r--devtools/client/performance/test/xpcshell/test_tree-model-08.js250
-rw-r--r--devtools/client/performance/test/xpcshell/test_tree-model-09.js116
-rw-r--r--devtools/client/performance/test/xpcshell/test_tree-model-10.js157
-rw-r--r--devtools/client/performance/test/xpcshell/test_tree-model-11.js92
-rw-r--r--devtools/client/performance/test/xpcshell/test_tree-model-12.js103
-rw-r--r--devtools/client/performance/test/xpcshell/test_tree-model-13.js90
-rw-r--r--devtools/client/performance/test/xpcshell/test_tree-model-allocations-01.js131
-rw-r--r--devtools/client/performance/test/xpcshell/test_tree-model-allocations-02.js151
-rw-r--r--devtools/client/performance/test/xpcshell/test_waterfall-utils-collapse-01.js82
-rw-r--r--devtools/client/performance/test/xpcshell/test_waterfall-utils-collapse-02.js92
-rw-r--r--devtools/client/performance/test/xpcshell/test_waterfall-utils-collapse-03.js78
-rw-r--r--devtools/client/performance/test/xpcshell/test_waterfall-utils-collapse-04.js118
-rw-r--r--devtools/client/performance/test/xpcshell/test_waterfall-utils-collapse-05.js185
-rw-r--r--devtools/client/performance/test/xpcshell/xpcshell.ini35
-rw-r--r--devtools/client/performance/views/details-abstract-subview.js232
-rw-r--r--devtools/client/performance/views/details-js-call-tree.js234
-rw-r--r--devtools/client/performance/views/details-js-flamegraph.js143
-rw-r--r--devtools/client/performance/views/details-memory-call-tree.js145
-rw-r--r--devtools/client/performance/views/details-memory-flamegraph.js138
-rw-r--r--devtools/client/performance/views/details-waterfall.js282
-rw-r--r--devtools/client/performance/views/details.js295
-rw-r--r--devtools/client/performance/views/moz.build17
-rw-r--r--devtools/client/performance/views/overview.js457
-rw-r--r--devtools/client/performance/views/recordings.js249
-rw-r--r--devtools/client/performance/views/toolbar.js188
222 files changed, 26818 insertions, 0 deletions
diff --git a/devtools/client/performance/components/JITOptimizations.js b/devtools/client/performance/components/JITOptimizations.js
new file mode 100644
index 0000000000..3e924d28de
--- /dev/null
+++ b/devtools/client/performance/components/JITOptimizations.js
@@ -0,0 +1,274 @@
+/* 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 {
+ Component,
+ createFactory,
+} = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const Tree = createFactory(
+ require("devtools/client/shared/components/VirtualizedTree")
+);
+const OptimizationsItem = createFactory(
+ require("devtools/client/performance/components/JITOptimizationsItem")
+);
+const FrameView = createFactory(
+ require("devtools/client/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,
+};
+
+class JITOptimizations extends Component {
+ static get propTypes() {
+ return {
+ onViewSourceInDebugger: PropTypes.func.isRequired,
+ frameData: PropTypes.object.isRequired,
+ optimizationSites: PropTypes.arrayOf(optimizationSiteModel).isRequired,
+ autoExpandDepth: PropTypes.number,
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ autoExpandDepth: 0,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ expanded: new Set(),
+ };
+
+ this._createHeader = this._createHeader.bind(this);
+ this._createTree = this._createTree.bind(this);
+ }
+
+ /**
+ * 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({ frameData, onViewSourceInDebugger }) {
+ const { isMetaCategory, url, line } = frameData;
+ const name = isMetaCategory
+ ? frameData.categoryData.label
+ : frameData.functionName || "";
+
+ // 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: {
+ source: url,
+ line: +line,
+ functionDisplayName: name,
+ },
+ onClick: onViewSourceInDebugger,
+ });
+ }
+
+ return dom.div(
+ { className: "optimization-header" },
+ dom.span({ className: "header-title" }, JIT_TITLE),
+ dom.span({ className: "header-function-name" }, name),
+ frameComponent
+ );
+ }
+
+ _createTree(props) {
+ const {
+ autoExpandDepth,
+ frameData,
+ onViewSourceInDebugger,
+ optimizationSites: sites,
+ } = this.props;
+
+ const getSite = id => sites.find(site => site.id === id);
+ const getIonTypeForObserved = type => {
+ return getSite(type.id).data.types.find(iontype =>
+ (iontype.typeset || []).includes(type)
+ );
+ };
+ const isSite = site => getSite(site.id) === site;
+ const isAttempts = attempts =>
+ getSite(attempts.id).data.attempts === attempts;
+ const isAttempt = attempt =>
+ getSite(attempt.id).data.attempts.includes(attempt);
+ const isTypes = types => getSite(types.id).data.types === types;
+ const isType = type => getSite(type.id).data.types.includes(type);
+ const isObservedType = type => getIonTypeForObserved(type);
+
+ const 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
+ const getKey = node => {
+ const 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)) {
+ const iontype = getIonTypeForObserved(node);
+ return `${getKey(iontype)}-O-${iontype.typeset.indexOf(node)}`;
+ }
+ return "";
+ };
+
+ return Tree({
+ autoExpandDepth,
+ preventNavigationOnArrowRight: false,
+ getParent: node => {
+ const 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 => {
+ const expanded = new Set(state.expanded);
+ expanded.add(node);
+ return { expanded };
+ }),
+ onCollapse: node =>
+ this.setState(state => {
+ const 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() {
+ const header = this._createHeader(this.props);
+ const tree = this._createTree(this.props);
+
+ return dom.div({}, header, tree);
+ }
+}
+
+module.exports = JITOptimizations;
diff --git a/devtools/client/performance/components/JITOptimizationsItem.js b/devtools/client/performance/components/JITOptimizationsItem.js
new file mode 100644
index 0000000000..6f962a8606
--- /dev/null
+++ b/devtools/client/performance/components/JITOptimizationsItem.js
@@ -0,0 +1,228 @@
+/* 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 {
+ Component,
+ createFactory,
+} = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+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 */
+
+class JITOptimizationsItem extends Component {
+ static get propTypes() {
+ return {
+ onViewSourceInDebugger: PropTypes.func.isRequired,
+ frameData: PropTypes.object.isRequired,
+ type: PropTypes.oneOf(OPTIMIZATION_ITEM_TYPES).isRequired,
+ depth: PropTypes.number.isRequired,
+ arrow: PropTypes.element.isRequired,
+ item: PropTypes.object,
+ focused: PropTypes.bool,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this._renderSite = this._renderSite.bind(this);
+ this._renderAttempts = this._renderAttempts.bind(this);
+ this._renderTypes = this._renderTypes.bind(this);
+ this._renderAttempt = this._renderAttempt.bind(this);
+ this._renderType = this._renderType.bind(this);
+ this._renderObservedType = this._renderObservedType.bind(this);
+ }
+
+ _renderSite({ item: site, onViewSourceInDebugger, frameData }) {
+ const attempts = site.data.attempts;
+ const lastStrategy = attempts[attempts.length - 1].strategy;
+ let propString = "";
+ const 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})`;
+ }
+ }
+
+ const sampleString = PluralForm.get(site.samples, JIT_SAMPLES).replace(
+ "#1",
+ site.samples
+ );
+ const text = dom.span(
+ { className: "optimization-site-title" },
+ `${lastStrategy}${propString} – (${sampleString})`
+ );
+ const frame = Frame({
+ onClick: onViewSourceInDebugger,
+ frame: {
+ source: frameData.url,
+ line: +site.data.line,
+ column: site.data.column,
+ },
+ });
+ const 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 }) {
+ const success = isSuccessfulOutcome(attempt.outcome);
+ const { 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 }) {
+ const 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,
+ 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.
+ */
+ const {
+ 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/RecordingButton.js b/devtools/client/performance/components/RecordingButton.js
new file mode 100644
index 0000000000..f11b684a3d
--- /dev/null
+++ b/devtools/client/performance/components/RecordingButton.js
@@ -0,0 +1,45 @@
+/* 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 { Component } = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const { button } = dom;
+
+class RecordingButton extends Component {
+ static get propTypes() {
+ return {
+ onRecordButtonClick: PropTypes.func.isRequired,
+ isRecording: PropTypes.bool,
+ isLocked: PropTypes.bool,
+ };
+ }
+
+ render() {
+ const { onRecordButtonClick, isRecording, isLocked } = this.props;
+
+ const 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")
+ );
+ }
+}
+
+module.exports = RecordingButton;
diff --git a/devtools/client/performance/components/RecordingControls.js b/devtools/client/performance/components/RecordingControls.js
new file mode 100644
index 0000000000..8c753c45e6
--- /dev/null
+++ b/devtools/client/performance/components/RecordingControls.js
@@ -0,0 +1,63 @@
+/* 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 { Component } = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const { div, button } = dom;
+
+class RecordingControls extends Component {
+ static get propTypes() {
+ return {
+ onClearButtonClick: PropTypes.func.isRequired,
+ onRecordButtonClick: PropTypes.func.isRequired,
+ onImportButtonClick: PropTypes.func.isRequired,
+ isRecording: PropTypes.bool,
+ isLocked: PropTypes.bool,
+ };
+ }
+
+ render() {
+ const {
+ onClearButtonClick,
+ onRecordButtonClick,
+ onImportButtonClick,
+ isRecording,
+ isLocked,
+ } = this.props;
+
+ const recordButtonClassList = ["devtools-button", "record-button"];
+
+ if (isRecording) {
+ recordButtonClassList.push("checked");
+ }
+
+ return div(
+ { className: "devtools-toolbar" },
+ 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,
+ })
+ );
+ }
+}
+
+module.exports = RecordingControls;
diff --git a/devtools/client/performance/components/RecordingList.js b/devtools/client/performance/components/RecordingList.js
new file mode 100644
index 0000000000..161b612232
--- /dev/null
+++ b/devtools/client/performance/components/RecordingList.js
@@ -0,0 +1,32 @@
+/* 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 { Component } = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const { L10N } = require("devtools/client/performance/modules/global");
+const { ul, div } = dom;
+
+class RecordingList extends Component {
+ static get propTypes() {
+ return {
+ items: PropTypes.arrayOf(PropTypes.object).isRequired,
+ itemComponent: PropTypes.func.isRequired,
+ };
+ }
+
+ 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")
+ );
+ }
+}
+
+module.exports = RecordingList;
diff --git a/devtools/client/performance/components/RecordingListItem.js b/devtools/client/performance/components/RecordingListItem.js
new file mode 100644
index 0000000000..e714aeebf2
--- /dev/null
+++ b/devtools/client/performance/components/RecordingListItem.js
@@ -0,0 +1,65 @@
+/* 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 { Component } = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const { div, li, span, button } = dom;
+const { L10N } = require("devtools/client/performance/modules/global");
+
+class RecordingListItem extends Component {
+ static get propTypes() {
+ return {
+ label: PropTypes.string.isRequired,
+ duration: PropTypes.string,
+ onSelect: PropTypes.func.isRequired,
+ onSave: PropTypes.func.isRequired,
+ isLoading: PropTypes.bool,
+ isSelected: PropTypes.bool,
+ isRecording: PropTypes.bool,
+ };
+ }
+
+ 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")
+ )
+ )
+ );
+ }
+}
+
+module.exports = RecordingListItem;
diff --git a/devtools/client/performance/components/Waterfall.js b/devtools/client/performance/components/Waterfall.js
new file mode 100644
index 0000000000..fe49948a8a
--- /dev/null
+++ b/devtools/client/performance/components/Waterfall.js
@@ -0,0 +1,42 @@
+/* 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 { createFactory } = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const WaterfallHeader = createFactory(
+ require("devtools/client/performance/components/WaterfallHeader")
+);
+const WaterfallTree = createFactory(
+ require("devtools/client/performance/components/WaterfallTree")
+);
+
+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;
diff --git a/devtools/client/performance/components/WaterfallHeader.js b/devtools/client/performance/components/WaterfallHeader.js
new file mode 100644
index 0000000000..eac566506b
--- /dev/null
+++ b/devtools/client/performance/components/WaterfallHeader.js
@@ -0,0 +1,73 @@
+/* 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 PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const { L10N } = require("devtools/client/performance/modules/global");
+const {
+ TickUtils,
+} = require("devtools/client/performance/modules/waterfall-ticks");
+
+const WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms
+const WATERFALL_HEADER_TICKS_SPACING_MIN = 50; // px
+const WATERFALL_HEADER_TEXT_PADDING = 3; // px
+
+function WaterfallHeader(props) {
+ const { startTime, dataScale, sidebarWidth, waterfallWidth } = props;
+
+ const tickInterval = TickUtils.findOptimalTickInterval({
+ ticksMultiple: WATERFALL_HEADER_TICKS_MULTIPLE,
+ ticksSpacingMin: WATERFALL_HEADER_TICKS_SPACING_MIN,
+ dataScale: dataScale,
+ });
+
+ const ticks = [];
+ for (let x = 0; x < waterfallWidth; x += tickInterval) {
+ const left = x + WATERFALL_HEADER_TEXT_PADDING;
+ const time = Math.round(x / dataScale + startTime);
+ const label = L10N.getFormatStr("timeline.tick", time);
+
+ const node = dom.div(
+ {
+ key: x,
+ 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/WaterfallTree.js b/devtools/client/performance/components/WaterfallTree.js
new file mode 100644
index 0000000000..c235f90b33
--- /dev/null
+++ b/devtools/client/performance/components/WaterfallTree.js
@@ -0,0 +1,189 @@
+/* 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 {
+ Component,
+ createFactory,
+} = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const Tree = createFactory(
+ require("devtools/client/shared/components/VirtualizedTree")
+);
+const WaterfallTreeRow = createFactory(
+ require("devtools/client/performance/components/WaterfallTreeRow")
+);
+
+// Keep in sync with var(--waterfall-tree-row-height) in performance.css
+const WATERFALL_TREE_ROW_HEIGHT = 15; // px
+
+/**
+ * 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) {
+ const mStart = e.start | 0;
+ const 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)
+ );
+}
+
+class WaterfallTree extends Component {
+ static get propTypes() {
+ return {
+ 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,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ focused: null,
+ expanded: new Set(),
+ };
+
+ this._getRoots = this._getRoots.bind(this);
+ this._getParent = this._getParent.bind(this);
+ this._getChildren = this._getChildren.bind(this);
+ this._getKey = this._getKey.bind(this);
+ this._isExpanded = this._isExpanded.bind(this);
+ this._onExpand = this._onExpand.bind(this);
+ this._onCollapse = this._onCollapse.bind(this);
+ this._onFocus = this._onFocus.bind(this);
+ this._filter = this._filter.bind(this);
+ this._renderItem = this._renderItem.bind(this);
+ }
+
+ _getRoots(node) {
+ const 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 (const submarker of marker.submarkers) {
+ if (submarker === node) {
+ return marker;
+ }
+
+ const parent = findParent(submarker);
+ if (parent) {
+ return parent;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ const rootMarker = this.props.marker;
+ const 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) {
+ const 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 => {
+ const expanded = new Set(state.expanded);
+ expanded.add(node);
+ return { expanded };
+ });
+ }
+
+ _onCollapse(node) {
+ this.setState(state => {
+ const 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) {
+ const { startTime, endTime } = this.props;
+ return isMarkerInRange(node, startTime, endTime);
+ }
+
+ _renderItem(marker, level, focused, arrow, expanded) {
+ const { startTime, dataScale, sidebarWidth } = this.props;
+ return WaterfallTreeRow({
+ marker,
+ level,
+ arrow,
+ expanded,
+ focused,
+ startTime,
+ dataScale,
+ sidebarWidth,
+ });
+ }
+
+ render() {
+ return Tree({
+ preventNavigationOnArrowRight: false,
+ 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/WaterfallTreeRow.js b/devtools/client/performance/components/WaterfallTreeRow.js
new file mode 100644
index 0000000000..745bb5d23e
--- /dev/null
+++ b/devtools/client/performance/components/WaterfallTreeRow.js
@@ -0,0 +1,116 @@
+/* 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 PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const {
+ MarkerBlueprintUtils,
+} = require("devtools/client/performance/modules/marker-blueprint-utils");
+
+const LEVEL_INDENT = 10; // px
+const ARROW_NODE_OFFSET = -14; // px
+const WATERFALL_MARKER_TIMEBAR_WIDTH_MIN = 5; // px
+
+function buildMarkerSidebar(blueprint, props) {
+ const { marker, level, sidebarWidth } = props;
+
+ const bullet = dom.div({
+ className: `waterfall-marker-bullet marker-color-${blueprint.colorName}`,
+ style: { transform: `translateX(${level * LEVEL_INDENT}px)` },
+ "data-type": marker.name,
+ });
+
+ const label = MarkerBlueprintUtils.getMarkerLabel(marker);
+
+ const 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
+ );
+
+ const 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);
+
+ const attrs = {
+ className: "waterfall-tree-item" + (focused ? " focused" : ""),
+ "data-otmt": marker.isOffMainThread,
+ };
+
+ // Don't render an expando-arrow for leaf nodes.
+ const submarkers = marker.submarkers;
+ const 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/chrome/chrome.ini b/devtools/client/performance/components/chrome/chrome.ini
new file mode 100644
index 0000000000..5ba24a9af7
--- /dev/null
+++ b/devtools/client/performance/components/chrome/chrome.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+support-files =
+ head.js
+
+[test_jit_optimizations_01.html]
diff --git a/devtools/client/performance/components/chrome/head.js b/devtools/client/performance/components/chrome/head.js
new file mode 100644
index 0000000000..4b6627001e
--- /dev/null
+++ b/devtools/client/performance/components/chrome/head.js
@@ -0,0 +1,245 @@
+/* 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 */
+const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+let { Assert } = require("resource://testing-common/Assert.jsm");
+const { BrowserLoader } = ChromeUtils.import(
+ "resource://devtools/client/shared/browser-loader.js"
+);
+const flags = require("devtools/shared/flags");
+let { TargetFactory } = require("devtools/client/framework/target");
+let { Toolbox } = require("devtools/client/framework/toolbox");
+
+let { require: browserRequire } = BrowserLoader({
+ baseURI: "resource://devtools/client/performance/",
+ window,
+});
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+
+const $ = (selector, scope = document) => scope.querySelector(selector);
+const $$ = (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) {
+ return new Promise(resolve => {
+ component.setState(newState, onNextAnimationFrame(resolve));
+ });
+}
+
+function setProps(component, newState) {
+ return new Promise(resolve => {
+ component.setProps(newState, onNextAnimationFrame(resolve));
+ });
+}
+
+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.
+ */
+const 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) {
+ const rows = $$(".tree .tree-node");
+
+ for (let i = 0; i < rowData.length; i++) {
+ const row = rows[i];
+ const 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/chrome/test_jit_optimizations_01.html b/devtools/client/performance/components/chrome/test_jit_optimizations_01.html
new file mode 100644
index 0000000000..cf2823d387
--- /dev/null
+++ b/devtools/client/performance/components/chrome/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 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"></script>
+<script type="application/javascript">
+"use strict";
+
+window.onload = async function () {
+ try {
+ const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+ const React = browserRequire("devtools/client/shared/vendor/react");
+ const JITOptimizations = React.createFactory(browserRequire("devtools/client/performance/components/JITOptimizations"));
+ ok(JITOptimizations, "Should get JITOptimizations");
+ const 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);
+ await 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/moz.build b/devtools/client/performance/components/moz.build
new file mode 100644
index 0000000000..dc9ccff784
--- /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(
+ "JITOptimizations.js",
+ "JITOptimizationsItem.js",
+ "RecordingButton.js",
+ "RecordingControls.js",
+ "RecordingList.js",
+ "RecordingListItem.js",
+ "Waterfall.js",
+ "WaterfallHeader.js",
+ "WaterfallTree.js",
+ "WaterfallTreeRow.js",
+)
+
+MOCHITEST_CHROME_MANIFESTS += ["chrome/chrome.ini"]
diff --git a/devtools/client/performance/docs/markers.md b/devtools/client/performance/docs/markers.md
new file mode 100644
index 0000000000..0053837321
--- /dev/null
+++ b/devtools/client/performance/docs/markers.md
@@ -0,0 +1,171 @@
+# Timeline Markers
+
+## Common
+
+* DOMHighResTimeStamp start
+* DOMHighResTimeStamp end
+* DOMString name
+* object? stack
+* object? endStack
+* unsigned short processType;
+* boolean isOffMainThread;
+
+The `processType` a GeckoProcessType enum listed in xpcom/build/nsXULAppAPI.h,
+specifying if this marker originates in a content, chrome, plugin etc. process.
+Additionally, markers may be created from any thread on those processes, and
+`isOffMainThread` highights whether or not they're from the main thread. The most
+common type of marker is probably going to be from a GeckoProcessType_Content's
+main thread when debugging content.
+
+## DOMEvent
+
+Triggered when a DOM event occurs, like a click or a keypress.
+
+* unsigned short eventPhase - a number indicating what phase this event is
+ in (target, bubbling, capturing, maps to Event constants)
+* DOMString type - the type of event, like "keypress" or "click"
+
+## Reflow
+
+Reflow markers (labeled as "Layout") indicate when a change has occurred to
+a DOM element's positioning that requires the frame tree (rendering
+representation of the DOM) to figure out the new position of a handful of
+elements. Fired via `PresShell::DoReflow`
+
+## Paint
+
+* sequence<{ long height, long width, long x, long y }> rectangles - An array
+ of rectangle objects indicating where painting has occurred.
+
+## Styles
+
+Style markers (labeled as "Recalculating Styles") are triggered when Gecko
+needs to figure out the computational style of an element. Fired via
+`RestyleTracker::DoProcessRestyles` when there are elements to restyle.
+
+## Javascript
+
+`Javascript` markers are emitted indicating when JS execution begins and ends,
+with a reason that triggered it (causeName), like a requestAnimationFrame or
+a setTimeout.
+
+* string causeName - The reason that JS was entered. There are many possible
+ reasons, and the interesting ones to show web developers (triggered by content) are:
+
+ * "\<script\> element"
+ * "EventListener.handleEvent"
+ * "setInterval handler"
+ * "setTimeout handler"
+ * "FrameRequestCallback"
+ * "EventHandlerNonNull"
+ * "promise callback"
+ * "promise initializer"
+ * "Worker runnable"
+
+ There are also many more potential JS causes, some which are just internally
+ used and won't emit a marker, but the below ones are only of interest to
+ Gecko hackers, most likely
+
+ * "promise thenable"
+ * "worker runnable"
+ * "nsHTTPIndex set HTTPIndex property"
+ * "XPCWrappedJS method call"
+ * "nsHTTPIndex OnFTPControlLog"
+ * "XPCWrappedJS QueryInterface"
+ * "xpcshell argument processing”
+ * "XPConnect sandbox evaluation "
+ * "component loader report global"
+ * "component loader load module"
+ * "Cross-Process Object Wrapper call/construct"
+ * "Cross-Process Object Wrapper ’set'"
+ * "Cross-Process Object Wrapper 'get'"
+ * "nsXULTemplateBuilder creation"
+ * "TestShellCommand"
+ * "precompiled XUL \<script\> element"
+ * "XBL \<field\> initialization "
+ * "NPAPI NPN_evaluate"
+ * "NPAPI get"
+ * "NPAPI set"
+ * "NPAPI doInvoke"
+ * "javascript: URI"
+ * "geolocation.always_precise indexing"
+ * "geolocation.app_settings enumeration"
+ * "WebIDL dictionary creation"
+ * "XBL \<constructor\>/\<destructor\> invocation"
+ * "message manager script load"
+ * "message handler script load"
+ * "nsGlobalWindow report new global"
+
+## GarbageCollection
+
+Emitted after a full GC cycle has completed (which is after any number of
+incremental slices).
+
+* DOMString causeName - The reason for a GC event to occur. A full list of
+ GC reasons can be found [on MDN](https://developer.mozilla.org/en-US/docs/Tools/Debugger-API/Debugger.Memory#Debugger.Memory_Handler_Functions).
+* DOMString nonincremenetalReason - If the GC could not do an incremental
+ GC (smaller, quick GC events), and we have to walk the entire heap and
+ GC everything marked, then the reason listed here is why.
+
+## nsCycleCollector::Collect
+
+An `nsCycleCollector::Collect` marker is emitted for each incremental cycle
+collection slice and each non-incremental cycle collection.
+
+# nsCycleCollector::ForgetSkippable
+
+`nsCycleCollector::ForgetSkippable` is presented as "Cycle Collection", but in
+reality it is preparation/pre-optimization for cycle collection, and not the
+actual tracing of edges and collecting of cycles.
+
+## ConsoleTime
+
+A marker generated via `console.time()` and `console.timeEnd()`.
+
+* DOMString causeName - the label passed into `console.time(label)` and
+ `console.timeEnd(label)` if passed in.
+
+## TimeStamp
+
+A marker generated via `console.timeStamp(label)`.
+
+* DOMString causeName - the label passed into `console.timeStamp(label)`
+ if passed in.
+
+## document::DOMContentLoaded
+
+A marker generated when the DOMContentLoaded event is fired.
+
+## document::Load
+
+A marker generated when the document's "load" event is fired.
+
+## Parse HTML
+
+## Parse XML
+
+## Worker
+
+Emitted whenever there's an operation dealing with Workers (any kind of worker,
+Web Workers, Service Workers etc.). Currently there are 4 types of operations
+being tracked: serializing/deserializing data on the main thread, and also
+serializing/deserializing data off the main thread.
+
+* ProfileTimelineWorkerOperationType operationType - the type of operation
+ being done by the Worker or the main thread when dealing with workers.
+ Can be one of the following enums defined in ProfileTimelineMarker.webidl
+ * "serializeDataOffMainThread"
+ * "serializeDataOnMainThread"
+ * "deserializeDataOffMainThread"
+ * "deserializeDataOnMainThread"
+
+## Composite
+
+Composite markers trace the actual time an inner composite operation
+took on the compositor thread. Currently, these markers are only especially
+interesting for Gecko platform developers, and thus disabled by default.
+
+## CompositeForwardTransaction
+
+Markers generated when the IPC request was made to the compositor from
+the child process's main thread.
diff --git a/devtools/client/performance/events.js b/devtools/client/performance/events.js
new file mode 100644
index 0000000000..c6de3339c9
--- /dev/null
+++ b/devtools/client/performance/events.js
@@ -0,0 +1,110 @@
+/* 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 ControllerEvents = {
+ // Fired when a performance pref changes (either because the user changed it
+ // via the tool's UI, by changing something about:config or programatically).
+ PREF_CHANGED: "Performance:PrefChanged",
+
+ // Fired when the devtools theme changes.
+ THEME_CHANGED: "Performance:ThemeChanged",
+
+ // When a new recording model is received by the controller.
+ RECORDING_ADDED: "Performance:RecordingAdded",
+
+ // When a recording model gets removed from the controller.
+ RECORDING_DELETED: "Performance:RecordingDeleted",
+
+ // When a recording model becomes "started", "stopping" or "stopped".
+ RECORDING_STATE_CHANGE: "Performance:RecordingStateChange",
+
+ // When a recording is offering information on the profiler's circular buffer.
+ RECORDING_PROFILER_STATUS_UPDATE: "Performance:RecordingProfilerStatusUpdate",
+
+ // When a recording model becomes marked as selected.
+ RECORDING_SELECTED: "Performance:RecordingSelected",
+
+ // When starting a recording is attempted and fails because the backend
+ // does not permit it at this time.
+ BACKEND_FAILED_AFTER_RECORDING_START:
+ "Performance:BackendFailedRecordingStart",
+
+ // When a recording is started and the backend has started working.
+ BACKEND_READY_AFTER_RECORDING_START: "Performance:BackendReadyRecordingStart",
+
+ // When a recording is stopped and the backend has finished cleaning up.
+ BACKEND_READY_AFTER_RECORDING_STOP: "Performance:BackendReadyRecordingStop",
+
+ // When a recording is exported.
+ RECORDING_EXPORTED: "Performance:RecordingExported",
+
+ // When a recording is imported.
+ RECORDING_IMPORTED: "Performance:RecordingImported",
+
+ // When a source is shown in the JavaScript Debugger at a specific location.
+ SOURCE_SHOWN_IN_JS_DEBUGGER: "Performance:UI:SourceShownInJsDebugger",
+ SOURCE_NOT_FOUND_IN_JS_DEBUGGER: "Performance:UI:SourceNotFoundInJsDebugger",
+
+ // Fired by the PerformanceController when `populateWithRecordings` is finished.
+ RECORDINGS_SEEDED: "Performance:RecordingsSeeded",
+};
+
+const ViewEvents = {
+ // Emitted by the `ToolbarView` when a preference changes.
+ UI_PREF_CHANGED: "Performance:UI:PrefChanged",
+
+ // When the state (display mode) changes, for example when switching between
+ // "empty", "recording" or "recorded". This causes certain parts of the UI
+ // to be hidden or visible.
+ UI_STATE_CHANGED: "Performance:UI:StateChanged",
+
+ // Emitted by the `PerformanceView` on clear button click.
+ UI_CLEAR_RECORDINGS: "Performance:UI:ClearRecordings",
+
+ // Emitted by the `PerformanceView` on record button click.
+ UI_START_RECORDING: "Performance:UI:StartRecording",
+ UI_STOP_RECORDING: "Performance:UI:StopRecording",
+
+ // Emitted by the `PerformanceView` on import/export button click.
+ UI_IMPORT_RECORDING: "Performance:UI:ImportRecording",
+ UI_EXPORT_RECORDING: "Performance:UI:ExportRecording",
+
+ // Emitted by the `PerformanceView` when the profiler's circular buffer
+ // status has been rendered.
+ UI_RECORDING_PROFILER_STATUS_RENDERED:
+ "Performance:UI:RecordingProfilerStatusRendered",
+
+ // When a recording is selected in the UI.
+ UI_RECORDING_SELECTED: "Performance:UI:RecordingSelected",
+
+ // Emitted by the `DetailsView` when a subview is selected
+ UI_DETAILS_VIEW_SELECTED: "Performance:UI:DetailsViewSelected",
+
+ // Emitted by the `OverviewView` after something has been rendered.
+ UI_OVERVIEW_RENDERED: "Performance:UI:OverviewRendered",
+ UI_MARKERS_GRAPH_RENDERED: "Performance:UI:OverviewMarkersRendered",
+ UI_MEMORY_GRAPH_RENDERED: "Performance:UI:OverviewMemoryRendered",
+ UI_FRAMERATE_GRAPH_RENDERED: "Performance:UI:OverviewFramerateRendered",
+
+ // Emitted by the `OverviewView` when a range has been selected in the graphs.
+ UI_OVERVIEW_RANGE_SELECTED: "Performance:UI:OverviewRangeSelected",
+
+ // Emitted by the `WaterfallView` when it has been rendered.
+ UI_WATERFALL_RENDERED: "Performance:UI:WaterfallRendered",
+
+ // Emitted by the `JsCallTreeView` when it has been rendered.
+ UI_JS_CALL_TREE_RENDERED: "Performance:UI:JsCallTreeRendered",
+
+ // Emitted by the `JsFlameGraphView` when it has been rendered.
+ UI_JS_FLAMEGRAPH_RENDERED: "Performance:UI:JsFlameGraphRendered",
+
+ // Emitted by the `MemoryCallTreeView` when it has been rendered.
+ UI_MEMORY_CALL_TREE_RENDERED: "Performance:UI:MemoryCallTreeRendered",
+
+ // Emitted by the `MemoryFlameGraphView` when it has been rendered.
+ UI_MEMORY_FLAMEGRAPH_RENDERED: "Performance:UI:MemoryFlameGraphRendered",
+};
+
+module.exports = Object.assign({}, ControllerEvents, ViewEvents);
diff --git a/devtools/client/performance/index.xhtml b/devtools/client/performance/index.xhtml
new file mode 100644
index 0000000000..4ef54d0662
--- /dev/null
+++ b/devtools/client/performance/index.xhtml
@@ -0,0 +1,354 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/content/shared/toolbarbutton.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/widgets.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/performance.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/jit-optimizations.css" type="text/css"?>
+<?xml-stylesheet href="chrome://devtools/skin/components-frame.css" type="text/css"?>
+<!DOCTYPE window [
+ <!ENTITY % performanceDTD SYSTEM "chrome://devtools/locale/performance.dtd">
+ %performanceDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+ <script src="chrome://devtools/content/shared/theme-switching.js"/>
+ <script src="resource://devtools/client/performance/initializer.js"/>
+
+ <popupset id="performance-options-popupset">
+ <menupopup id="performance-filter-menupopup" position="before_start"/>
+ <menupopup id="performance-options-menupopup" position="before_end">
+ <menuitem id="option-show-platform-data"
+ type="checkbox"
+ data-pref="show-platform-data"
+ label="&performanceUI.showPlatformData;"
+ tooltiptext="&performanceUI.showPlatformData.tooltiptext;"/>
+ <menuitem id="option-show-jit-optimizations"
+ class="experimental-option"
+ type="checkbox"
+ data-pref="show-jit-optimizations"
+ label="&performanceUI.showJITOptimizations;"
+ tooltiptext="&performanceUI.showJITOptimizations.tooltiptext;"/>
+ <menuitem id="option-enable-memory"
+ class="experimental-option"
+ type="checkbox"
+ data-pref="enable-memory"
+ label="&performanceUI.enableMemory;"
+ tooltiptext="&performanceUI.enableMemory.tooltiptext;"/>
+ <menuitem id="option-enable-allocations"
+ type="checkbox"
+ data-pref="enable-allocations"
+ label="&performanceUI.enableAllocations;"
+ tooltiptext="&performanceUI.enableAllocations.tooltiptext;"/>
+ <menuitem id="option-enable-framerate"
+ type="checkbox"
+ data-pref="enable-framerate"
+ label="&performanceUI.enableFramerate;"
+ tooltiptext="&performanceUI.enableFramerate.tooltiptext;"/>
+ <menuitem id="option-invert-call-tree"
+ type="checkbox"
+ data-pref="invert-call-tree"
+ label="&performanceUI.invertTree;"
+ tooltiptext="&performanceUI.invertTree.tooltiptext;"/>
+ <menuitem id="option-invert-flame-graph"
+ type="checkbox"
+ data-pref="invert-flame-graph"
+ label="&performanceUI.invertFlameGraph;"
+ tooltiptext="&performanceUI.invertFlameGraph.tooltiptext;"/>
+ <menuitem id="option-flatten-tree-recursion"
+ type="checkbox"
+ data-pref="flatten-tree-recursion"
+ label="&performanceUI.flattenTreeRecursion;"
+ tooltiptext="&performanceUI.flattenTreeRecursion.tooltiptext;"/>
+ </menupopup>
+ </popupset>
+
+ <hbox id="body" class="theme-body performance-tool" flex="1">
+
+ <!-- Sidebar: controls and recording list -->
+ <vbox id="recordings-pane">
+ <hbox id="recordings-controls">
+ <html:div id='recording-controls-mount'/>
+ </hbox>
+ <vbox id="recordings-list" class="theme-sidebar" flex="1">
+ <html:div id="recording-list-mount"/>
+ </vbox>
+ </vbox>
+
+ <!-- Main panel content -->
+ <vbox id="performance-pane" flex="1">
+
+ <!-- Top toolbar controls -->
+ <toolbar id="performance-toolbar"
+ class="devtools-toolbar">
+ <hbox id="performance-toolbar-controls-other"
+ class="devtools-toolbarbutton-group">
+ <toolbarbutton id="filter-button"
+ class="devtools-toolbarbutton"
+ popup="performance-filter-menupopup"
+ tooltiptext="&performanceUI.options.filter.tooltiptext;"/>
+ </hbox>
+ <hbox id="performance-toolbar-controls-detail-views"
+ class="devtools-toolbarbutton-group">
+ <toolbarbutton id="select-waterfall-view"
+ class="devtools-toolbarbutton"
+ label="&performanceUI.toolbar.waterfall;"
+ hidden="true"
+ data-view="waterfall"
+ tooltiptext="&performanceUI.toolbar.waterfall.tooltiptext;" />
+ <toolbarbutton id="select-js-calltree-view"
+ class="devtools-toolbarbutton"
+ label="&performanceUI.toolbar.js-calltree;"
+ hidden="true"
+ data-view="js-calltree"
+ tooltiptext="&performanceUI.toolbar.js-calltree.tooltiptext;" />
+ <toolbarbutton id="select-js-flamegraph-view"
+ class="devtools-toolbarbutton"
+ label="&performanceUI.toolbar.js-flamegraph;"
+ hidden="true"
+ data-view="js-flamegraph"
+ tooltiptext="&performanceUI.toolbar.js-flamegraph.tooltiptext;" />
+ <toolbarbutton id="select-memory-calltree-view"
+ class="devtools-toolbarbutton"
+ label="&performanceUI.toolbar.memory-calltree;"
+ hidden="true"
+ data-view="memory-calltree"
+ tooltiptext="&performanceUI.toolbar.allocations.tooltiptext;" />
+ <toolbarbutton id="select-memory-flamegraph-view"
+ class="devtools-toolbarbutton"
+ label="&performanceUI.toolbar.memory-flamegraph;"
+ hidden="true"
+ data-view="memory-flamegraph" />
+ </hbox>
+ <spacer flex="1"></spacer>
+ <hbox id="performance-toolbar-controls-options"
+ class="devtools-toolbarbutton-group">
+ <toolbarbutton id="performance-options-button"
+ class="devtools-toolbarbutton devtools-option-toolbarbutton"
+ popup="performance-options-menupopup"
+ tooltiptext="&performanceUI.options.gear.tooltiptext;"/>
+ </hbox>
+ </toolbar>
+
+ <!-- Recording contents and general notice messages -->
+ <deck id="performance-view" flex="1">
+
+ <!-- A default notice, shown while initially opening the tool.
+ Keep this element the first child of #performance-view. -->
+ <hbox id="tool-loading-notice"
+ class="notice-container"
+ flex="1">
+ </hbox>
+
+ <!-- "Unavailable" notice, shown when the entire tool is disabled,
+ for example, when in private browsing mode. -->
+ <vbox id="unavailable-notice"
+ class="notice-container"
+ align="center"
+ pack="center"
+ flex="1">
+ <hbox pack="center">
+ <html:div class='recording-button-mount'/>
+ </hbox>
+ <description class="tool-disabled-message">
+ &performanceUI.unavailableNoticePB;
+ </description>
+ </vbox>
+
+ <!-- "Empty" notice, shown when there's no recordings available -->
+ <hbox id="empty-notice"
+ class="notice-container"
+ align="center"
+ pack="center"
+ flex="1">
+ <hbox pack="center">
+ <html:div class='recording-button-mount'/>
+ </hbox>
+ </hbox>
+
+ <!-- Recording contents -->
+ <vbox id="performance-view-content" flex="1">
+
+ <!-- Overview graphs -->
+ <vbox id="overview-pane">
+ <hbox id="markers-overview"/>
+ <hbox id="memory-overview"/>
+ <hbox id="time-framerate"/>
+ </vbox>
+
+ <!-- Detail views and specific notice messages -->
+ <deck id="details-pane-container" flex="1">
+
+ <!-- "Loading" notice, shown when a recording is being loaded -->
+ <hbox id="loading-notice"
+ class="notice-container devtools-throbber"
+ align="center"
+ pack="center"
+ flex="1">
+ <label value="&performanceUI.loadingNotice;"/>
+ </hbox>
+
+ <!-- "Recording" notice, shown when a recording is in progress -->
+ <vbox id="recording-notice"
+ class="notice-container"
+ align="center"
+ pack="center"
+ flex="1">
+ <hbox pack="center">
+ <html:div class='recording-button-mount'/>
+ </hbox>
+ <label class="realtime-disabled-on-e10s-message"
+ value="&performanceUI.disabledRealTime.disabledE10S;"/>
+ <label class="buffer-status-message"
+ tooltiptext="&performanceUI.bufferStatusTooltip;"/>
+ <label class="buffer-status-message-full"
+ value="&performanceUI.bufferStatusFull;"/>
+ </vbox>
+
+ <!-- "Console" notice, shown when a console recording is in progress -->
+ <vbox id="console-recording-notice"
+ class="notice-container"
+ align="center"
+ pack="center"
+ flex="1">
+ <hbox class="console-profile-recording-notice">
+ <label value="&performanceUI.console.recordingNoticeStart;" />
+ <label class="console-profile-command" />
+ <label value="&performanceUI.console.recordingNoticeEnd;" />
+ </hbox>
+ <hbox class="console-profile-stop-notice">
+ <label value="&performanceUI.console.stopCommandStart;" />
+ <label class="console-profile-command" />
+ <label value="&performanceUI.console.stopCommandEnd;" />
+ </hbox>
+ <label class="realtime-disabled-on-e10s-message"
+ value="&performanceUI.disabledRealTime.disabledE10S;"/>
+ <label class="buffer-status-message"
+ tooltiptext="&performanceUI.bufferStatusTooltip;"/>
+ <label class="buffer-status-message-full"
+ value="&performanceUI.bufferStatusFull;"/>
+ </vbox>
+
+ <!-- Detail views -->
+ <deck id="details-pane" flex="1">
+
+ <!-- Waterfall -->
+ <hbox id="waterfall-view" flex="1">
+ <html:div xmlns="http://www.w3.org/1999/xhtml" id="waterfall-tree" />
+ <splitter class="devtools-side-splitter"/>
+ <vbox id="waterfall-details"
+ class="theme-sidebar"/>
+ </hbox>
+
+ <!-- JS Tree and JIT view -->
+ <hbox id="js-profile-view" flex="1">
+ <vbox id="js-calltree-view" flex="1">
+ <hbox class="call-tree-headers-container">
+ <label class="plain call-tree-header"
+ type="duration"
+ crop="end"
+ value="&performanceUI.table.totalDuration;"
+ tooltiptext="&performanceUI.table.totalDuration.tooltip;"/>
+ <label class="plain call-tree-header"
+ type="percentage"
+ crop="end"
+ value="&performanceUI.table.totalPercentage;"
+ tooltiptext="&performanceUI.table.totalPercentage.tooltip;"/>
+ <label class="plain call-tree-header"
+ type="self-duration"
+ crop="end"
+ value="&performanceUI.table.selfDuration;"
+ tooltiptext="&performanceUI.table.selfDuration.tooltip;"/>
+ <label class="plain call-tree-header"
+ type="self-percentage"
+ crop="end"
+ value="&performanceUI.table.selfPercentage;"
+ tooltiptext="&performanceUI.table.selfPercentage.tooltip;"/>
+ <label class="plain call-tree-header"
+ type="samples"
+ crop="end"
+ value="&performanceUI.table.samples;"
+ tooltiptext="&performanceUI.table.samples.tooltip;"/>
+ <label class="plain call-tree-header"
+ type="function"
+ crop="end"
+ value="&performanceUI.table.function;"
+ tooltiptext="&performanceUI.table.function.tooltip;"/>
+ </hbox>
+ <vbox class="call-tree-cells-container" flex="1"/>
+ </vbox>
+ <splitter class="devtools-side-splitter"/>
+ <!-- Optimizations Panel -->
+ <vbox id="jit-optimizations-view"
+ class="hidden">
+ </vbox>
+ </hbox>
+
+ <!-- JS FlameChart -->
+ <hbox id="js-flamegraph-view" flex="1">
+ </hbox>
+
+ <!-- Memory Tree -->
+ <vbox id="memory-calltree-view" flex="1">
+ <hbox class="call-tree-headers-container">
+ <label class="plain call-tree-header"
+ type="self-size"
+ crop="end"
+ value="Self Bytes"
+ tooltiptext="&performanceUI.table.totalAlloc.tooltip;"/>
+ <label class="plain call-tree-header"
+ type="self-size-percentage"
+ crop="end"
+ value="Self Bytes %"
+ tooltiptext="&performanceUI.table.totalAlloc.tooltip;"/>
+ <label class="plain call-tree-header"
+ type="self-count"
+ crop="end"
+ value="Self Count"
+ tooltiptext="&performanceUI.table.totalAlloc.tooltip;"/>
+ <label class="plain call-tree-header"
+ type="self-count-percentage"
+ crop="end"
+ value="Self Count %"
+ tooltiptext="&performanceUI.table.totalAlloc.tooltip;"/>
+ <label class="plain call-tree-header"
+ type="size"
+ crop="end"
+ value="Total Size"
+ tooltiptext="&performanceUI.table.totalAlloc.tooltip;"/>
+ <label class="plain call-tree-header"
+ type="size-percentage"
+ crop="end"
+ value="Total Size %"
+ tooltiptext="&performanceUI.table.totalAlloc.tooltip;"/>
+ <label class="plain call-tree-header"
+ type="count"
+ crop="end"
+ value="Total Count"
+ tooltiptext="&performanceUI.table.totalAlloc.tooltip;"/>
+ <label class="plain call-tree-header"
+ type="count-percentage"
+ crop="end"
+ value="Total Count %"
+ tooltiptext="&performanceUI.table.totalAlloc.tooltip;"/>
+ <label class="plain call-tree-header"
+ type="function"
+ crop="end"
+ value="&performanceUI.table.function;"/>
+ </hbox>
+ <vbox class="call-tree-cells-container" flex="1"/>
+ </vbox>
+
+ <!-- Memory FlameChart -->
+ <hbox id="memory-flamegraph-view" flex="1"></hbox>
+ </deck>
+ </deck>
+ </vbox>
+ </deck>
+ </vbox>
+ </hbox>
+</window>
diff --git a/devtools/client/performance/initializer.js b/devtools/client/performance/initializer.js
new file mode 100644
index 0000000000..6623be5157
--- /dev/null
+++ b/devtools/client/performance/initializer.js
@@ -0,0 +1,80 @@
+/* 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 { BrowserLoader } = ChromeUtils.import(
+ "resource://devtools/client/shared/browser-loader.js"
+);
+const { require } = BrowserLoader({
+ baseURI: "resource://devtools/client/performance/",
+ window: window,
+});
+
+const {
+ PerformanceController,
+} = require("devtools/client/performance/performance-controller");
+const {
+ PerformanceView,
+} = require("devtools/client/performance/performance-view");
+const { DetailsView } = require("devtools/client/performance/views/details");
+const {
+ DetailsSubview,
+} = require("devtools/client/performance/views/details-abstract-subview");
+const {
+ JsCallTreeView,
+} = require("devtools/client/performance/views/details-js-call-tree");
+const {
+ JsFlameGraphView,
+} = require("devtools/client/performance/views/details-js-flamegraph");
+const {
+ MemoryCallTreeView,
+} = require("devtools/client/performance/views/details-memory-call-tree");
+const {
+ MemoryFlameGraphView,
+} = require("devtools/client/performance/views/details-memory-flamegraph");
+const { OverviewView } = require("devtools/client/performance/views/overview");
+const {
+ RecordingsView,
+} = require("devtools/client/performance/views/recordings");
+const { ToolbarView } = require("devtools/client/performance/views/toolbar");
+const {
+ WaterfallView,
+} = require("devtools/client/performance/views/details-waterfall");
+
+const EVENTS = require("devtools/client/performance/events");
+
+/**
+ * The performance panel used to only share modules through references on the window
+ * object. We started cleaning this up and to require() explicitly in Bug 1524982, but
+ * some modules and tests are still relying on those references so we keep exposing them
+ * for the moment. Bug 1528777.
+ */
+window.PerformanceController = PerformanceController;
+window.PerformanceView = PerformanceView;
+window.DetailsView = DetailsView;
+window.DetailsSubview = DetailsSubview;
+window.JsCallTreeView = JsCallTreeView;
+window.JsFlameGraphView = JsFlameGraphView;
+window.MemoryCallTreeView = MemoryCallTreeView;
+window.MemoryFlameGraphView = MemoryFlameGraphView;
+window.OverviewView = OverviewView;
+window.RecordingsView = RecordingsView;
+window.ToolbarView = ToolbarView;
+window.WaterfallView = WaterfallView;
+
+window.EVENTS = EVENTS;
+
+/**
+ * DOM query helpers.
+ */
+/* exported $, $$ */
+function $(selector, target = document) {
+ return target.querySelector(selector);
+}
+window.$ = $;
+function $$(selector, target = document) {
+ return target.querySelectorAll(selector);
+}
+window.$$ = $$;
diff --git a/devtools/client/performance/modules/categories.js b/devtools/client/performance/modules/categories.js
new file mode 100644
index 0000000000..f6ef7073ef
--- /dev/null
+++ b/devtools/client/performance/modules/categories.js
@@ -0,0 +1,87 @@
+/* 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");
+
+/**
+ * Details about each label stack frame category.
+ * To be kept in sync with the JS::ProfilingCategory enum in ProfilingCategory.h
+ */
+const CATEGORIES = [
+ {
+ color: "#d99b28",
+ abbrev: "idle",
+ label: L10N.getStr("category.idle"),
+ },
+ {
+ color: "#5e88b0",
+ abbrev: "other",
+ label: L10N.getStr("category.other"),
+ },
+ {
+ color: "#46afe3",
+ abbrev: "layout",
+ label: L10N.getStr("category.layout"),
+ },
+ {
+ color: "#d96629",
+ abbrev: "js",
+ label: L10N.getStr("category.js"),
+ },
+ {
+ color: "#eb5368",
+ abbrev: "gc",
+ label: L10N.getStr("category.gc"),
+ },
+ {
+ color: "#df80ff",
+ abbrev: "network",
+ label: L10N.getStr("category.network"),
+ },
+ {
+ color: "#70bf53",
+ abbrev: "graphics",
+ label: L10N.getStr("category.graphics"),
+ },
+ {
+ color: "#8fa1b2",
+ abbrev: "dom",
+ label: L10N.getStr("category.dom"),
+ },
+ {
+ // The devtools-only "tools" category which gets computed by
+ // computeIsContentAndCategory and is not part of the category list in
+ // ProfilingStack.h.
+ color: "#8fa1b2",
+ abbrev: "tools",
+ label: L10N.getStr("category.tools"),
+ },
+];
+
+/**
+ * Get the numeric index for the given category abbreviation.
+ * See `CATEGORIES` above.
+ */
+const CATEGORY_INDEX = (() => {
+ const indexForCategory = {};
+ for (
+ let categoryIndex = 0;
+ categoryIndex < CATEGORIES.length;
+ categoryIndex++
+ ) {
+ const category = CATEGORIES[categoryIndex];
+ indexForCategory[category.abbrev] = categoryIndex;
+ }
+
+ return function(name) {
+ if (!(name in indexForCategory)) {
+ throw new Error(`Category abbreviation "${name}" does not exist.`);
+ }
+ return indexForCategory[name];
+ };
+})();
+
+exports.CATEGORIES = CATEGORIES;
+exports.CATEGORY_INDEX = CATEGORY_INDEX;
diff --git a/devtools/client/performance/modules/constants.js b/devtools/client/performance/modules/constants.js
new file mode 100644
index 0000000000..a0adaf5961
--- /dev/null
+++ b/devtools/client/performance/modules/constants.js
@@ -0,0 +1,11 @@
+/* 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";
+
+exports.Constants = {
+ // ms
+ FRAMERATE_GRAPH_LOW_RES_INTERVAL: 100,
+ // ms
+ FRAMERATE_GRAPH_HIGH_RES_INTERVAL: 16,
+};
diff --git a/devtools/client/performance/modules/global.js b/devtools/client/performance/modules/global.js
new file mode 100644
index 0000000000..7816becc70
--- /dev/null
+++ b/devtools/client/performance/modules/global.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";
+
+const { MultiLocalizationHelper } = require("devtools/shared/l10n");
+const { PrefsHelper } = require("devtools/client/shared/prefs");
+
+/**
+ * Localization convenience methods.
+ */
+exports.L10N = new MultiLocalizationHelper(
+ "devtools/client/locales/markers.properties",
+ "devtools/client/locales/performance.properties"
+);
+
+/**
+ * A list of preferences for this tool. The values automatically update
+ * if somebody edits edits about:config or the prefs change somewhere else.
+ *
+ * This needs to be registered and unregistered when used for the auto-update
+ * functionality to work. The PerformanceController handles this, but if you
+ * just use this module in a test independently, ensure you call
+ * `registerObserver()` and `unregisterUnobserver()`.
+ */
+exports.PREFS = new PrefsHelper("devtools.performance", {
+ "show-triggers-for-gc-types": ["Char", "ui.show-triggers-for-gc-types"],
+ "show-platform-data": ["Bool", "ui.show-platform-data"],
+ "hidden-markers": ["Json", "timeline.hidden-markers"],
+ "memory-sample-probability": ["Float", "memory.sample-probability"],
+ "memory-max-log-length": ["Int", "memory.max-log-length"],
+ "profiler-buffer-size": ["Int", "profiler.buffer-size"],
+ "profiler-sample-frequency": ["Int", "profiler.sample-frequency-hz"],
+ // TODO: re-enable once we flame charts via bug 1148663.
+ "enable-memory-flame": ["Bool", "ui.enable-memory-flame"],
+});
diff --git a/devtools/client/performance/modules/io.js b/devtools/client/performance/modules/io.js
new file mode 100644
index 0000000000..97fb16dc41
--- /dev/null
+++ b/devtools/client/performance/modules/io.js
@@ -0,0 +1,173 @@
+/* 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 { Cc, Ci } = require("chrome");
+
+const RecordingUtils = require("devtools/shared/performance/recording-utils");
+const { FileUtils } = require("resource://gre/modules/FileUtils.jsm");
+const { NetUtil } = require("resource://gre/modules/NetUtil.jsm");
+
+// This identifier string is used to tentatively ascertain whether or not
+// a JSON loaded from disk is actually something generated by this tool.
+// It isn't, of course, a definitive verification, but a Good Enough™
+// approximation before continuing the import. Don't localize this.
+const PERF_TOOL_SERIALIZER_IDENTIFIER = "Recorded Performance Data";
+const PERF_TOOL_SERIALIZER_LEGACY_VERSION = 1;
+const PERF_TOOL_SERIALIZER_CURRENT_VERSION = 2;
+
+/**
+ * Helpers for importing/exporting JSON.
+ */
+
+/**
+ * Gets a nsIScriptableUnicodeConverter instance with a default UTF-8 charset.
+ * @return object
+ */
+function getUnicodeConverter() {
+ const cname = "@mozilla.org/intl/scriptableunicodeconverter";
+ const converter = Cc[cname].createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ return converter;
+}
+
+/**
+ * Saves a recording as JSON to a file. The provided data is assumed to be
+ * acyclical, so that it can be properly serialized.
+ *
+ * @param object recordingData
+ * The recording data to stream as JSON.
+ * @param nsIFile file
+ * The file to stream the data into.
+ * @return object
+ * A promise that is resolved once streaming finishes, or rejected
+ * if there was an error.
+ */
+function saveRecordingToFile(recordingData, file) {
+ recordingData.fileType = PERF_TOOL_SERIALIZER_IDENTIFIER;
+ recordingData.version = PERF_TOOL_SERIALIZER_CURRENT_VERSION;
+
+ const string = JSON.stringify(recordingData);
+ const inputStream = getUnicodeConverter().convertToInputStream(string);
+ const outputStream = FileUtils.openSafeFileOutputStream(file);
+
+ return new Promise(resolve => {
+ NetUtil.asyncCopy(inputStream, outputStream, resolve);
+ });
+}
+
+/**
+ * Loads a recording stored as JSON from a file.
+ *
+ * @param nsIFile file
+ * The file to import the data from.
+ * @return object
+ * A promise that is resolved once importing finishes, or rejected
+ * if there was an error.
+ */
+function loadRecordingFromFile(file) {
+ const channel = NetUtil.newChannel({
+ uri: NetUtil.newURI(file),
+ loadUsingSystemPrincipal: true,
+ });
+
+ channel.contentType = "text/plain";
+
+ return new Promise((resolve, reject) => {
+ NetUtil.asyncFetch(channel, inputStream => {
+ let recordingData;
+
+ try {
+ const string = NetUtil.readInputStreamToString(
+ inputStream,
+ inputStream.available()
+ );
+ recordingData = JSON.parse(string);
+ } catch (e) {
+ reject(new Error("Could not read recording data file."));
+ return;
+ }
+
+ if (recordingData.fileType != PERF_TOOL_SERIALIZER_IDENTIFIER) {
+ reject(new Error("Unrecognized recording data file."));
+ return;
+ }
+
+ if (!isValidSerializerVersion(recordingData.version)) {
+ reject(new Error("Unsupported recording data file version."));
+ return;
+ }
+
+ if (recordingData.version === PERF_TOOL_SERIALIZER_LEGACY_VERSION) {
+ recordingData = convertLegacyData(recordingData);
+ }
+
+ if (recordingData.profile.meta.version === 2) {
+ RecordingUtils.deflateProfile(recordingData.profile);
+ }
+
+ // If the recording has no label, set it to be the
+ // filename without its extension.
+ if (!recordingData.label) {
+ recordingData.label = file.leafName.replace(/\.[^.]+$/, "");
+ }
+
+ resolve(recordingData);
+ });
+ });
+}
+
+/**
+ * Returns a boolean indicating whether or not the passed in `version`
+ * is supported by this serializer.
+ *
+ * @param number version
+ * @return boolean
+ */
+function isValidSerializerVersion(version) {
+ return !!~[
+ PERF_TOOL_SERIALIZER_LEGACY_VERSION,
+ PERF_TOOL_SERIALIZER_CURRENT_VERSION,
+ ].indexOf(version);
+}
+
+/**
+ * Takes recording data (with version `1`, from the original profiler tool),
+ * and massages the data to be line with the current performance tool's
+ * property names and values.
+ *
+ * @param object legacyData
+ * @return object
+ */
+function convertLegacyData(legacyData) {
+ const { profilerData, ticksData, recordingDuration } = legacyData;
+
+ // The `profilerData` and `ticksData` stay, but the previously unrecorded
+ // fields just are empty arrays or objects.
+ const data = {
+ label: profilerData.profilerLabel,
+ duration: recordingDuration,
+ markers: [],
+ frames: [],
+ memory: [],
+ ticks: ticksData,
+ allocations: { sites: [], timestamps: [], frames: [], sizes: [] },
+ profile: profilerData.profile,
+ // Fake a configuration object here if there's tick data,
+ // so that it can be rendered.
+ configuration: {
+ withTicks: !!ticksData.length,
+ withMarkers: false,
+ withMemory: false,
+ withAllocations: false,
+ },
+ systemHost: {},
+ systemClient: {},
+ };
+
+ return data;
+}
+
+exports.saveRecordingToFile = saveRecordingToFile;
+exports.loadRecordingFromFile = loadRecordingFromFile;
diff --git a/devtools/client/performance/modules/logic/frame-utils.js b/devtools/client/performance/modules/logic/frame-utils.js
new file mode 100644
index 0000000000..a799381274
--- /dev/null
+++ b/devtools/client/performance/modules/logic/frame-utils.js
@@ -0,0 +1,510 @@
+/* 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 global = require("devtools/client/performance/modules/global");
+const demangle = require("devtools/client/shared/demangle");
+const { assert } = require("devtools/shared/DevToolsUtils");
+const {
+ isChromeScheme,
+ isContentScheme,
+ isWASM,
+ parseURL,
+} = require("devtools/client/shared/source-utils");
+
+const {
+ CATEGORY_INDEX,
+ CATEGORIES,
+} = require("devtools/client/performance/modules/categories");
+
+// Character codes used in various parsing helper functions.
+const CHAR_CODE_R = "r".charCodeAt(0);
+const CHAR_CODE_0 = "0".charCodeAt(0);
+const CHAR_CODE_9 = "9".charCodeAt(0);
+const CHAR_CODE_CAP_Z = "Z".charCodeAt(0);
+
+const CHAR_CODE_LPAREN = "(".charCodeAt(0);
+const CHAR_CODE_RPAREN = ")".charCodeAt(0);
+const CHAR_CODE_COLON = ":".charCodeAt(0);
+const CHAR_CODE_SPACE = " ".charCodeAt(0);
+const CHAR_CODE_UNDERSCORE = "_".charCodeAt(0);
+
+const EVAL_TOKEN = "%20%3E%20eval";
+
+// The cache used to store inflated frames.
+const gInflatedFrameStore = new WeakMap();
+
+// The cache used to store frame data from `getInfo`.
+const gFrameData = new WeakMap();
+
+/**
+ * Parses the raw location of this function call to retrieve the actual
+ * function name, source url, host name, line and column.
+ */
+// eslint-disable-next-line complexity
+function parseLocation(location, fallbackLine, fallbackColumn) {
+ // Parse the `location` for the function name, source url, line, column etc.
+
+ let line, column, url;
+
+ // These two indices are used to extract the resource substring, which is
+ // location[parenIndex + 1 .. lineAndColumnIndex].
+ //
+ // There are 3 variants of location strings in the profiler (with optional
+ // column numbers):
+ // 1) "name (resource:line)"
+ // 2) "resource:line"
+ // 3) "resource"
+ //
+ // For example for (1), take "foo (bar.js:1)".
+ // ^ ^
+ // | |
+ // | |
+ // | |
+ // parenIndex will point to ------+ |
+ // |
+ // lineAndColumnIndex will point to -----+
+ //
+ // For an example without parentheses, take "bar.js:2".
+ // ^ ^
+ // | |
+ // parenIndex will point to ----------------+ |
+ // |
+ // lineAndColumIndex will point to ----------------+
+ //
+ // To parse, we look for the last occurrence of the string ' ('.
+ //
+ // For 1), all occurrences of space ' ' characters in the resource string
+ // are urlencoded, so the last occurrence of ' (' is the separator between
+ // the function name and the resource.
+ //
+ // For 2) and 3), there can be no occurences of ' (' since ' ' characters
+ // are urlencoded in the resource string.
+ //
+ // XXX: Note that 3) is ambiguous with Gecko Profiler marker locations like
+ // "EnterJIT". We can't distinguish the two, so we treat 3) like a function
+ // name.
+ let parenIndex = -1;
+ let lineAndColumnIndex = -1;
+
+ const lastCharCode = location.charCodeAt(location.length - 1);
+ let i;
+ if (lastCharCode === CHAR_CODE_RPAREN) {
+ // Case 1)
+ i = location.length - 2;
+ } else if (isNumeric(lastCharCode)) {
+ // Case 2)
+ i = location.length - 1;
+ } else {
+ // Case 3)
+ i = 0;
+ }
+
+ if (i !== 0) {
+ // Look for a :number.
+ let end = i;
+ while (isNumeric(location.charCodeAt(i))) {
+ i--;
+ }
+ if (location.charCodeAt(i) === CHAR_CODE_COLON) {
+ column = location.substr(i + 1, end - i);
+ i--;
+ }
+
+ // Look for a preceding :number.
+ end = i;
+ while (isNumeric(location.charCodeAt(i))) {
+ i--;
+ }
+
+ // If two were found, the first is the line and the second is the
+ // column. If only a single :number was found, then it is the line number.
+ if (location.charCodeAt(i) === CHAR_CODE_COLON) {
+ line = location.substr(i + 1, end - i);
+ lineAndColumnIndex = i;
+ i--;
+ } else {
+ lineAndColumnIndex = i + 1;
+ line = column;
+ column = undefined;
+ }
+ }
+
+ // Look for the last occurrence of ' (' in case 1).
+ if (lastCharCode === CHAR_CODE_RPAREN) {
+ for (; i >= 0; i--) {
+ if (
+ location.charCodeAt(i) === CHAR_CODE_LPAREN &&
+ i > 0 &&
+ location.charCodeAt(i - 1) === CHAR_CODE_SPACE
+ ) {
+ parenIndex = i;
+ break;
+ }
+ }
+ }
+
+ let parsedUrl;
+ if (lineAndColumnIndex > 0) {
+ const resource = location.substring(parenIndex + 1, lineAndColumnIndex);
+ url = resource.split(" -> ").pop();
+ if (url) {
+ parsedUrl = parseURL(url);
+ }
+ }
+
+ let functionName, fileName, port, host;
+ line = line || fallbackLine;
+ column = column || fallbackColumn;
+
+ // If the URL digged out from the `location` is valid, this is a JS frame.
+ if (parsedUrl) {
+ functionName = location.substring(0, parenIndex - 1);
+ fileName = parsedUrl.fileName;
+ port = parsedUrl.port;
+ host = parsedUrl.host;
+
+ // Check for the case of the filename containing eval
+ // e.g. "file.js%20line%2065%20%3E%20eval"
+ const evalIndex = fileName.indexOf(EVAL_TOKEN);
+ if (evalIndex !== -1 && evalIndex === fileName.length - EVAL_TOKEN.length) {
+ // Match the filename
+ const evalLine = line;
+ const [, _fileName, , _line] =
+ fileName.match(/(.+)(%20line%20(\d+)%20%3E%20eval)/) || [];
+ fileName = `${_fileName} (eval:${evalLine})`;
+ line = _line;
+ assert(
+ _fileName !== undefined,
+ "Filename could not be found from an eval location site"
+ );
+ assert(
+ _line !== undefined,
+ "Line could not be found from an eval location site"
+ );
+
+ // Match the url as well
+ [, url] = url.match(/(.+)( line (\d+) > eval)/) || [];
+ assert(
+ url !== undefined,
+ "The URL could not be parsed correctly from an eval location site"
+ );
+ }
+ } else {
+ functionName = location;
+ url = null;
+ }
+
+ return { functionName, fileName, host, port, url, line, column };
+}
+
+/**
+ * Sets the properties of `isContent` and `category` on a frame.
+ *
+ * @param {InflatedFrame} frame
+ */
+function computeIsContentAndCategory(frame) {
+ const location = frame.location;
+
+ // There are 3 variants of location strings in the profiler (with optional
+ // column numbers):
+ // 1) "name (resource:line)"
+ // 2) "resource:line"
+ // 3) "resource"
+ const lastCharCode = location.charCodeAt(location.length - 1);
+ let schemeStartIndex = -1;
+ if (lastCharCode === CHAR_CODE_RPAREN) {
+ // Case 1)
+ //
+ // Need to search for the last occurrence of ' (' to find the start of the
+ // resource string.
+ for (let i = location.length - 2; i >= 0; i--) {
+ if (
+ location.charCodeAt(i) === CHAR_CODE_LPAREN &&
+ i > 0 &&
+ location.charCodeAt(i - 1) === CHAR_CODE_SPACE
+ ) {
+ schemeStartIndex = i + 1;
+ break;
+ }
+ }
+ } else {
+ // Cases 2) and 3)
+ schemeStartIndex = 0;
+ }
+
+ // We can't know if WASM frames are content or not at the time of this writing, so label
+ // them all as content.
+ if (isContentScheme(location, schemeStartIndex) || isWASM(location)) {
+ frame.isContent = true;
+ return;
+ }
+
+ if (frame.category !== null && frame.category !== undefined) {
+ return;
+ }
+
+ if (schemeStartIndex !== 0) {
+ for (let j = schemeStartIndex; j < location.length; j++) {
+ if (
+ location.charCodeAt(j) === CHAR_CODE_R &&
+ isChromeScheme(location, j) &&
+ (location.includes("resource://devtools") ||
+ location.includes("resource://devtools"))
+ ) {
+ frame.category = CATEGORY_INDEX("tools");
+ return;
+ }
+ }
+ }
+
+ if (location === "EnterJIT") {
+ frame.category = CATEGORY_INDEX("js");
+ return;
+ }
+
+ frame.category = CATEGORY_INDEX("other");
+}
+
+/**
+ * Get caches to cache inflated frames and computed frame keys of a frame
+ * table.
+ *
+ * @param object framesTable
+ * @return object
+ */
+function getInflatedFrameCache(frameTable) {
+ let inflatedCache = gInflatedFrameStore.get(frameTable);
+ if (inflatedCache !== undefined) {
+ return inflatedCache;
+ }
+
+ // Fill with nulls to ensure no holes.
+ inflatedCache = Array.from({ length: frameTable.data.length }, () => null);
+ gInflatedFrameStore.set(frameTable, inflatedCache);
+ return inflatedCache;
+}
+
+/**
+ * Get or add an inflated frame to a cache.
+ *
+ * @param object cache
+ * @param number index
+ * @param object frameTable
+ * @param object stringTable
+ */
+function getOrAddInflatedFrame(cache, index, frameTable, stringTable) {
+ let inflatedFrame = cache[index];
+ if (inflatedFrame === null) {
+ inflatedFrame = cache[index] = new InflatedFrame(
+ index,
+ frameTable,
+ stringTable
+ );
+ }
+ return inflatedFrame;
+}
+
+/**
+ * An intermediate data structured used to hold inflated frames.
+ *
+ * @param number index
+ * @param object frameTable
+ * @param object stringTable
+ */
+function InflatedFrame(index, frameTable, stringTable) {
+ const LOCATION_SLOT = frameTable.schema.location;
+ const IMPLEMENTATION_SLOT = frameTable.schema.implementation;
+ const OPTIMIZATIONS_SLOT = frameTable.schema.optimizations;
+ const LINE_SLOT = frameTable.schema.line;
+ const CATEGORY_SLOT = frameTable.schema.category;
+
+ const frame = frameTable.data[index];
+ const category = frame[CATEGORY_SLOT];
+ this.location = stringTable[frame[LOCATION_SLOT]];
+ this.implementation = frame[IMPLEMENTATION_SLOT];
+ this.optimizations = frame[OPTIMIZATIONS_SLOT];
+ this.line = frame[LINE_SLOT];
+ this.column = undefined;
+ this.category = category;
+ this.isContent = false;
+
+ // Attempt to compute if this frame is a content frame, and if not,
+ // its category.
+ //
+ // Since only C++ stack frames have associated category information,
+ // attempt to generate a useful category, fallback to the one provided
+ // by the profiling data, or fallback to an unknown category.
+ computeIsContentAndCategory(this);
+}
+
+/**
+ * Gets the frame key (i.e., equivalence group) according to options. Content
+ * frames are always identified by location. Chrome frames are identified by
+ * location if content-only filtering is off. If content-filtering is on, they
+ * are identified by their category.
+ *
+ * @param object options
+ * @return string
+ */
+InflatedFrame.prototype.getFrameKey = function getFrameKey(options) {
+ if (this.isContent || !options.contentOnly || options.isRoot) {
+ options.isMetaCategoryOut = false;
+ return this.location;
+ }
+
+ if (options.isLeaf) {
+ // We only care about leaf platform frames if we are displaying content
+ // only. If no category is present, give the default category of "other".
+ //
+ // 1. The leaf is where time is _actually_ being spent, so we _need_ to
+ // show it to developers in some way to give them accurate profiling
+ // data. We decide to split the platform into various category buckets
+ // and just show time spent in each bucket.
+ //
+ // 2. The calls leading to the leaf _aren't_ where we are spending time,
+ // but _do_ give the developer context for how they got to the leaf
+ // where they _are_ spending time. For non-platform hackers, the
+ // non-leaf platform frames don't give any meaningful context, and so we
+ // can safely filter them out.
+ options.isMetaCategoryOut = true;
+ return this.category;
+ }
+
+ // Return an empty string denoting that this frame should be skipped.
+ return "";
+};
+
+function isNumeric(c) {
+ return c >= CHAR_CODE_0 && c <= CHAR_CODE_9;
+}
+
+function shouldDemangle(name) {
+ return (
+ name?.charCodeAt &&
+ name.charCodeAt(0) === CHAR_CODE_UNDERSCORE &&
+ name.charCodeAt(1) === CHAR_CODE_UNDERSCORE &&
+ name.charCodeAt(2) === CHAR_CODE_CAP_Z
+ );
+}
+
+/**
+ * Calculates the relative costs of this frame compared to a root,
+ * and generates allocations information if specified. Uses caching
+ * if possible.
+ *
+ * @param {ThreadNode|FrameNode} node
+ * The node we are calculating.
+ * @param {ThreadNode} options.root
+ * The root thread node to calculate relative costs.
+ * Generates [self|total] [duration|percentage] values.
+ * @param {boolean} options.allocations
+ * Generates `totalAllocations` and `selfAllocations`.
+ *
+ * @return {object}
+ */
+function getFrameInfo(node, options) {
+ let data = gFrameData.get(node);
+
+ if (!data) {
+ if (node.nodeType === "Thread") {
+ data = Object.create(null);
+ data.functionName = global.L10N.getStr("table.root");
+ } else {
+ data = parseLocation(node.location, node.line, node.column);
+ data.hasOptimizations = node.hasOptimizations();
+ data.isContent = node.isContent;
+ data.isMetaCategory = node.isMetaCategory;
+ }
+ data.samples = node.youngestFrameSamples;
+ const hasCategory = node.category !== null && node.category !== undefined;
+ data.categoryData = hasCategory
+ ? CATEGORIES[node.category] || CATEGORIES[CATEGORY_INDEX("other")]
+ : {};
+ data.nodeType = node.nodeType;
+
+ // Frame name (function location or some meta information)
+ if (data.isMetaCategory) {
+ data.name = data.categoryData.label;
+ } else if (shouldDemangle(data.functionName)) {
+ data.name = demangle(data.functionName);
+ } else {
+ data.name = data.functionName;
+ }
+
+ data.tooltiptext = data.isMetaCategory
+ ? data.categoryData.label
+ : node.location || "";
+
+ gFrameData.set(node, data);
+ }
+
+ // If no options specified, we can't calculate relative values, abort here
+ if (!options) {
+ return data;
+ }
+
+ // If a root specified, calculate the relative costs in the context of
+ // this call tree. The cached store may already have this, but generate
+ // if it does not.
+ const totalSamples = options.root.samples;
+ const totalDuration = options.root.duration;
+ if (options?.root && !data.COSTS_CALCULATED) {
+ data.selfDuration =
+ (node.youngestFrameSamples / totalSamples) * totalDuration;
+ data.selfPercentage = (node.youngestFrameSamples / totalSamples) * 100;
+ data.totalDuration = (node.samples / totalSamples) * totalDuration;
+ data.totalPercentage = (node.samples / totalSamples) * 100;
+ data.COSTS_CALCULATED = true;
+ }
+
+ if (options?.allocations && !data.ALLOCATION_DATA_CALCULATED) {
+ const totalBytes = options.root.byteSize;
+ data.selfCount = node.youngestFrameSamples;
+ data.totalCount = node.samples;
+ data.selfCountPercentage = (node.youngestFrameSamples / totalSamples) * 100;
+ data.totalCountPercentage = (node.samples / totalSamples) * 100;
+ data.selfSize = node.youngestFrameByteSize;
+ data.totalSize = node.byteSize;
+ data.selfSizePercentage = (node.youngestFrameByteSize / totalBytes) * 100;
+ data.totalSizePercentage = (node.byteSize / totalBytes) * 100;
+ data.ALLOCATION_DATA_CALCULATED = true;
+ }
+
+ return data;
+}
+
+exports.getFrameInfo = getFrameInfo;
+
+/**
+ * Takes an inverted ThreadNode and searches its youngest frames for
+ * a FrameNode with matching location.
+ *
+ * @param {ThreadNode} threadNode
+ * @param {string} location
+ * @return {?FrameNode}
+ */
+function findFrameByLocation(threadNode, location) {
+ if (!threadNode.inverted) {
+ throw new Error(
+ "FrameUtils.findFrameByLocation only supports leaf nodes in an inverted tree."
+ );
+ }
+
+ const calls = threadNode.calls;
+ for (let i = 0; i < calls.length; i++) {
+ if (calls[i].location === location) {
+ return calls[i];
+ }
+ }
+ return null;
+}
+
+exports.findFrameByLocation = findFrameByLocation;
+exports.computeIsContentAndCategory = computeIsContentAndCategory;
+exports.parseLocation = parseLocation;
+exports.getInflatedFrameCache = getInflatedFrameCache;
+exports.getOrAddInflatedFrame = getOrAddInflatedFrame;
+exports.InflatedFrame = InflatedFrame;
+exports.shouldDemangle = shouldDemangle;
diff --git a/devtools/client/performance/modules/logic/jit.js b/devtools/client/performance/modules/logic/jit.js
new file mode 100644
index 0000000000..5063dd4a8b
--- /dev/null
+++ b/devtools/client/performance/modules/logic/jit.js
@@ -0,0 +1,350 @@
+/* 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";
+
+// An outcome of an OptimizationAttempt that is considered successful.
+const SUCCESSFUL_OUTCOMES = [
+ "GenericSuccess",
+ "Inlined",
+ "DOM",
+ "Monomorphic",
+ "Polymorphic",
+];
+
+/**
+ * Model representing JIT optimization sites from the profiler
+ * for a frame (represented by a FrameNode). Requires optimization data from
+ * a profile, which is an array of RawOptimizationSites.
+ *
+ * When the ThreadNode for the profile iterates over the samples' frames, each
+ * frame's optimizations are accumulated in their respective FrameNodes. Each
+ * FrameNode may contain many different optimization sites. One sample may
+ * pick up optimization X on line Y in the frame, with the next sample
+ * containing optimization Z on line W in the same frame, as each frame is
+ * only function.
+ *
+ * An OptimizationSite contains a record of how many times the
+ * RawOptimizationSite was sampled, as well as the unique id based off of the
+ * original profiler array, and the RawOptimizationSite itself as a reference.
+ * @see devtools/client/performance/modules/logic/tree-model.js
+ *
+ * @struct RawOptimizationSite
+ * A structure describing a location in a script that was attempted to be optimized.
+ * Contains all the IonTypes observed, and the sequence of OptimizationAttempts that
+ * were attempted, and the line and column in the script. This is retrieved from the
+ * profiler after a recording, and our base data structure. Should always be referenced,
+ * and unmodified.
+ *
+ * Note that propertyName is an index into a string table, which needs to be
+ * provided in order for the raw optimization site to be inflated.
+ *
+ * @type {Array<IonType>} types
+ * @type {Array<OptimizationAttempt>} attempts
+ * @type {?number} propertyName
+ * @type {number} line
+ * @type {number} column
+ *
+ *
+ * @struct IonType
+ * IonMonkey attempts to classify each value in an optimization site by some type.
+ * Based off of the observed types for a value (like a variable that could be a
+ * string or an instance of an object), it determines what kind of type it should be
+ * classified as. Each IonType here contains an array of all ObservedTypes under `types`,
+ * the Ion type that IonMonkey decided this value should be (Int32, Object, etc.) as
+ * `mirType`, and the component of this optimization type that this value refers to --
+ * like a "getter" optimization, `a[b]`, has site `a` (the "Receiver") and `b`
+ * (the "Index").
+ *
+ * Generally the more ObservedTypes, the more deoptimized this OptimizationSite is.
+ * There could be no ObservedTypes, in which case `typeset` is undefined.
+ *
+ * @type {?Array<ObservedType>} typeset
+ * @type {string} site
+ * @type {string} mirType
+ *
+ *
+ * @struct ObservedType
+ * When IonMonkey attempts to determine what type a value is, it checks on each sample.
+ * The ObservedType can be thought of in more of JavaScripty-terms, rather than C++.
+ * The `keyedBy` property is a high level description of the type, like "primitive",
+ * "constructor", "function", "singleton", "alloc-site" (that one is a bit more weird).
+ * If the `keyedBy` type is a function or constructor, the ObservedType should have a
+ * `name` property, referring to the function or constructor name from the JS source.
+ * If IonMonkey can determine the origin of this type (like where the constructor is
+ * defined), the ObservedType will also have `location` and `line` properties, but
+ * `location` can sometimes be non-URL strings like "self-hosted" or a memory location
+ * like "102ca7880", or no location at all, and maybe `line` is 0 or undefined.
+ *
+ * @type {string} keyedBy
+ * @type {?string} name
+ * @type {?string} location
+ * @type {?string} line
+ *
+ *
+ * @struct OptimizationAttempt
+ * Each RawOptimizationSite contains an array of OptimizationAttempts. Generally,
+ * IonMonkey goes through a series of strategies for each kind of optimization, starting
+ * from most-niche and optimized, to the less-optimized, but more general strategies --
+ * for example, a getter opt may first try to optimize for the scenario of a getter on an
+ * `arguments` object -- that will fail most of the time, as most objects are not
+ * arguments objects, but it will attempt several strategies in order until it finds a
+ * strategy that works, or fails. Even in the best scenarios, some attempts will fail
+ * (like the arguments getter example), which is OK, as long as some attempt succeeds
+ * (with the earlier attempts preferred, as those are more optimized). In an
+ * OptimizationAttempt structure, we store just the `strategy` name and `outcome` name,
+ * both from enums in js/public/TrackedOptimizationInfo.h as TRACKED_STRATEGY_LIST and
+ * TRACKED_OUTCOME_LIST, respectively. An array of successful outcome strings are above
+ * in SUCCESSFUL_OUTCOMES.
+ *
+ * @see js/public/TrackedOptimizationInfo.h
+ *
+ * @type {string} strategy
+ * @type {string} outcome
+ */
+
+/*
+ * A wrapper around RawOptimizationSite to record sample count and ID (referring to the
+ * index of where this is in the initially seeded optimizations data), so we don't mutate
+ * the original data from the profiler. Provides methods to access the underlying
+ * optimization data easily, so understanding the semantics of JIT data isn't necessary.
+ *
+ * @constructor
+ *
+ * @param {Array<RawOptimizationSite>} optimizations
+ * @param {number} optsIndex
+ *
+ * @type {RawOptimizationSite} data
+ * @type {number} samples
+ * @type {number} id
+ */
+
+const OptimizationSite = function(id, opts) {
+ this.id = id;
+ this.data = opts;
+ this.samples = 1;
+};
+
+/**
+ * Constructor for JITOptimizations. A collection of OptimizationSites for a frame.
+ *
+ * @constructor
+ * @param {Array<RawOptimizationSite>} rawSites
+ * Array of raw optimization sites.
+ * @param {Array<string>} stringTable
+ * Array of strings from the profiler used to inflate
+ * JIT optimizations. Do not modify this!
+ */
+
+const JITOptimizations = function(rawSites, stringTable) {
+ // Build a histogram of optimization sites.
+ const sites = [];
+
+ for (const rawSite of rawSites) {
+ const existingSite = sites.find(site => site.data === rawSite);
+ if (existingSite) {
+ existingSite.samples++;
+ } else {
+ sites.push(new OptimizationSite(sites.length, rawSite));
+ }
+ }
+
+ // Inflate the optimization information.
+ for (const site of sites) {
+ const data = site.data;
+ const STRATEGY_SLOT = data.attempts.schema.strategy;
+ const OUTCOME_SLOT = data.attempts.schema.outcome;
+ const attempts = data.attempts.data.map(a => {
+ return {
+ id: site.id,
+ strategy: stringTable[a[STRATEGY_SLOT]],
+ outcome: stringTable[a[OUTCOME_SLOT]],
+ };
+ });
+ const types = data.types.map(t => {
+ const typeset = maybeTypeset(t.typeset, stringTable);
+ if (typeset) {
+ typeset.forEach(ts => {
+ ts.id = site.id;
+ });
+ }
+
+ return {
+ id: site.id,
+ typeset,
+ site: stringTable[t.site],
+ mirType: stringTable[t.mirType],
+ };
+ });
+ // Add IDs to to all children objects, so we can correllate sites when
+ // just looking at a specific type, attempt, etc..
+ attempts.id = types.id = site.id;
+
+ site.data = {
+ attempts,
+ types,
+ propertyName: maybeString(stringTable, data.propertyName),
+ line: data.line,
+ column: data.column,
+ };
+ }
+
+ this.optimizationSites = sites.sort((a, b) => b.samples - a.samples);
+};
+
+/**
+ * Make JITOptimizations iterable.
+ */
+JITOptimizations.prototype = {
+ [Symbol.iterator]: function*() {
+ yield* this.optimizationSites;
+ },
+
+ get length() {
+ return this.optimizationSites.length;
+ },
+};
+
+/**
+ * Takes an "outcome" string from an OptimizationAttempt and returns
+ * a boolean indicating whether or not its a successful outcome.
+ *
+ * @return {boolean}
+ */
+
+function isSuccessfulOutcome(outcome) {
+ return !!~SUCCESSFUL_OUTCOMES.indexOf(outcome);
+}
+
+/**
+ * Takes an OptimizationSite. Returns a boolean indicating if the passed
+ * in OptimizationSite has a "good" outcome at the end of its attempted strategies.
+ *
+ * @param {OptimizationSite} optimizationSite
+ * @return {boolean}
+ */
+
+function hasSuccessfulOutcome(optimizationSite) {
+ const attempts = optimizationSite.data.attempts;
+ const lastOutcome = attempts[attempts.length - 1].outcome;
+ return isSuccessfulOutcome(lastOutcome);
+}
+
+function maybeString(stringTable, index) {
+ return index ? stringTable[index] : undefined;
+}
+
+function maybeTypeset(typeset, stringTable) {
+ if (!typeset) {
+ return undefined;
+ }
+ return typeset.map(ty => {
+ return {
+ keyedBy: maybeString(stringTable, ty.keyedBy),
+ name: maybeString(stringTable, ty.name),
+ location: maybeString(stringTable, ty.location),
+ line: ty.line,
+ };
+ });
+}
+
+// Map of optimization implementation names to an enum.
+const IMPLEMENTATION_MAP = {
+ interpreter: 0,
+ baseline: 1,
+ ion: 2,
+};
+const IMPLEMENTATION_NAMES = Object.keys(IMPLEMENTATION_MAP);
+
+/**
+ * Takes data from a FrameNode and computes rendering positions for
+ * a stacked mountain graph, to visualize JIT optimization tiers over time.
+ *
+ * @param {FrameNode} frameNode
+ * The FrameNode who's optimizations we're iterating.
+ * @param {Array<number>} sampleTimes
+ * An array of every sample time within the range we're counting.
+ * From a ThreadNode's `sampleTimes` property.
+ * @param {number} bucketSize
+ * Size of each bucket in milliseconds.
+ * `duration / resolution = bucketSize` in OptimizationsGraph.
+ * @return {?Array<object>}
+ */
+function createTierGraphDataFromFrameNode(frameNode, sampleTimes, bucketSize) {
+ const tierData = frameNode.getTierData();
+ const stringTable = frameNode._stringTable;
+ const output = [];
+ let implEnum;
+
+ let tierDataIndex = 0;
+ let nextOptSample = tierData[tierDataIndex];
+
+ // Bucket data
+ let samplesInCurrentBucket = 0;
+ let currentBucketStartTime = sampleTimes[0];
+ let bucket = [];
+
+ // Store previous data point so we can have straight vertical lines
+ let previousValues;
+
+ // Iterate one after the samples, so we can finalize the last bucket
+ for (let i = 0; i <= sampleTimes.length; i++) {
+ const sampleTime = sampleTimes[i];
+
+ // If this sample is in the next bucket, or we're done
+ // checking sampleTimes and on the last iteration, finalize previous bucket
+ if (
+ sampleTime >= currentBucketStartTime + bucketSize ||
+ i >= sampleTimes.length
+ ) {
+ const dataPoint = {};
+ dataPoint.values = [];
+ dataPoint.delta = currentBucketStartTime;
+
+ // Map the opt site counts as a normalized percentage (0-1)
+ // of its count in context of total samples this bucket
+ for (let j = 0; j < IMPLEMENTATION_NAMES.length; j++) {
+ dataPoint.values[j] = (bucket[j] || 0) / (samplesInCurrentBucket || 1);
+ }
+
+ // Push the values from the previous bucket to the same time
+ // as the current bucket so we get a straight vertical line.
+ if (previousValues) {
+ const data = Object.create(null);
+ data.values = previousValues;
+ data.delta = currentBucketStartTime;
+ output.push(data);
+ }
+
+ output.push(dataPoint);
+
+ // Set the new start time of this bucket and reset its count
+ currentBucketStartTime += bucketSize;
+ samplesInCurrentBucket = 0;
+ previousValues = dataPoint.values;
+ bucket = [];
+ }
+
+ // If this sample observed an optimization in this frame, record it
+ if (nextOptSample && nextOptSample.time === sampleTime) {
+ // If no implementation defined, it was the "interpreter".
+ implEnum =
+ IMPLEMENTATION_MAP[
+ stringTable[nextOptSample.implementation] || "interpreter"
+ ];
+ bucket[implEnum] = (bucket[implEnum] || 0) + 1;
+ nextOptSample = tierData[++tierDataIndex];
+ }
+
+ samplesInCurrentBucket++;
+ }
+
+ return output;
+}
+
+exports.createTierGraphDataFromFrameNode = createTierGraphDataFromFrameNode;
+exports.OptimizationSite = OptimizationSite;
+exports.JITOptimizations = JITOptimizations;
+exports.hasSuccessfulOutcome = hasSuccessfulOutcome;
+exports.isSuccessfulOutcome = isSuccessfulOutcome;
+exports.SUCCESSFUL_OUTCOMES = SUCCESSFUL_OUTCOMES;
diff --git a/devtools/client/performance/modules/logic/moz.build b/devtools/client/performance/modules/logic/moz.build
new file mode 100644
index 0000000000..01f77231d7
--- /dev/null
+++ b/devtools/client/performance/modules/logic/moz.build
@@ -0,0 +1,12 @@
+# 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(
+ "frame-utils.js",
+ "jit.js",
+ "telemetry.js",
+ "tree-model.js",
+ "waterfall-utils.js",
+)
diff --git a/devtools/client/performance/modules/logic/telemetry.js b/devtools/client/performance/modules/logic/telemetry.js
new file mode 100644
index 0000000000..4ea267d747
--- /dev/null
+++ b/devtools/client/performance/modules/logic/telemetry.js
@@ -0,0 +1,106 @@
+/* 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 Telemetry = require("devtools/client/shared/telemetry");
+const EVENTS = require("devtools/client/performance/events");
+
+const EVENT_MAP_FLAGS = new Map([
+ [EVENTS.RECORDING_IMPORTED, "DEVTOOLS_PERFTOOLS_RECORDING_IMPORT_FLAG"],
+ [EVENTS.RECORDING_EXPORTED, "DEVTOOLS_PERFTOOLS_RECORDING_EXPORT_FLAG"],
+]);
+
+const RECORDING_FEATURES = [
+ "withMarkers",
+ "withTicks",
+ "withMemory",
+ "withAllocations",
+];
+
+const SELECTED_VIEW_HISTOGRAM_NAME = "DEVTOOLS_PERFTOOLS_SELECTED_VIEW_MS";
+
+function PerformanceTelemetry(emitter) {
+ this._emitter = emitter;
+ this._telemetry = new Telemetry();
+ this.onFlagEvent = this.onFlagEvent.bind(this);
+ this.onRecordingStateChange = this.onRecordingStateChange.bind(this);
+ this.onViewSelected = this.onViewSelected.bind(this);
+
+ for (const [event] of EVENT_MAP_FLAGS) {
+ this._emitter.on(event, this.onFlagEvent.bind(this, event));
+ }
+
+ this._emitter.on(EVENTS.RECORDING_STATE_CHANGE, this.onRecordingStateChange);
+ this._emitter.on(EVENTS.UI_DETAILS_VIEW_SELECTED, this.onViewSelected);
+}
+
+PerformanceTelemetry.prototype.destroy = function() {
+ if (this._previousView) {
+ this._telemetry.finishKeyed(
+ SELECTED_VIEW_HISTOGRAM_NAME,
+ this._previousView,
+ this,
+ false
+ );
+ }
+
+ for (const [event] of EVENT_MAP_FLAGS) {
+ this._emitter.off(event, this.onFlagEvent);
+ }
+ this._emitter.off(EVENTS.RECORDING_STATE_CHANGE, this.onRecordingStateChange);
+ this._emitter.off(EVENTS.UI_DETAILS_VIEW_SELECTED, this.onViewSelected);
+ this._emitter = null;
+};
+
+PerformanceTelemetry.prototype.onFlagEvent = function(eventName, ...data) {
+ this._telemetry.getHistogramById(EVENT_MAP_FLAGS.get(eventName)).add(true);
+};
+
+PerformanceTelemetry.prototype.onRecordingStateChange = function(
+ status,
+ model
+) {
+ if (status != "recording-stopped") {
+ return;
+ }
+
+ if (model.isConsole()) {
+ this._telemetry
+ .getHistogramById("DEVTOOLS_PERFTOOLS_CONSOLE_RECORDING_COUNT")
+ .add(true);
+ } else {
+ this._telemetry
+ .getHistogramById("DEVTOOLS_PERFTOOLS_RECORDING_COUNT")
+ .add(true);
+ }
+
+ this._telemetry
+ .getHistogramById("DEVTOOLS_PERFTOOLS_RECORDING_DURATION_MS")
+ .add(model.getDuration());
+
+ const config = model.getConfiguration();
+ for (const k in config) {
+ if (RECORDING_FEATURES.includes(k)) {
+ this._telemetry
+ .getKeyedHistogramById("DEVTOOLS_PERFTOOLS_RECORDING_FEATURES_USED")
+ .add(k, config[k]);
+ }
+ }
+};
+
+PerformanceTelemetry.prototype.onViewSelected = function(viewName) {
+ if (this._previousView) {
+ this._telemetry.finishKeyed(
+ SELECTED_VIEW_HISTOGRAM_NAME,
+ this._previousView,
+ this,
+ false
+ );
+ }
+ this._previousView = viewName;
+ this._telemetry.startKeyed(SELECTED_VIEW_HISTOGRAM_NAME, viewName, this);
+};
+
+exports.PerformanceTelemetry = PerformanceTelemetry;
diff --git a/devtools/client/performance/modules/logic/tree-model.js b/devtools/client/performance/modules/logic/tree-model.js
new file mode 100644
index 0000000000..518b4838a2
--- /dev/null
+++ b/devtools/client/performance/modules/logic/tree-model.js
@@ -0,0 +1,589 @@
+/* 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 {
+ JITOptimizations,
+} = require("devtools/client/performance/modules/logic/jit");
+const FrameUtils = require("devtools/client/performance/modules/logic/frame-utils");
+
+/**
+ * A call tree for a thread. This is essentially a linkage between all frames
+ * of all samples into a single tree structure, with additional information
+ * on each node, like the time spent (in milliseconds) and samples count.
+ *
+ * @param object thread
+ * The raw thread object received from the backend. Contains samples,
+ * stackTable, frameTable, and stringTable.
+ * @param object options
+ * Additional supported options
+ * - number startTime
+ * - number endTime
+ * - boolean contentOnly [optional]
+ * - boolean invertTree [optional]
+ * - boolean flattenRecursion [optional]
+ */
+function ThreadNode(thread, options = {}) {
+ if (options.endTime == void 0 || options.startTime == void 0) {
+ throw new Error("ThreadNode requires both `startTime` and `endTime`.");
+ }
+ this.samples = 0;
+ this.sampleTimes = [];
+ this.youngestFrameSamples = 0;
+ this.calls = [];
+ this.duration = options.endTime - options.startTime;
+ this.nodeType = "Thread";
+ this.inverted = options.invertTree;
+
+ // Total bytesize of all allocations if enabled
+ this.byteSize = 0;
+ this.youngestFrameByteSize = 0;
+
+ const { samples, stackTable, frameTable, stringTable } = thread;
+
+ // Nothing to do if there are no samples.
+ if (samples.data.length === 0) {
+ return;
+ }
+
+ this._buildInverted(samples, stackTable, frameTable, stringTable, options);
+ if (!options.invertTree) {
+ this._uninvert();
+ }
+}
+
+ThreadNode.prototype = {
+ /**
+ * Build an inverted call tree from profile samples. The format of the
+ * samples is described in tools/profiler/ProfileEntry.h, under the heading
+ * "Thread profile JSON Format".
+ *
+ * The profile data is naturally presented inverted. Inverting the call tree
+ * is also the default in the Performance tool.
+ *
+ * @param object samples
+ * The raw samples array received from the backend.
+ * @param object stackTable
+ * The table of deduplicated stacks from the backend.
+ * @param object frameTable
+ * The table of deduplicated frames from the backend.
+ * @param object stringTable
+ * The table of deduplicated strings from the backend.
+ * @param object options
+ * Additional supported options
+ * - number startTime
+ * - number endTime
+ * - boolean contentOnly [optional]
+ * - boolean invertTree [optional]
+ */
+ _buildInverted: function buildInverted(
+ samples,
+ stackTable,
+ frameTable,
+ stringTable,
+ options
+ ) {
+ function getOrAddFrameNode(
+ calls,
+ isLeaf,
+ frameKey,
+ inflatedFrame,
+ isMetaCategory,
+ leafTable
+ ) {
+ // Insert the inflated frame into the call tree at the current level.
+ let frameNode;
+
+ // Leaf nodes have fan out much greater than non-leaf nodes, thus the
+ // use of a hash table. Otherwise, do linear search.
+ //
+ // Note that this method is very hot, thus the manual looping over
+ // Array.prototype.find.
+ if (isLeaf) {
+ frameNode = leafTable[frameKey];
+ } else {
+ for (let i = 0; i < calls.length; i++) {
+ if (calls[i].key === frameKey) {
+ frameNode = calls[i];
+ break;
+ }
+ }
+ }
+
+ if (!frameNode) {
+ frameNode = new FrameNode(frameKey, inflatedFrame, isMetaCategory);
+ if (isLeaf) {
+ leafTable[frameKey] = frameNode;
+ }
+ calls.push(frameNode);
+ }
+
+ return frameNode;
+ }
+
+ const SAMPLE_STACK_SLOT = samples.schema.stack;
+ const SAMPLE_TIME_SLOT = samples.schema.time;
+ const SAMPLE_BYTESIZE_SLOT = samples.schema.size;
+
+ const STACK_PREFIX_SLOT = stackTable.schema.prefix;
+ const STACK_FRAME_SLOT = stackTable.schema.frame;
+
+ const getOrAddInflatedFrame = FrameUtils.getOrAddInflatedFrame;
+
+ const samplesData = samples.data;
+ const stacksData = stackTable.data;
+
+ // Caches.
+ const inflatedFrameCache = FrameUtils.getInflatedFrameCache(frameTable);
+ const leafTable = Object.create(null);
+
+ const startTime = options.startTime;
+ const endTime = options.endTime;
+ const flattenRecursion = options.flattenRecursion;
+
+ // Reused options object passed to InflatedFrame.prototype.getFrameKey.
+ const mutableFrameKeyOptions = {
+ contentOnly: options.contentOnly,
+ isRoot: false,
+ isLeaf: false,
+ isMetaCategoryOut: false,
+ };
+
+ let byteSize = 0;
+ for (let i = 0; i < samplesData.length; i++) {
+ const sample = samplesData[i];
+ const sampleTime = sample[SAMPLE_TIME_SLOT];
+
+ if (SAMPLE_BYTESIZE_SLOT !== void 0) {
+ byteSize = sample[SAMPLE_BYTESIZE_SLOT];
+ }
+
+ // A sample's end time is considered to be its time of sampling. Its
+ // start time is the sampling time of the previous sample.
+ //
+ // Thus, we compare sampleTime <= start instead of < to filter out
+ // samples that end exactly at the start time.
+ if (!sampleTime || sampleTime <= startTime || sampleTime > endTime) {
+ continue;
+ }
+
+ let stackIndex = sample[SAMPLE_STACK_SLOT];
+ let calls = this.calls;
+ let prevCalls = this.calls;
+ let prevFrameKey;
+ let isLeaf = (mutableFrameKeyOptions.isLeaf = true);
+ const skipRoot = options.invertTree;
+
+ // Inflate the stack and build the FrameNode call tree directly.
+ //
+ // In the profiler data, each frame's stack is referenced by an index
+ // into stackTable.
+ //
+ // Each entry in stackTable is a pair [ prefixIndex, frameIndex ]. The
+ // prefixIndex is itself an index into stackTable, referencing the
+ // prefix of the current stack (that is, the younger frames). In other
+ // words, the stackTable is encoded as a trie of the inverted
+ // callstack. The frameIndex is an index into frameTable, describing the
+ // frame at the current depth.
+ //
+ // This algorithm inflates each frame in the frame table while walking
+ // the stack trie as described above.
+ //
+ // The frame key is then computed from the inflated frame /and/ the
+ // current depth in the FrameNode call tree. That is, the frame key is
+ // not wholly determinable from just the inflated frame.
+ //
+ // For content frames, the frame key is just its location. For chrome
+ // frames, the key may be a metacategory or its location, depending on
+ // rendering options and its position in the FrameNode call tree.
+ //
+ // The frame key is then used to build up the inverted FrameNode call
+ // tree.
+ //
+ // Note that various filtering functions, such as filtering for content
+ // frames or flattening recursion, are inlined into the stack inflation
+ // loop. This is important for performance as it avoids intermediate
+ // structures and multiple passes.
+ while (stackIndex !== null) {
+ const stackEntry = stacksData[stackIndex];
+ const frameIndex = stackEntry[STACK_FRAME_SLOT];
+
+ // Fetch the stack prefix (i.e. older frames) index.
+ stackIndex = stackEntry[STACK_PREFIX_SLOT];
+
+ // Do not include the (root) node in this sample, as the costs of each frame
+ // will make it clear to differentiate (root)->B vs (root)->A->B
+ // when a tree is inverted, a revert of bug 1147604
+ if (stackIndex === null && skipRoot) {
+ break;
+ }
+
+ // Inflate the frame.
+ const inflatedFrame = getOrAddInflatedFrame(
+ inflatedFrameCache,
+ frameIndex,
+ frameTable,
+ stringTable
+ );
+
+ // Compute the frame key.
+ mutableFrameKeyOptions.isRoot = stackIndex === null;
+ const frameKey = inflatedFrame.getFrameKey(mutableFrameKeyOptions);
+
+ // An empty frame key means this frame should be skipped.
+ if (frameKey === "") {
+ continue;
+ }
+
+ // If we shouldn't flatten the current frame into the previous one, advance a
+ // level in the call tree.
+ const shouldFlatten = flattenRecursion && frameKey === prevFrameKey;
+ if (!shouldFlatten) {
+ calls = prevCalls;
+ }
+
+ const frameNode = getOrAddFrameNode(
+ calls,
+ isLeaf,
+ frameKey,
+ inflatedFrame,
+ mutableFrameKeyOptions.isMetaCategoryOut,
+ leafTable
+ );
+ if (isLeaf) {
+ frameNode.youngestFrameSamples++;
+ frameNode._addOptimizations(
+ inflatedFrame.optimizations,
+ inflatedFrame.implementation,
+ sampleTime,
+ stringTable
+ );
+
+ if (byteSize) {
+ frameNode.youngestFrameByteSize += byteSize;
+ }
+ }
+
+ // Don't overcount flattened recursive frames.
+ if (!shouldFlatten) {
+ frameNode.samples++;
+ if (byteSize) {
+ frameNode.byteSize += byteSize;
+ }
+ }
+
+ prevFrameKey = frameKey;
+ prevCalls = frameNode.calls;
+ isLeaf = mutableFrameKeyOptions.isLeaf = false;
+ }
+
+ this.samples++;
+ this.sampleTimes.push(sampleTime);
+ if (byteSize) {
+ this.byteSize += byteSize;
+ }
+ }
+ },
+
+ /**
+ * Uninverts the call tree after its having been built.
+ */
+ _uninvert: function uninvert() {
+ function mergeOrAddFrameNode(calls, node, samples, size) {
+ // Unlike the inverted call tree, we don't use a root table for the top
+ // level, as in general, there are many fewer entry points than
+ // leaves. Instead, linear search is used regardless of level.
+ for (let i = 0; i < calls.length; i++) {
+ if (calls[i].key === node.key) {
+ const foundNode = calls[i];
+ foundNode._merge(node, samples, size);
+ return foundNode.calls;
+ }
+ }
+ const copy = node._clone(samples, size);
+ calls.push(copy);
+ return copy.calls;
+ }
+
+ const workstack = [{ node: this, level: 0 }];
+ const spine = [];
+ let entry;
+
+ // The new root.
+ const rootCalls = [];
+
+ // Walk depth-first and keep the current spine (e.g., callstack).
+ do {
+ entry = workstack.pop();
+ if (entry) {
+ spine[entry.level] = entry;
+
+ const node = entry.node;
+ const calls = node.calls;
+ let callSamples = 0;
+ let callByteSize = 0;
+
+ // Continue the depth-first walk.
+ for (let i = 0; i < calls.length; i++) {
+ workstack.push({ node: calls[i], level: entry.level + 1 });
+ callSamples += calls[i].samples;
+ callByteSize += calls[i].byteSize;
+ }
+
+ // The sample delta is used to distinguish stacks.
+ //
+ // Suppose we have the following stack samples:
+ //
+ // A -> B
+ // A -> C
+ // A
+ //
+ // The inverted tree is:
+ //
+ // A
+ // / \
+ // B C
+ //
+ // with A.samples = 3, B.samples = 1, C.samples = 1.
+ //
+ // A is distinguished as being its own stack because
+ // A.samples - (B.samples + C.samples) > 0.
+ //
+ // Note that bottoming out is a degenerate where callSamples = 0.
+
+ const samplesDelta = node.samples - callSamples;
+ const byteSizeDelta = node.byteSize - callByteSize;
+ if (samplesDelta > 0) {
+ // Reverse the spine and add them to the uninverted call tree.
+ let uninvertedCalls = rootCalls;
+ for (let level = entry.level; level > 0; level--) {
+ const callee = spine[level];
+ uninvertedCalls = mergeOrAddFrameNode(
+ uninvertedCalls,
+ callee.node,
+ samplesDelta,
+ byteSizeDelta
+ );
+ }
+ }
+ }
+ } while (entry);
+
+ // Replace the toplevel calls with rootCalls, which now contains the
+ // uninverted roots.
+ this.calls = rootCalls;
+ },
+
+ /**
+ * Gets additional details about this node.
+ * @see FrameNode.prototype.getInfo for more information.
+ *
+ * @return object
+ */
+ getInfo: function(options) {
+ return FrameUtils.getFrameInfo(this, options);
+ },
+
+ /**
+ * Mimicks the interface of FrameNode, and a ThreadNode can never have
+ * optimization data (at the moment, anyway), so provide a function
+ * to return null so we don't need to check if a frame node is a thread
+ * or not everytime we fetch optimization data.
+ *
+ * @return {null}
+ */
+
+ hasOptimizations: function() {
+ return null;
+ },
+};
+
+/**
+ * A function call node in a tree. Represents a function call with a unique context,
+ * resulting in each FrameNode having its own row in the corresponding tree view.
+ * Take samples:
+ * A()->B()->C()
+ * A()->B()
+ * Q()->B()
+ *
+ * In inverted tree, A()->B()->C() would have one frame node, and A()->B() and
+ * Q()->B() would share a frame node.
+ * In an uninverted tree, A()->B()->C() and A()->B() would share a frame node,
+ * with Q()->B() having its own.
+ *
+ * In all cases, all the frame nodes originated from the same InflatedFrame.
+ *
+ * @param string frameKey
+ * The key associated with this frame. The key determines identity of
+ * the node.
+ * @param string location
+ * The location of this function call. Note that this isn't sanitized,
+ * so it may very well (not?) include the function name, url, etc.
+ * @param number line
+ * The line number inside the source containing this function call.
+ * @param number category
+ * The category type of this function call ("js", "graphics" etc.).
+ * @param number allocations
+ * The number of memory allocations performed in this frame.
+ * @param number isContent
+ * Whether this frame is content.
+ * @param boolean isMetaCategory
+ * Whether or not this is a platform node that should appear as a
+ * generalized meta category or not.
+ */
+function FrameNode(
+ frameKey,
+ { location, line, category, isContent },
+ isMetaCategory
+) {
+ this.key = frameKey;
+ this.location = location;
+ this.line = line;
+ this.youngestFrameSamples = 0;
+ this.samples = 0;
+ this.calls = [];
+ this.isContent = !!isContent;
+ this._optimizations = null;
+ this._tierData = [];
+ this._stringTable = null;
+ this.isMetaCategory = !!isMetaCategory;
+ this.category = category;
+ this.nodeType = "Frame";
+ this.byteSize = 0;
+ this.youngestFrameByteSize = 0;
+}
+
+FrameNode.prototype = {
+ /**
+ * Take optimization data observed for this frame.
+ *
+ * @param object optimizationSite
+ * Any JIT optimization information attached to the current
+ * sample. Lazily inflated via stringTable.
+ * @param number implementation
+ * JIT implementation used for this observed frame (baseline, ion);
+ * can be null indicating "interpreter"
+ * @param number time
+ * The time this optimization occurred.
+ * @param object stringTable
+ * The string table used to inflate the optimizationSite.
+ */
+ _addOptimizations: function(site, implementation, time, stringTable) {
+ // Simply accumulate optimization sites for now. Processing is done lazily
+ // by JITOptimizations, if optimization information is actually displayed.
+ if (site) {
+ let opts = this._optimizations;
+ if (opts === null) {
+ opts = this._optimizations = [];
+ }
+ opts.push(site);
+ }
+
+ if (!this._stringTable) {
+ this._stringTable = stringTable;
+ }
+
+ // Record type of implementation used and the sample time
+ this._tierData.push({ implementation, time });
+ },
+
+ _clone: function(samples, size) {
+ const newNode = new FrameNode(this.key, this, this.isMetaCategory);
+ newNode._merge(this, samples, size);
+ return newNode;
+ },
+
+ _merge: function(otherNode, samples, size) {
+ if (this === otherNode) {
+ return;
+ }
+
+ this.samples += samples;
+ this.byteSize += size;
+ if (otherNode.youngestFrameSamples > 0) {
+ this.youngestFrameSamples += samples;
+ }
+
+ if (otherNode.youngestFrameByteSize > 0) {
+ this.youngestFrameByteSize += otherNode.youngestFrameByteSize;
+ }
+
+ if (this._stringTable === null) {
+ this._stringTable = otherNode._stringTable;
+ }
+
+ if (otherNode._optimizations) {
+ if (!this._optimizations) {
+ this._optimizations = [];
+ }
+ const opts = this._optimizations;
+ const otherOpts = otherNode._optimizations;
+ for (let i = 0; i < otherOpts.length; i++) {
+ opts.push(otherOpts[i]);
+ }
+ }
+
+ if (otherNode._tierData.length) {
+ const tierData = this._tierData;
+ const otherTierData = otherNode._tierData;
+ for (let i = 0; i < otherTierData.length; i++) {
+ tierData.push(otherTierData[i]);
+ }
+ tierData.sort((a, b) => a.time - b.time);
+ }
+ },
+
+ /**
+ * Returns the parsed location and additional data describing
+ * this frame. Uses cached data if possible. Takes the following
+ * options:
+ *
+ * @param {ThreadNode} options.root
+ * The root thread node to calculate relative costs.
+ * Generates [self|total] [duration|percentage] values.
+ * @param {boolean} options.allocations
+ * Generates `totalAllocations` and `selfAllocations`.
+ *
+ * @return object
+ * The computed { name, file, url, line } properties for this
+ * function call, as well as additional params if options specified.
+ */
+ getInfo: function(options) {
+ return FrameUtils.getFrameInfo(this, options);
+ },
+
+ /**
+ * Returns whether or not the frame node has an JITOptimizations model.
+ *
+ * @return {Boolean}
+ */
+ hasOptimizations: function() {
+ return !this.isMetaCategory && !!this._optimizations;
+ },
+
+ /**
+ * Returns the underlying JITOptimizations model representing
+ * the optimization attempts occuring in this frame.
+ *
+ * @return {JITOptimizations|null}
+ */
+ getOptimizations: function() {
+ if (!this._optimizations) {
+ return null;
+ }
+ return new JITOptimizations(this._optimizations, this._stringTable);
+ },
+
+ /**
+ * Returns the tiers used overtime.
+ *
+ * @return {Array<object>}
+ */
+ getTierData: function() {
+ return this._tierData;
+ },
+};
+
+exports.ThreadNode = ThreadNode;
+exports.FrameNode = FrameNode;
diff --git a/devtools/client/performance/modules/logic/waterfall-utils.js b/devtools/client/performance/modules/logic/waterfall-utils.js
new file mode 100644
index 0000000000..5fc7e768e1
--- /dev/null
+++ b/devtools/client/performance/modules/logic/waterfall-utils.js
@@ -0,0 +1,171 @@
+/* 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";
+
+/**
+ * Utility functions for collapsing markers into a waterfall.
+ */
+
+const {
+ MarkerBlueprintUtils,
+} = require("devtools/client/performance/modules/marker-blueprint-utils");
+
+/**
+ * Creates a parent marker, which functions like a regular marker,
+ * but is able to hold additional child markers.
+ *
+ * The marker is seeded with values from `marker`.
+ * @param object marker
+ * @return object
+ */
+function createParentNode(marker) {
+ return Object.assign({}, marker, { submarkers: [] });
+}
+
+/**
+ * Collapses markers into a tree-like structure.
+ * @param object rootNode
+ * @param array markersList
+ * @param array filter
+ */
+function collapseMarkersIntoNode({ rootNode, markersList, filter }) {
+ const {
+ getCurrentParentNode,
+ pushNode,
+ popParentNode,
+ } = createParentNodeFactory(rootNode);
+
+ for (let i = 0, len = markersList.length; i < len; i++) {
+ const curr = markersList[i];
+
+ // If this marker type should not be displayed, just skip
+ if (!MarkerBlueprintUtils.shouldDisplayMarker(curr, filter)) {
+ continue;
+ }
+
+ let parentNode = getCurrentParentNode();
+ const blueprint = MarkerBlueprintUtils.getBlueprintFor(curr);
+
+ const nestable = "nestable" in blueprint ? blueprint.nestable : true;
+ const collapsible =
+ "collapsible" in blueprint ? blueprint.collapsible : true;
+
+ let finalized = false;
+
+ // Extend the marker with extra properties needed in the marker tree
+ const extendedProps = { index: i };
+ if (collapsible) {
+ extendedProps.submarkers = [];
+ }
+ Object.assign(curr, extendedProps);
+
+ // If not nestible, just push it inside the root node. Additionally,
+ // markers originating outside the main thread are considered to be
+ // "never collapsible", to avoid confusion.
+ // A beter solution would be to collapse every marker with its siblings
+ // from the same thread, but that would require a thread id attached
+ // to all markers, which is potentially expensive and rather useless at
+ // the moment, since we don't really have that many OTMT markers.
+ if (!nestable || curr.isOffMainThread) {
+ pushNode(rootNode, curr);
+ continue;
+ }
+
+ // First off, if any parent nodes exist, finish them off
+ // recursively upwards if this marker is outside their ranges and nestable.
+ while (!finalized && parentNode) {
+ // If this marker is eclipsed by the current parent marker,
+ // make it a child of the current parent and stop going upwards.
+ // If the markers aren't from the same process, attach them to the root
+ // node as well. Every process has its own main thread.
+ if (
+ nestable &&
+ curr.start >= parentNode.start &&
+ curr.end <= parentNode.end &&
+ curr.processType == parentNode.processType
+ ) {
+ pushNode(parentNode, curr);
+ finalized = true;
+ break;
+ }
+
+ // If this marker is still nestable, but outside of the range
+ // of the current parent, iterate upwards on the next parent
+ // and finalize the current parent.
+ if (nestable) {
+ popParentNode();
+ parentNode = getCurrentParentNode();
+ continue;
+ }
+ }
+
+ if (!finalized) {
+ pushNode(rootNode, curr);
+ }
+ }
+}
+
+/**
+ * Takes a root marker node and creates a hash of functions used
+ * to manage the creation and nesting of additional parent markers.
+ *
+ * @param {object} root
+ * @return {object}
+ */
+function createParentNodeFactory(root) {
+ const parentMarkers = [];
+ const factory = {
+ /**
+ * Pops the most recent parent node off the stack, finalizing it.
+ * Sets the `end` time based on the most recent child if not defined.
+ */
+ popParentNode: () => {
+ if (parentMarkers.length === 0) {
+ throw new Error("Cannot pop parent markers when none exist.");
+ }
+
+ const lastParent = parentMarkers.pop();
+
+ // If this finished parent marker doesn't have an end time,
+ // so probably a synthesized marker, use the last marker's end time.
+ if (lastParent.end == void 0) {
+ lastParent.end =
+ lastParent.submarkers[lastParent.submarkers.length - 1].end;
+ }
+
+ // If no children were ever pushed into this parent node,
+ // remove its submarkers so it behaves like a non collapsible
+ // node.
+ if (!lastParent.submarkers.length) {
+ delete lastParent.submarkers;
+ }
+
+ return lastParent;
+ },
+
+ /**
+ * Returns the most recent parent node.
+ */
+ getCurrentParentNode: () =>
+ parentMarkers.length ? parentMarkers[parentMarkers.length - 1] : null,
+
+ /**
+ * Push this marker into the most recent parent node.
+ */
+ pushNode: (parent, marker) => {
+ parent.submarkers.push(marker);
+
+ // If pushing a parent marker, track it as the top of
+ // the parent stack.
+ if (marker.submarkers) {
+ parentMarkers.push(marker);
+ }
+ },
+ };
+
+ return factory;
+}
+
+exports.createParentNode = createParentNode;
+exports.collapseMarkersIntoNode = collapseMarkersIntoNode;
diff --git a/devtools/client/performance/modules/marker-blueprint-utils.js b/devtools/client/performance/modules/marker-blueprint-utils.js
new file mode 100644
index 0000000000..f249c0cb7b
--- /dev/null
+++ b/devtools/client/performance/modules/marker-blueprint-utils.js
@@ -0,0 +1,110 @@
+/* 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 {
+ TIMELINE_BLUEPRINT,
+} = require("devtools/client/performance/modules/markers");
+
+/**
+ * This file contains utilities for parsing out the markers blueprint
+ * to generate strings to be displayed in the UI.
+ */
+
+exports.MarkerBlueprintUtils = {
+ /**
+ * Takes a marker and a list of marker names that should be hidden, and
+ * determines if this marker should be filtered or not.
+ *
+ * @param object marker
+ * @return boolean
+ */
+ shouldDisplayMarker: function(marker, hiddenMarkerNames) {
+ if (!hiddenMarkerNames || hiddenMarkerNames.length == 0) {
+ return true;
+ }
+
+ // If this marker isn't yet defined in the blueprint, simply check if the
+ // entire category of "UNKNOWN" markers are supposed to be visible or not.
+ const isUnknown = !(marker.name in TIMELINE_BLUEPRINT);
+ if (isUnknown) {
+ return !hiddenMarkerNames.includes("UNKNOWN");
+ }
+
+ return !hiddenMarkerNames.includes(marker.name);
+ },
+
+ /**
+ * Takes a marker and returns the blueprint definition for that marker type,
+ * falling back to the UNKNOWN blueprint definition if undefined.
+ *
+ * @param object marker
+ * @return object
+ */
+ getBlueprintFor: function(marker) {
+ return TIMELINE_BLUEPRINT[marker.name] || TIMELINE_BLUEPRINT.UNKNOWN;
+ },
+
+ /**
+ * Returns the label to display for a marker, based off the blueprints.
+ *
+ * @param object marker
+ * @return string
+ */
+ getMarkerLabel: function(marker) {
+ const blueprint = this.getBlueprintFor(marker);
+ const dynamic = typeof blueprint.label === "function";
+ const label = dynamic ? blueprint.label(marker) : blueprint.label;
+ return label;
+ },
+
+ /**
+ * Returns the generic label to display for a marker name.
+ * (e.g. "Function Call" for JS markers, rather than "setTimeout", etc.)
+ *
+ * @param string type
+ * @return string
+ */
+ getMarkerGenericName: function(markerName) {
+ const blueprint = this.getBlueprintFor({ name: markerName });
+ const dynamic = typeof blueprint.label === "function";
+ const generic = dynamic ? blueprint.label() : blueprint.label;
+
+ // If no class name found, attempt to throw a descriptive error as to
+ // how the marker implementor can fix this.
+ if (!generic) {
+ let message = `Could not find marker generic name for "${markerName}".`;
+ if (typeof blueprint.label === "function") {
+ message +=
+ ` The following function must return a generic name string when no` +
+ ` marker passed: ${blueprint.label}`;
+ } else {
+ message += ` ${markerName}.label must be defined in the marker blueprint.`;
+ }
+ throw new Error(message);
+ }
+
+ return generic;
+ },
+
+ /**
+ * Returns an array of objects with key/value pairs of what should be rendered
+ * in the marker details view.
+ *
+ * @param object marker
+ * @return array<object>
+ */
+ getMarkerFields: function(marker) {
+ const blueprint = this.getBlueprintFor(marker);
+ const dynamic = typeof blueprint.fields === "function";
+ const fields = dynamic ? blueprint.fields(marker) : blueprint.fields;
+
+ return Object.entries(fields || {})
+ .filter(([_, value]) => (dynamic ? true : value in marker))
+ .map(([label, value]) => ({
+ label,
+ value: dynamic ? value : marker[value],
+ }));
+ },
+};
diff --git a/devtools/client/performance/modules/marker-dom-utils.js b/devtools/client/performance/modules/marker-dom-utils.js
new file mode 100644
index 0000000000..556f940da9
--- /dev/null
+++ b/devtools/client/performance/modules/marker-dom-utils.js
@@ -0,0 +1,275 @@
+/* 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 utilities for creating DOM nodes for markers
+ * to be displayed in the UI.
+ */
+
+const { L10N, PREFS } = require("devtools/client/performance/modules/global");
+const {
+ MarkerBlueprintUtils,
+} = require("devtools/client/performance/modules/marker-blueprint-utils");
+const { getSourceNames } = require("devtools/client/shared/source-utils");
+
+/**
+ * Utilites for creating elements for markers.
+ */
+exports.MarkerDOMUtils = {
+ /**
+ * Builds all the fields possible for the given marker. Returns an
+ * array of elements to be appended to a parent element.
+ *
+ * @param document doc
+ * @param object marker
+ * @return array<Node>
+ */
+ buildFields: function(doc, marker) {
+ const fields = MarkerBlueprintUtils.getMarkerFields(marker);
+ return fields.map(({ label, value }) =>
+ this.buildNameValueLabel(doc, label, value)
+ );
+ },
+
+ /**
+ * Builds the label representing the marker's type.
+ *
+ * @param document doc
+ * @param object marker
+ * @return Node
+ */
+ buildTitle: function(doc, marker) {
+ const blueprint = MarkerBlueprintUtils.getBlueprintFor(marker);
+
+ const hbox = doc.createXULElement("hbox");
+ hbox.setAttribute("align", "center");
+
+ const bullet = doc.createXULElement("hbox");
+ bullet.className = `marker-details-bullet marker-color-${blueprint.colorName}`;
+
+ const title = MarkerBlueprintUtils.getMarkerLabel(marker);
+ const label = doc.createXULElement("label");
+ label.className = "marker-details-type";
+ label.setAttribute("value", title);
+
+ hbox.appendChild(bullet);
+ hbox.appendChild(label);
+
+ return hbox;
+ },
+
+ /**
+ * Builds the label representing the marker's duration.
+ *
+ * @param document doc
+ * @param object marker
+ * @return Node
+ */
+ buildDuration: function(doc, marker) {
+ const label = L10N.getStr("marker.field.duration");
+ const start = L10N.getFormatStrWithNumbers("timeline.tick", marker.start);
+ const end = L10N.getFormatStrWithNumbers("timeline.tick", marker.end);
+ const duration = L10N.getFormatStrWithNumbers(
+ "timeline.tick",
+ marker.end - marker.start
+ );
+
+ const el = this.buildNameValueLabel(doc, label, duration);
+ el.classList.add("marker-details-duration");
+ el.setAttribute("tooltiptext", `${start} → ${end}`);
+
+ return el;
+ },
+
+ /**
+ * Builds labels for name:value pairs.
+ * E.g. "Start: 100ms", "Duration: 200ms", ...
+ *
+ * @param document doc
+ * @param string field
+ * @param string value
+ * @return Node
+ */
+ buildNameValueLabel: function(doc, field, value) {
+ const hbox = doc.createXULElement("hbox");
+ hbox.className = "marker-details-labelcontainer";
+
+ const nameLabel = doc.createXULElement("label");
+ nameLabel.className = "plain marker-details-name-label";
+ nameLabel.setAttribute("value", field);
+ hbox.appendChild(nameLabel);
+
+ const valueLabel = doc.createXULElement("label");
+ valueLabel.className = "plain marker-details-value-label";
+ valueLabel.setAttribute("value", value);
+ hbox.appendChild(valueLabel);
+
+ return hbox;
+ },
+
+ /**
+ * Builds a stack trace in an element.
+ *
+ * @param document doc
+ * @param object params
+ * An options object with the following members:
+ * - string type: string identifier for type of stack ("stack", "startStack"
+ or "endStack"
+ * - number frameIndex: the index of the topmost stack frame
+ * - array frames: array of stack frames
+ */
+ buildStackTrace: function(doc, { type, frameIndex, frames }) {
+ const container = doc.createXULElement("vbox");
+ container.className = "marker-details-stack";
+ container.setAttribute("type", type);
+
+ const nameLabel = doc.createXULElement("label");
+ nameLabel.className = "plain marker-details-name-label";
+ nameLabel.setAttribute("value", L10N.getStr(`marker.field.${type}`));
+ container.appendChild(nameLabel);
+
+ // Workaround for profiles that have looping stack traces. See
+ // bug 1246555.
+ let wasAsyncParent = false;
+ const seen = new Set();
+
+ while (frameIndex > 0) {
+ if (seen.has(frameIndex)) {
+ break;
+ }
+ seen.add(frameIndex);
+
+ const frame = frames[frameIndex];
+ const url = frame.source;
+ const displayName = frame.functionDisplayName;
+ const line = frame.line;
+
+ // If the previous frame had an async parent, then the async
+ // cause is in this frame and should be displayed.
+ if (wasAsyncParent) {
+ const asyncStr = L10N.getFormatStr(
+ "marker.field.asyncStack",
+ frame.asyncCause
+ );
+ const asyncBox = doc.createXULElement("hbox");
+ const asyncLabel = doc.createXULElement("label");
+ asyncLabel.className = "devtools-monospace";
+ asyncLabel.setAttribute("value", asyncStr);
+ asyncBox.appendChild(asyncLabel);
+ container.appendChild(asyncBox);
+ wasAsyncParent = false;
+ }
+
+ const hbox = doc.createXULElement("hbox");
+
+ if (displayName) {
+ const functionLabel = doc.createXULElement("label");
+ functionLabel.className = "devtools-monospace";
+ functionLabel.setAttribute("value", displayName);
+ hbox.appendChild(functionLabel);
+ }
+
+ if (url) {
+ const linkNode = doc.createXULElement("a");
+ linkNode.className = "waterfall-marker-location devtools-source-link";
+ linkNode.href = url;
+ linkNode.draggable = false;
+ linkNode.setAttribute("title", url);
+
+ const urlLabel = doc.createXULElement("label");
+ urlLabel.className = "filename";
+ urlLabel.setAttribute("value", getSourceNames(url).short);
+ linkNode.appendChild(urlLabel);
+
+ const lineLabel = doc.createXULElement("label");
+ lineLabel.className = "line-number";
+ lineLabel.setAttribute("value", `:${line}`);
+ linkNode.appendChild(lineLabel);
+
+ hbox.appendChild(linkNode);
+
+ // Clicking here will bubble up to the parent,
+ // which handles the view source.
+ linkNode.setAttribute(
+ "data-action",
+ JSON.stringify({
+ url: url,
+ line: line,
+ action: "view-source",
+ })
+ );
+ }
+
+ if (!displayName && !url) {
+ const unknownLabel = doc.createXULElement("label");
+ unknownLabel.setAttribute(
+ "value",
+ L10N.getStr("marker.value.unknownFrame")
+ );
+ hbox.appendChild(unknownLabel);
+ }
+
+ container.appendChild(hbox);
+
+ if (frame.asyncParent) {
+ frameIndex = frame.asyncParent;
+ wasAsyncParent = true;
+ } else {
+ frameIndex = frame.parent;
+ }
+ }
+
+ return container;
+ },
+
+ /**
+ * Builds any custom fields specific to the marker.
+ *
+ * @param document doc
+ * @param object marker
+ * @param object options
+ * @return array<Node>
+ */
+ buildCustom: function(doc, marker, options) {
+ const elements = [];
+
+ if (options.allocations && shouldShowAllocationsTrigger(marker)) {
+ const hbox = doc.createXULElement("hbox");
+ hbox.className = "marker-details-customcontainer";
+
+ const label = doc.createXULElement("label");
+ label.className = "custom-button";
+ label.setAttribute("value", "Show allocation triggers");
+ label.setAttribute("type", "show-allocations");
+ label.setAttribute(
+ "data-action",
+ JSON.stringify({
+ endTime: marker.start,
+ action: "show-allocations",
+ })
+ );
+
+ hbox.appendChild(label);
+ elements.push(hbox);
+ }
+
+ return elements;
+ },
+};
+
+/**
+ * Takes a marker and determines if this marker should display
+ * the allocations trigger button.
+ *
+ * @param object marker
+ * @return boolean
+ */
+function shouldShowAllocationsTrigger(marker) {
+ if (marker.name == "GarbageCollection") {
+ const showTriggers = PREFS["show-triggers-for-gc-types"];
+ return showTriggers.split(" ").includes(marker.causeName);
+ }
+ return false;
+}
diff --git a/devtools/client/performance/modules/marker-formatters.js b/devtools/client/performance/modules/marker-formatters.js
new file mode 100644
index 0000000000..b4b9472259
--- /dev/null
+++ b/devtools/client/performance/modules/marker-formatters.js
@@ -0,0 +1,204 @@
+/* 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 utilities for creating elements for markers to be displayed,
+ * and parsing out the blueprint to generate correct values for markers.
+ */
+const { L10N, PREFS } = require("devtools/client/performance/modules/global");
+
+// String used to fill in platform data when it should be hidden.
+const GECKO_SYMBOL = "(Gecko)";
+
+/**
+ * Mapping of JS marker causes to a friendlier form. Only
+ * markers that are considered "from content" should be labeled here.
+ */
+const JS_MARKER_MAP = {
+ "<script> element": L10N.getStr("marker.label.javascript.scriptElement"),
+ "promise callback": L10N.getStr("marker.label.javascript.promiseCallback"),
+ "promise initializer": L10N.getStr("marker.label.javascript.promiseInit"),
+ "Worker runnable": L10N.getStr("marker.label.javascript.workerRunnable"),
+ "javascript: URI": L10N.getStr("marker.label.javascript.jsURI"),
+ // The difference between these two event handler markers are differences
+ // in their WebIDL implementation, so distinguishing them is not necessary.
+ EventHandlerNonNull: L10N.getStr("marker.label.javascript.eventHandler"),
+ "EventListener.handleEvent": L10N.getStr(
+ "marker.label.javascript.eventHandler"
+ ),
+ // These markers do not get L10N'd because they're JS names.
+ "setInterval handler": "setInterval",
+ "setTimeout handler": "setTimeout",
+ FrameRequestCallback: "requestAnimationFrame",
+};
+
+/**
+ * A series of formatters used by the blueprint.
+ */
+exports.Formatters = {
+ /**
+ * Uses the marker name as the label for markers that do not have
+ * a blueprint entry. Uses "Other" in the marker filter menu.
+ */
+ UnknownLabel: function(marker = {}) {
+ return marker.name || L10N.getStr("marker.label.unknown");
+ },
+
+ /* Group 0 - Reflow and Rendering pipeline */
+
+ StylesFields: function(marker) {
+ if ("isAnimationOnly" in marker) {
+ return {
+ [L10N.getStr("marker.field.isAnimationOnly")]: marker.isAnimationOnly,
+ };
+ }
+ return null;
+ },
+
+ /* Group 1 - JS */
+
+ DOMEventFields: function(marker) {
+ const fields = Object.create(null);
+
+ if ("type" in marker) {
+ fields[L10N.getStr("marker.field.DOMEventType")] = marker.type;
+ }
+
+ if ("eventPhase" in marker) {
+ let label;
+ switch (marker.eventPhase) {
+ case Event.AT_TARGET:
+ label = L10N.getStr("marker.value.DOMEventTargetPhase");
+ break;
+ case Event.CAPTURING_PHASE:
+ label = L10N.getStr("marker.value.DOMEventCapturingPhase");
+ break;
+ case Event.BUBBLING_PHASE:
+ label = L10N.getStr("marker.value.DOMEventBubblingPhase");
+ break;
+ }
+ fields[L10N.getStr("marker.field.DOMEventPhase")] = label;
+ }
+
+ return fields;
+ },
+
+ JSLabel: function(marker = {}) {
+ const generic = L10N.getStr("marker.label.javascript");
+ if ("causeName" in marker) {
+ return JS_MARKER_MAP[marker.causeName] || generic;
+ }
+ return generic;
+ },
+
+ JSFields: function(marker) {
+ if ("causeName" in marker && !JS_MARKER_MAP[marker.causeName]) {
+ const label = PREFS["show-platform-data"]
+ ? marker.causeName
+ : GECKO_SYMBOL;
+ return {
+ [L10N.getStr("marker.field.causeName")]: label,
+ };
+ }
+ return null;
+ },
+
+ GCLabel: function(marker) {
+ if (!marker) {
+ return L10N.getStr("marker.label.garbageCollection2");
+ }
+ // Only if a `nonincrementalReason` exists, do we want to label
+ // this as a non incremental GC event.
+ if ("nonincrementalReason" in marker) {
+ return L10N.getStr("marker.label.garbageCollection.nonIncremental");
+ }
+ return L10N.getStr("marker.label.garbageCollection.incremental");
+ },
+
+ GCFields: function(marker) {
+ const fields = Object.create(null);
+
+ if ("causeName" in marker) {
+ const cause = marker.causeName;
+ const label = L10N.getStr(`marker.gcreason.label.${cause}`) || cause;
+ fields[L10N.getStr("marker.field.causeName")] = label;
+ }
+
+ if ("nonincrementalReason" in marker) {
+ const label = marker.nonincrementalReason;
+ fields[L10N.getStr("marker.field.nonIncrementalCause")] = label;
+ }
+
+ return fields;
+ },
+
+ MinorGCFields: function(marker) {
+ const fields = Object.create(null);
+
+ if ("causeName" in marker) {
+ const cause = marker.causeName;
+ const label = L10N.getStr(`marker.gcreason.label.${cause}`) || cause;
+ fields[L10N.getStr("marker.field.causeName")] = label;
+ }
+
+ fields[L10N.getStr("marker.field.type")] = L10N.getStr(
+ "marker.nurseryCollection"
+ );
+
+ return fields;
+ },
+
+ CycleCollectionFields: function(marker) {
+ const label = marker.name.replace(/nsCycleCollector::/g, "");
+ return {
+ [L10N.getStr("marker.field.type")]: label,
+ };
+ },
+
+ WorkerFields: function(marker) {
+ if ("workerOperation" in marker) {
+ const label = L10N.getStr(`marker.worker.${marker.workerOperation}`);
+ return {
+ [L10N.getStr("marker.field.type")]: label,
+ };
+ }
+ return null;
+ },
+
+ MessagePortFields: function(marker) {
+ if ("messagePortOperation" in marker) {
+ const label = L10N.getStr(
+ `marker.messagePort.${marker.messagePortOperation}`
+ );
+ return {
+ [L10N.getStr("marker.field.type")]: label,
+ };
+ }
+ return null;
+ },
+
+ /* Group 2 - User Controlled */
+
+ ConsoleTimeFields: {
+ [L10N.getStr("marker.field.consoleTimerName")]: "causeName",
+ },
+
+ TimeStampFields: {
+ [L10N.getStr("marker.field.label")]: "causeName",
+ },
+};
+
+/**
+ * Takes a main label (e.g. "Timestamp") and a property name (e.g. "causeName"),
+ * and returns a string that represents that property value for a marker if it
+ * exists (e.g. "Timestamp (rendering)"), or just the main label if it does not.
+ *
+ * @param string mainLabel
+ * @param string propName
+ */
+exports.Formatters.labelForProperty = function(mainLabel, propName) {
+ return (marker = {}) =>
+ marker[propName] ? `${mainLabel} (${marker[propName]})` : mainLabel;
+};
diff --git a/devtools/client/performance/modules/markers.js b/devtools/client/performance/modules/markers.js
new file mode 100644
index 0000000000..dbcd661205
--- /dev/null
+++ b/devtools/client/performance/modules/markers.js
@@ -0,0 +1,180 @@
+/* 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 {
+ Formatters,
+} = require("devtools/client/performance/modules/marker-formatters");
+
+/**
+ * A simple schema for mapping markers to the timeline UI. The keys correspond
+ * to marker names, while the values are objects with the following format:
+ *
+ * - group: The row index in the overview graph; multiple markers
+ * can be added on the same row. @see <overview.js/buildGraphImage>
+ * - label: The label used in the waterfall to identify the marker. Can be a
+ * string or just a function that accepts the marker and returns a
+ * string (if you want to use a dynamic property for the main label).
+ * If you use a function for a label, it *must* handle the case where
+ * no marker is provided, to get a generic label used to describe
+ * all markers of this type.
+ * - fields: The fields used in the marker details view to display more
+ * information about a currently selected marker. Can either be an
+ * object of fields, or simply a function that accepts the marker and
+ * returns such an object (if you want to use properties dynamically).
+ * For example, a field in the object such as { "Cause": "causeName" }
+ * would render something like `Cause: ${marker.causeName}` in the UI.
+ * - colorName: The label of the DevTools color used for this marker. If
+ * adding a new color, be sure to check that there's an entry
+ * for `.marker-color-graphs-{COLORNAME}` for the equivilent
+ * entry in "./devtools/client/themes/performance.css"
+ * - collapsible: Whether or not this marker can contain other markers it
+ * eclipses, and becomes collapsible to reveal its nestable
+ * children. Defaults to true.
+ * - nestable: Whether or not this marker can be nested inside an eclipsing
+ * collapsible marker. Defaults to true.
+ */
+const TIMELINE_BLUEPRINT = {
+ /* Default definition used for markers that occur but are not defined here.
+ * Should ultimately be defined, but this gives us room to work on the
+ * front end separately from the platform. */
+ UNKNOWN: {
+ group: 2,
+ colorName: "graphs-grey",
+ label: Formatters.UnknownLabel,
+ },
+
+ /* Group 0 - Reflow and Rendering pipeline */
+
+ Styles: {
+ group: 0,
+ colorName: "graphs-purple",
+ label: L10N.getStr("marker.label.styles"),
+ fields: Formatters.StylesFields,
+ },
+ StylesApplyChanges: {
+ group: 0,
+ colorName: "graphs-purple",
+ label: L10N.getStr("marker.label.stylesApplyChanges"),
+ },
+ Reflow: {
+ group: 0,
+ colorName: "graphs-purple",
+ label: L10N.getStr("marker.label.reflow"),
+ },
+ Paint: {
+ group: 0,
+ colorName: "graphs-green",
+ label: L10N.getStr("marker.label.paint"),
+ },
+ Composite: {
+ group: 0,
+ colorName: "graphs-green",
+ label: L10N.getStr("marker.label.composite"),
+ },
+ CompositeForwardTransaction: {
+ group: 0,
+ colorName: "graphs-bluegrey",
+ label: L10N.getStr("marker.label.compositeForwardTransaction"),
+ },
+
+ /* Group 1 - JS */
+
+ DOMEvent: {
+ group: 1,
+ colorName: "graphs-yellow",
+ label: L10N.getStr("marker.label.domevent"),
+ fields: Formatters.DOMEventFields,
+ },
+ "document::DOMContentLoaded": {
+ group: 1,
+ colorName: "graphs-full-red",
+ label: "DOMContentLoaded",
+ },
+ "document::Load": {
+ group: 1,
+ colorName: "graphs-full-blue",
+ label: "Load",
+ },
+ Javascript: {
+ group: 1,
+ colorName: "graphs-yellow",
+ label: Formatters.JSLabel,
+ fields: Formatters.JSFields,
+ },
+ "Parse HTML": {
+ group: 1,
+ colorName: "graphs-yellow",
+ label: L10N.getStr("marker.label.parseHTML"),
+ },
+ "Parse XML": {
+ group: 1,
+ colorName: "graphs-yellow",
+ label: L10N.getStr("marker.label.parseXML"),
+ },
+ GarbageCollection: {
+ group: 1,
+ colorName: "graphs-red",
+ label: Formatters.GCLabel,
+ fields: Formatters.GCFields,
+ },
+ MinorGC: {
+ group: 1,
+ colorName: "graphs-red",
+ label: L10N.getStr("marker.label.minorGC"),
+ fields: Formatters.MinorGCFields,
+ },
+ "nsCycleCollector::Collect": {
+ group: 1,
+ colorName: "graphs-red",
+ label: L10N.getStr("marker.label.cycleCollection"),
+ fields: Formatters.CycleCollectionFields,
+ },
+ "nsCycleCollector::ForgetSkippable": {
+ group: 1,
+ colorName: "graphs-red",
+ label: L10N.getStr("marker.label.cycleCollection.forgetSkippable"),
+ fields: Formatters.CycleCollectionFields,
+ },
+ Worker: {
+ group: 1,
+ colorName: "graphs-orange",
+ label: L10N.getStr("marker.label.worker"),
+ fields: Formatters.WorkerFields,
+ },
+ MessagePort: {
+ group: 1,
+ colorName: "graphs-orange",
+ label: L10N.getStr("marker.label.messagePort"),
+ fields: Formatters.MessagePortFields,
+ },
+
+ /* Group 2 - User Controlled */
+
+ ConsoleTime: {
+ group: 2,
+ colorName: "graphs-blue",
+ label: Formatters.labelForProperty(
+ L10N.getStr("marker.label.consoleTime"),
+ "causeName"
+ ),
+ fields: Formatters.ConsoleTimeFields,
+ nestable: false,
+ collapsible: false,
+ },
+ TimeStamp: {
+ group: 2,
+ colorName: "graphs-blue",
+ label: Formatters.labelForProperty(
+ L10N.getStr("marker.label.timestamp"),
+ "causeName"
+ ),
+ fields: Formatters.TimeStampFields,
+ collapsible: false,
+ },
+};
+
+// Exported symbols.
+exports.TIMELINE_BLUEPRINT = TIMELINE_BLUEPRINT;
diff --git a/devtools/client/performance/modules/moz.build b/devtools/client/performance/modules/moz.build
new file mode 100644
index 0000000000..c054669022
--- /dev/null
+++ b/devtools/client/performance/modules/moz.build
@@ -0,0 +1,22 @@
+# 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/.
+
+DIRS += [
+ "logic",
+ "widgets",
+]
+
+DevToolsModules(
+ "categories.js",
+ "constants.js",
+ "global.js",
+ "io.js",
+ "marker-blueprint-utils.js",
+ "marker-dom-utils.js",
+ "marker-formatters.js",
+ "markers.js",
+ "utils.js",
+ "waterfall-ticks.js",
+)
diff --git a/devtools/client/performance/modules/utils.js b/devtools/client/performance/modules/utils.js
new file mode 100644
index 0000000000..ebd0ba2ccb
--- /dev/null
+++ b/devtools/client/performance/modules/utils.js
@@ -0,0 +1,24 @@
+/* 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";
+
+/* globals document */
+
+/**
+ * React components grab the namespace of the element they are mounting to. This function
+ * takes a XUL element, and makes sure to create a properly namespaced HTML element to
+ * avoid React creating XUL elements.
+ *
+ * {XULElement} xulElement
+ * return {HTMLElement} div
+ */
+
+exports.createHtmlMount = function(xulElement) {
+ const htmlElement = document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "div"
+ );
+ xulElement.appendChild(htmlElement);
+ return htmlElement;
+};
diff --git a/devtools/client/performance/modules/waterfall-ticks.js b/devtools/client/performance/modules/waterfall-ticks.js
new file mode 100644
index 0000000000..efc88001f3
--- /dev/null
+++ b/devtools/client/performance/modules/waterfall-ticks.js
@@ -0,0 +1,98 @@
+/* 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 HTML_NS = "http://www.w3.org/1999/xhtml";
+
+const WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5; // ms
+const WATERFALL_BACKGROUND_TICKS_SCALES = 3;
+const WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10; // px
+const WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144];
+const WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32; // byte
+const WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32; // byte
+
+const FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS = 100;
+
+/**
+ * Creates the background displayed on the marker's waterfall.
+ */
+function drawWaterfallBackground(doc, dataScale, waterfallWidth) {
+ const canvas = doc.createElementNS(HTML_NS, "canvas");
+ const ctx = canvas.getContext("2d");
+
+ // Nuke the context.
+ const canvasWidth = (canvas.width = Math.max(waterfallWidth, 1));
+ // Awww yeah, 1px, repeats on Y axis.
+ const canvasHeight = (canvas.height = 1);
+
+ // Start over.
+ const imageData = ctx.createImageData(canvasWidth, canvasHeight);
+ const pixelArray = imageData.data;
+
+ const buf = new ArrayBuffer(pixelArray.length);
+ const view8bit = new Uint8ClampedArray(buf);
+ const view32bit = new Uint32Array(buf);
+
+ // Build new millisecond tick lines...
+ const [r, g, b] = WATERFALL_BACKGROUND_TICKS_COLOR_RGB;
+ let alphaComponent = WATERFALL_BACKGROUND_TICKS_OPACITY_MIN;
+ const tickInterval = findOptimalTickInterval({
+ ticksMultiple: WATERFALL_BACKGROUND_TICKS_MULTIPLE,
+ ticksSpacingMin: WATERFALL_BACKGROUND_TICKS_SPACING_MIN,
+ dataScale: dataScale,
+ });
+
+ // Insert one pixel for each division on each scale.
+ for (let i = 1; i <= WATERFALL_BACKGROUND_TICKS_SCALES; i++) {
+ const increment = tickInterval * Math.pow(2, i);
+ for (let x = 0; x < canvasWidth; x += increment) {
+ const position = x | 0;
+ view32bit[position] = (alphaComponent << 24) | (b << 16) | (g << 8) | r;
+ }
+ alphaComponent += WATERFALL_BACKGROUND_TICKS_OPACITY_ADD;
+ }
+
+ // Flush the image data and cache the waterfall background.
+ pixelArray.set(view8bit);
+ ctx.putImageData(imageData, 0, 0);
+ doc.mozSetImageElement("waterfall-background", canvas);
+
+ return canvas;
+}
+
+/**
+ * Finds the optimal tick interval between time markers in this timeline.
+ *
+ * @param number ticksMultiple
+ * @param number ticksSpacingMin
+ * @param number dataScale
+ * @return number
+ */
+function findOptimalTickInterval({
+ ticksMultiple,
+ ticksSpacingMin,
+ dataScale,
+}) {
+ let timingStep = ticksMultiple;
+ const maxIters = FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS;
+ let numIters = 0;
+
+ if (dataScale > ticksSpacingMin) {
+ return dataScale;
+ }
+
+ while (true) {
+ const scaledStep = dataScale * timingStep;
+ if (++numIters > maxIters) {
+ return scaledStep;
+ }
+ if (scaledStep < ticksSpacingMin) {
+ timingStep <<= 1;
+ continue;
+ }
+ return scaledStep;
+ }
+}
+
+exports.TickUtils = { findOptimalTickInterval, drawWaterfallBackground };
diff --git a/devtools/client/performance/modules/widgets/graphs.js b/devtools/client/performance/modules/widgets/graphs.js
new file mode 100644
index 0000000000..926fb43cf6
--- /dev/null
+++ b/devtools/client/performance/modules/widgets/graphs.js
@@ -0,0 +1,527 @@
+/* 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 base line graph that all Performance line graphs use.
+ */
+
+const { extend } = require("devtools/shared/extend");
+const LineGraphWidget = require("devtools/client/shared/widgets/LineGraphWidget");
+const MountainGraphWidget = require("devtools/client/shared/widgets/MountainGraphWidget");
+const { CanvasGraphUtils } = require("devtools/client/shared/widgets/Graphs");
+
+const EventEmitter = require("devtools/shared/event-emitter");
+
+const { colorUtils } = require("devtools/shared/css/color");
+const { getColor } = require("devtools/client/shared/theme");
+const ProfilerGlobal = require("devtools/client/performance/modules/global");
+const {
+ MarkersOverview,
+} = require("devtools/client/performance/modules/widgets/markers-overview");
+const {
+ createTierGraphDataFromFrameNode,
+} = require("devtools/client/performance/modules/logic/jit");
+
+/**
+ * For line graphs
+ */
+const HEIGHT = 35; // px
+const STROKE_WIDTH = 1; // px
+const DAMPEN_VALUES = 0.95;
+const CLIPHEAD_LINE_COLOR = "#666";
+const SELECTION_LINE_COLOR = "#555";
+const SELECTION_BACKGROUND_COLOR_NAME = "graphs-blue";
+const FRAMERATE_GRAPH_COLOR_NAME = "graphs-green";
+const MEMORY_GRAPH_COLOR_NAME = "graphs-blue";
+
+/**
+ * For timeline overview
+ */
+const MARKERS_GRAPH_HEADER_HEIGHT = 14; // px
+const MARKERS_GRAPH_ROW_HEIGHT = 10; // px
+const MARKERS_GROUP_VERTICAL_PADDING = 4; // px
+
+/**
+ * For optimization graph
+ */
+const OPTIMIZATIONS_GRAPH_RESOLUTION = 100;
+
+/**
+ * A base class for performance graphs to inherit from.
+ *
+ * @param Node parent
+ * The parent node holding the overview.
+ * @param string metric
+ * The unit of measurement for this graph.
+ */
+function PerformanceGraph(parent, metric) {
+ LineGraphWidget.call(this, parent, { metric });
+ this.setTheme();
+}
+
+PerformanceGraph.prototype = extend(LineGraphWidget.prototype, {
+ strokeWidth: STROKE_WIDTH,
+ dampenValuesFactor: DAMPEN_VALUES,
+ fixedHeight: HEIGHT,
+ clipheadLineColor: CLIPHEAD_LINE_COLOR,
+ selectionLineColor: SELECTION_LINE_COLOR,
+ withTooltipArrows: false,
+ withFixedTooltipPositions: true,
+
+ /**
+ * Disables selection and empties this graph.
+ */
+ clearView: function() {
+ this.selectionEnabled = false;
+ this.dropSelection();
+ this.setData([]);
+ },
+
+ /**
+ * Sets the theme via `theme` to either "light" or "dark",
+ * and updates the internal styling to match. Requires a redraw
+ * to see the effects.
+ */
+ setTheme: function(theme) {
+ theme = theme || "light";
+ const mainColor = getColor(this.mainColor || "graphs-blue", theme);
+ this.backgroundColor = getColor("body-background", theme);
+ this.strokeColor = mainColor;
+ this.backgroundGradientStart = colorUtils.setAlpha(mainColor, 0.2);
+ this.backgroundGradientEnd = colorUtils.setAlpha(mainColor, 0.2);
+ this.selectionBackgroundColor = colorUtils.setAlpha(
+ getColor(SELECTION_BACKGROUND_COLOR_NAME, theme),
+ 0.25
+ );
+ this.selectionStripesColor = "rgba(255, 255, 255, 0.1)";
+ this.maximumLineColor = colorUtils.setAlpha(mainColor, 0.4);
+ this.averageLineColor = colorUtils.setAlpha(mainColor, 0.7);
+ this.minimumLineColor = colorUtils.setAlpha(mainColor, 0.9);
+ },
+});
+
+/**
+ * Constructor for the framerate graph. Inherits from PerformanceGraph.
+ *
+ * @param Node parent
+ * The parent node holding the overview.
+ */
+function FramerateGraph(parent) {
+ PerformanceGraph.call(this, parent, ProfilerGlobal.L10N.getStr("graphs.fps"));
+}
+
+FramerateGraph.prototype = extend(PerformanceGraph.prototype, {
+ mainColor: FRAMERATE_GRAPH_COLOR_NAME,
+ setPerformanceData: function({ duration, ticks }, resolution) {
+ this.dataDuration = duration;
+ return this.setDataFromTimestamps(ticks, resolution, duration);
+ },
+});
+
+/**
+ * Constructor for the memory graph. Inherits from PerformanceGraph.
+ *
+ * @param Node parent
+ * The parent node holding the overview.
+ */
+function MemoryGraph(parent) {
+ PerformanceGraph.call(
+ this,
+ parent,
+ ProfilerGlobal.L10N.getStr("graphs.memory")
+ );
+}
+
+MemoryGraph.prototype = extend(PerformanceGraph.prototype, {
+ mainColor: MEMORY_GRAPH_COLOR_NAME,
+ setPerformanceData: function({ duration, memory }) {
+ this.dataDuration = duration;
+ return this.setData(memory);
+ },
+});
+
+function TimelineGraph(parent, filter) {
+ MarkersOverview.call(this, parent, filter);
+}
+
+TimelineGraph.prototype = extend(MarkersOverview.prototype, {
+ headerHeight: MARKERS_GRAPH_HEADER_HEIGHT,
+ rowHeight: MARKERS_GRAPH_ROW_HEIGHT,
+ groupPadding: MARKERS_GROUP_VERTICAL_PADDING,
+ setPerformanceData: MarkersOverview.prototype.setData,
+});
+
+/**
+ * Definitions file for GraphsController, indicating the constructor,
+ * selector and other meta for each of the graphs controller by
+ * GraphsController.
+ */
+const GRAPH_DEFINITIONS = {
+ memory: {
+ constructor: MemoryGraph,
+ selector: "#memory-overview",
+ },
+ framerate: {
+ constructor: FramerateGraph,
+ selector: "#time-framerate",
+ },
+ timeline: {
+ constructor: TimelineGraph,
+ selector: "#markers-overview",
+ primaryLink: true,
+ },
+};
+
+/**
+ * A controller for orchestrating the performance's tool overview graphs. Constructs,
+ * syncs, toggles displays and defines the memory, framerate and timeline view.
+ *
+ * @param {object} definition
+ * @param {DOMElement} root
+ * @param {function} getFilter
+ * @param {function} getTheme
+ */
+function GraphsController({ definition, root, getFilter, getTheme }) {
+ this._graphs = {};
+ this._enabled = new Set();
+ this._definition = definition || GRAPH_DEFINITIONS;
+ this._root = root;
+ this._getFilter = getFilter;
+ this._getTheme = getTheme;
+ this._primaryLink = Object.keys(this._definition).filter(
+ name => this._definition[name].primaryLink
+ )[0];
+ this.$ = root.ownerDocument.querySelector.bind(root.ownerDocument);
+
+ EventEmitter.decorate(this);
+ this._onSelecting = this._onSelecting.bind(this);
+}
+
+GraphsController.prototype = {
+ /**
+ * Returns the corresponding graph by `graphName`.
+ */
+ get: function(graphName) {
+ return this._graphs[graphName];
+ },
+
+ /**
+ * Iterates through all graphs and renders the data
+ * from a RecordingModel. Takes a resolution value used in
+ * some graphs.
+ * Saves rendering progress as a promise to be consumed by `destroy`,
+ * to wait for cleaning up rendering during destruction.
+ */
+ async render(recordingData, resolution) {
+ // Get the previous render promise so we don't start rendering
+ // until the previous render cycle completes, which can occur
+ // especially when a recording is finished, and triggers a
+ // fresh rendering at a higher rate
+ await this._rendering;
+
+ // Check after yielding to ensure we're not tearing down,
+ // as this can create a race condition in tests
+ if (this._destroyed) {
+ return;
+ }
+
+ this._rendering = (async () => {
+ for (const graph of await this._getEnabled()) {
+ await graph.setPerformanceData(recordingData, resolution);
+ this.emit("rendered", graph.graphName);
+ }
+ })();
+ await this._rendering;
+ },
+
+ /**
+ * Destroys the underlying graphs.
+ */
+ async destroy() {
+ const primary = this._getPrimaryLink();
+
+ this._destroyed = true;
+
+ if (primary) {
+ primary.off("selecting", this._onSelecting);
+ }
+
+ // If there was rendering, wait until the most recent render cycle
+ // has finished
+ if (this._rendering) {
+ await this._rendering;
+ }
+
+ for (const graph of this.getWidgets()) {
+ await graph.destroy();
+ }
+ },
+
+ /**
+ * Applies the theme to the underlying graphs. Optionally takes
+ * a `redraw` boolean in the options to force redraw.
+ */
+ setTheme: function(options = {}) {
+ const theme = options.theme || this._getTheme();
+ for (const graph of this.getWidgets()) {
+ graph.setTheme(theme);
+ graph.refresh({ force: options.redraw });
+ }
+ },
+
+ /**
+ * Sets up the graph, if needed. Returns a promise resolving
+ * to the graph if it is enabled once it's ready, or otherwise returns
+ * null if disabled.
+ */
+ async isAvailable(graphName) {
+ if (!this._enabled.has(graphName)) {
+ return null;
+ }
+
+ let graph = this.get(graphName);
+
+ if (!graph) {
+ graph = await this._construct(graphName);
+ }
+
+ await graph.ready();
+ return graph;
+ },
+
+ /**
+ * Enable or disable a subgraph controlled by GraphsController.
+ * This determines what graphs are visible and get rendered.
+ */
+ enable: function(graphName, isEnabled) {
+ const el = this.$(this._definition[graphName].selector);
+ el.classList[isEnabled ? "remove" : "add"]("hidden");
+
+ // If no status change, just return
+ if (this._enabled.has(graphName) === isEnabled) {
+ return;
+ }
+ if (isEnabled) {
+ this._enabled.add(graphName);
+ } else {
+ this._enabled.delete(graphName);
+ }
+
+ // Invalidate our cache of ready-to-go graphs
+ this._enabledGraphs = null;
+ },
+
+ /**
+ * Disables all graphs controller by the GraphsController, and
+ * also hides the root element. This is a one way switch, and used
+ * when older platforms do not have any timeline data.
+ */
+ disableAll: function() {
+ this._root.classList.add("hidden");
+ // Hide all the subelements
+ Object.keys(this._definition).forEach(graphName =>
+ this.enable(graphName, false)
+ );
+ },
+
+ /**
+ * Sets a mapped selection on the graph that is the main controller
+ * for keeping the graphs' selections in sync.
+ */
+ setMappedSelection: function(selection, { mapStart, mapEnd }) {
+ return this._getPrimaryLink().setMappedSelection(selection, {
+ mapStart,
+ mapEnd,
+ });
+ },
+
+ /**
+ * Fetches the currently mapped selection. If graphs are not yet rendered,
+ * (which throws in Graphs.js), return null.
+ */
+ getMappedSelection: function({ mapStart, mapEnd }) {
+ const primary = this._getPrimaryLink();
+ if (primary && primary.hasData()) {
+ return primary.getMappedSelection({ mapStart, mapEnd });
+ }
+ return null;
+ },
+
+ /**
+ * Returns an array of graphs that have been created, not necessarily
+ * enabled currently.
+ */
+ getWidgets: function() {
+ return Object.keys(this._graphs).map(name => this._graphs[name]);
+ },
+
+ /**
+ * Drops the selection.
+ */
+ dropSelection: function() {
+ if (this._getPrimaryLink()) {
+ return this._getPrimaryLink().dropSelection();
+ }
+ return null;
+ },
+
+ /**
+ * Makes sure the selection is enabled or disabled in all the graphs.
+ */
+ async selectionEnabled(enabled) {
+ for (const graph of await this._getEnabled()) {
+ graph.selectionEnabled = enabled;
+ }
+ },
+
+ /**
+ * Creates the graph `graphName` and initializes it.
+ */
+ async _construct(graphName) {
+ const def = this._definition[graphName];
+ const el = this.$(def.selector);
+ const filter = this._getFilter();
+ const graph = (this._graphs[graphName] = new def.constructor(el, filter));
+ graph.graphName = graphName;
+
+ await graph.ready();
+
+ // Sync the graphs' animations and selections together
+ if (def.primaryLink) {
+ graph.on("selecting", this._onSelecting);
+ } else {
+ CanvasGraphUtils.linkAnimation(this._getPrimaryLink(), graph);
+ CanvasGraphUtils.linkSelection(this._getPrimaryLink(), graph);
+ }
+
+ // Sets the container element's visibility based off of enabled status
+ el.classList[this._enabled.has(graphName) ? "remove" : "add"]("hidden");
+
+ this.setTheme();
+ return graph;
+ },
+
+ /**
+ * Returns the main graph for this collection, that all graphs
+ * are bound to for syncing and selection.
+ */
+ _getPrimaryLink: function() {
+ return this.get(this._primaryLink);
+ },
+
+ /**
+ * Emitted when a selection occurs.
+ */
+ _onSelecting: function() {
+ this.emit("selecting");
+ },
+
+ /**
+ * Resolves to an array with all graphs that are enabled, and
+ * creates them if needed. Different than just iterating over `this._graphs`,
+ * as those could be enabled. Uses caching, as rendering happens many times per second,
+ * compared to how often which graphs/features are changed (rarely).
+ */
+ async _getEnabled() {
+ if (this._enabledGraphs) {
+ return this._enabledGraphs;
+ }
+ const enabled = [];
+ for (const graphName of this._enabled) {
+ const graph = await this.isAvailable(graphName);
+ if (graph) {
+ enabled.push(graph);
+ }
+ }
+ this._enabledGraphs = enabled;
+ return this._enabledGraphs;
+ },
+};
+
+/**
+ * A base class for performance graphs to inherit from.
+ *
+ * @param Node parent
+ * The parent node holding the overview.
+ * @param string metric
+ * The unit of measurement for this graph.
+ */
+function OptimizationsGraph(parent) {
+ MountainGraphWidget.call(this, parent);
+ this.setTheme();
+}
+
+OptimizationsGraph.prototype = extend(MountainGraphWidget.prototype, {
+ async render(threadNode, frameNode) {
+ // Regardless if we draw or clear the graph, wait
+ // until it's ready.
+ await this.ready();
+
+ if (!threadNode || !frameNode) {
+ this.setData([]);
+ return;
+ }
+
+ const { sampleTimes } = threadNode;
+
+ if (!sampleTimes.length) {
+ this.setData([]);
+ return;
+ }
+
+ // Take startTime/endTime from samples recorded, rather than
+ // using duration directly from threadNode, as the first sample that
+ // equals the startTime does not get recorded.
+ const startTime = sampleTimes[0];
+ const endTime = sampleTimes[sampleTimes.length - 1];
+
+ const bucketSize = (endTime - startTime) / OPTIMIZATIONS_GRAPH_RESOLUTION;
+ const data = createTierGraphDataFromFrameNode(
+ frameNode,
+ sampleTimes,
+ bucketSize
+ );
+
+ // If for some reason we don't have data (like the frameNode doesn't
+ // have optimizations, but it shouldn't be at this point if it doesn't),
+ // log an error.
+ if (!data) {
+ console.error(
+ `FrameNode#${frameNode.location} does not have optimizations data to render.`
+ );
+ return;
+ }
+
+ this.dataOffsetX = startTime;
+ await this.setData(data);
+ },
+
+ /**
+ * Sets the theme via `theme` to either "light" or "dark",
+ * and updates the internal styling to match. Requires a redraw
+ * to see the effects.
+ */
+ setTheme: function(theme) {
+ theme = theme || "light";
+
+ const interpreterColor = getColor("graphs-red", theme);
+ const baselineColor = getColor("graphs-blue", theme);
+ const ionColor = getColor("graphs-green", theme);
+
+ this.format = [
+ { color: interpreterColor },
+ { color: baselineColor },
+ { color: ionColor },
+ ];
+
+ this.backgroundColor = getColor("sidebar-background", theme);
+ },
+});
+
+exports.OptimizationsGraph = OptimizationsGraph;
+exports.FramerateGraph = FramerateGraph;
+exports.MemoryGraph = MemoryGraph;
+exports.TimelineGraph = TimelineGraph;
+exports.GraphsController = GraphsController;
diff --git a/devtools/client/performance/modules/widgets/marker-details.js b/devtools/client/performance/modules/widgets/marker-details.js
new file mode 100644
index 0000000000..2a8ce17ce4
--- /dev/null
+++ b/devtools/client/performance/modules/widgets/marker-details.js
@@ -0,0 +1,177 @@
+/* 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 rendering code for the marker sidebar.
+ */
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const {
+ MarkerDOMUtils,
+} = require("devtools/client/performance/modules/marker-dom-utils");
+
+/**
+ * A detailed view for one single marker.
+ *
+ * @param Node parent
+ * The parent node holding the view.
+ * @param Node splitter
+ * The splitter node that the resize event is bound to.
+ */
+function MarkerDetails(parent, splitter) {
+ EventEmitter.decorate(this);
+
+ this._document = parent.ownerDocument;
+ this._parent = parent;
+ this._splitter = splitter;
+
+ this._onClick = this._onClick.bind(this);
+ this._onSplitterMouseUp = this._onSplitterMouseUp.bind(this);
+
+ this._parent.addEventListener("click", this._onClick);
+ this._splitter.addEventListener("mouseup", this._onSplitterMouseUp);
+
+ this.hidden = true;
+}
+
+MarkerDetails.prototype = {
+ /**
+ * Sets this view's width.
+ * @param number
+ */
+ set width(value) {
+ this._parent.setAttribute("width", value);
+ },
+
+ /**
+ * Sets this view's width.
+ * @return number
+ */
+ get width() {
+ return +this._parent.getAttribute("width");
+ },
+
+ /**
+ * Sets this view's visibility.
+ * @param boolean
+ */
+ set hidden(value) {
+ if (this._parent.hidden != value) {
+ this._parent.hidden = value;
+ this.emit("resize");
+ }
+ },
+
+ /**
+ * Gets this view's visibility.
+ * @param boolean
+ */
+ get hidden() {
+ return this._parent.hidden;
+ },
+
+ /**
+ * Clears the marker details from this view.
+ */
+ empty: function() {
+ this._parent.innerHTML = "";
+ },
+
+ /**
+ * Populates view with marker's details.
+ *
+ * @param object params
+ * An options object holding:
+ * - marker: The marker to display.
+ * - frames: Array of stack frame information; see stack.js.
+ * - allocations: Whether or not allocations were enabled for this
+ * recording. [optional]
+ */
+ render: function(options) {
+ const { marker, frames } = options;
+ this.empty();
+
+ const elements = [];
+ elements.push(MarkerDOMUtils.buildTitle(this._document, marker));
+ elements.push(MarkerDOMUtils.buildDuration(this._document, marker));
+ MarkerDOMUtils.buildFields(this._document, marker).forEach(f =>
+ elements.push(f)
+ );
+ MarkerDOMUtils.buildCustom(this._document, marker, options).forEach(f =>
+ elements.push(f)
+ );
+
+ // Build a stack element -- and use the "startStack" label if
+ // we have both a startStack and endStack.
+ if (marker.stack) {
+ const type = marker.endStack ? "startStack" : "stack";
+ elements.push(
+ MarkerDOMUtils.buildStackTrace(this._document, {
+ frameIndex: marker.stack,
+ frames,
+ type,
+ })
+ );
+ }
+ if (marker.endStack) {
+ const type = "endStack";
+ elements.push(
+ MarkerDOMUtils.buildStackTrace(this._document, {
+ frameIndex: marker.endStack,
+ frames,
+ type,
+ })
+ );
+ }
+
+ elements.forEach(el => this._parent.appendChild(el));
+ },
+
+ /**
+ * Handles click in the marker details view. Based on the target,
+ * can handle different actions -- only supporting view source links
+ * for the moment.
+ */
+ _onClick: function(e) {
+ const data = findActionFromEvent(e.target, this._parent);
+ if (!data) {
+ return;
+ }
+
+ this.emit(data.action, data);
+ },
+
+ /**
+ * Handles the "mouseup" event on the marker details view splitter.
+ */
+ _onSplitterMouseUp: function() {
+ this.emit("resize");
+ },
+};
+
+/**
+ * Take an element from an event `target`, and ascend through
+ * the DOM, looking for an element with a `data-action` attribute. Return
+ * the parsed `data-action` value found, or null if none found before
+ * reaching the parent `container`.
+ *
+ * @param {Element} target
+ * @param {Element} container
+ * @return {?object}
+ */
+function findActionFromEvent(target, container) {
+ let el = target;
+ let action;
+ while (el !== container) {
+ action = el.getAttribute("data-action");
+ if (action) {
+ return JSON.parse(action);
+ }
+ el = el.parentNode;
+ }
+ return null;
+}
+
+exports.MarkerDetails = MarkerDetails;
diff --git a/devtools/client/performance/modules/widgets/markers-overview.js b/devtools/client/performance/modules/widgets/markers-overview.js
new file mode 100644
index 0000000000..ea762a371d
--- /dev/null
+++ b/devtools/client/performance/modules/widgets/markers-overview.js
@@ -0,0 +1,256 @@
+/* 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 "markers overview" graph, which is a minimap of all
+ * the timeline data. Regions inside it may be selected, determining which
+ * markers are visible in the "waterfall".
+ */
+
+const { extend } = require("devtools/shared/extend");
+const {
+ AbstractCanvasGraph,
+} = require("devtools/client/shared/widgets/Graphs");
+
+const { colorUtils } = require("devtools/shared/css/color");
+const { getColor } = require("devtools/client/shared/theme");
+const ProfilerGlobal = require("devtools/client/performance/modules/global");
+const {
+ MarkerBlueprintUtils,
+} = require("devtools/client/performance/modules/marker-blueprint-utils");
+const {
+ TickUtils,
+} = require("devtools/client/performance/modules/waterfall-ticks");
+const {
+ TIMELINE_BLUEPRINT,
+} = require("devtools/client/performance/modules/markers");
+
+const OVERVIEW_HEADER_HEIGHT = 14; // px
+const OVERVIEW_ROW_HEIGHT = 11; // px
+
+const OVERVIEW_SELECTION_LINE_COLOR = "#666";
+const OVERVIEW_CLIPHEAD_LINE_COLOR = "#555";
+
+const OVERVIEW_HEADER_TICKS_MULTIPLE = 100; // ms
+const OVERVIEW_HEADER_TICKS_SPACING_MIN = 75; // px
+const OVERVIEW_HEADER_TEXT_FONT_SIZE = 9; // px
+const OVERVIEW_HEADER_TEXT_FONT_FAMILY = "sans-serif";
+const OVERVIEW_HEADER_TEXT_PADDING_LEFT = 6; // px
+const OVERVIEW_HEADER_TEXT_PADDING_TOP = 1; // px
+const OVERVIEW_MARKER_WIDTH_MIN = 4; // px
+const OVERVIEW_GROUP_VERTICAL_PADDING = 5; // px
+
+/**
+ * An overview for the markers data.
+ *
+ * @param Node parent
+ * The parent node holding the overview.
+ * @param Array<String> filter
+ * List of names of marker types that should not be shown.
+ */
+function MarkersOverview(parent, filter = [], ...args) {
+ AbstractCanvasGraph.apply(this, [parent, "markers-overview", ...args]);
+ this.setTheme();
+ this.setFilter(filter);
+}
+
+MarkersOverview.prototype = extend(AbstractCanvasGraph.prototype, {
+ clipheadLineColor: OVERVIEW_CLIPHEAD_LINE_COLOR,
+ selectionLineColor: OVERVIEW_SELECTION_LINE_COLOR,
+ headerHeight: OVERVIEW_HEADER_HEIGHT,
+ rowHeight: OVERVIEW_ROW_HEIGHT,
+ groupPadding: OVERVIEW_GROUP_VERTICAL_PADDING,
+
+ /**
+ * Compute the height of the overview.
+ */
+ get fixedHeight() {
+ return this.headerHeight + this.rowHeight * this._numberOfGroups;
+ },
+
+ /**
+ * List of marker types that should not be shown in the graph.
+ */
+ setFilter: function(filter) {
+ this._paintBatches = new Map();
+ this._filter = filter;
+ this._groupMap = Object.create(null);
+
+ const observedGroups = new Set();
+
+ for (const type in TIMELINE_BLUEPRINT) {
+ if (filter.includes(type)) {
+ continue;
+ }
+ this._paintBatches.set(type, {
+ definition: TIMELINE_BLUEPRINT[type],
+ batch: [],
+ });
+ observedGroups.add(TIMELINE_BLUEPRINT[type].group);
+ }
+
+ // Take our set of observed groups and order them and map
+ // the group numbers to fill in the holes via `_groupMap`.
+ // This normalizes our rows by removing rows that aren't used
+ // if filters are enabled.
+ let actualPosition = 0;
+ for (const groupNumber of Array.from(observedGroups).sort()) {
+ this._groupMap[groupNumber] = actualPosition++;
+ }
+ this._numberOfGroups = Object.keys(this._groupMap).length;
+ },
+
+ /**
+ * Disables selection and empties this graph.
+ */
+ clearView: function() {
+ this.selectionEnabled = false;
+ this.dropSelection();
+ this.setData({ duration: 0, markers: [] });
+ },
+
+ /**
+ * Renders the graph's data source.
+ * @see AbstractCanvasGraph.prototype.buildGraphImage
+ */
+ buildGraphImage: function() {
+ const { markers, duration } = this._data;
+
+ const { canvas, ctx } = this._getNamedCanvas("markers-overview-data");
+ const canvasWidth = this._width;
+ const canvasHeight = this._height;
+
+ // Group markers into separate paint batches. This is necessary to
+ // draw all markers sharing the same style at once.
+ for (const marker of markers) {
+ // Again skip over markers that we're filtering -- we don't want them
+ // to be labeled as "Unknown"
+ if (!MarkerBlueprintUtils.shouldDisplayMarker(marker, this._filter)) {
+ continue;
+ }
+
+ const markerType =
+ this._paintBatches.get(marker.name) ||
+ this._paintBatches.get("UNKNOWN");
+ markerType.batch.push(marker);
+ }
+
+ // Calculate each row's height, and the time-based scaling.
+
+ const groupHeight = this.rowHeight * this._pixelRatio;
+ const groupPadding = this.groupPadding * this._pixelRatio;
+ const headerHeight = this.headerHeight * this._pixelRatio;
+ const dataScale = (this.dataScaleX = canvasWidth / duration);
+
+ // Draw the header and overview background.
+
+ ctx.fillStyle = this.headerBackgroundColor;
+ ctx.fillRect(0, 0, canvasWidth, headerHeight);
+
+ ctx.fillStyle = this.backgroundColor;
+ ctx.fillRect(0, headerHeight, canvasWidth, canvasHeight);
+
+ // Draw the alternating odd/even group backgrounds.
+
+ ctx.fillStyle = this.alternatingBackgroundColor;
+ ctx.beginPath();
+
+ for (let i = 0; i < this._numberOfGroups; i += 2) {
+ const top = headerHeight + i * groupHeight;
+ ctx.rect(0, top, canvasWidth, groupHeight);
+ }
+
+ ctx.fill();
+
+ // Draw the timeline header ticks.
+
+ const fontSize = OVERVIEW_HEADER_TEXT_FONT_SIZE * this._pixelRatio;
+ const fontFamily = OVERVIEW_HEADER_TEXT_FONT_FAMILY;
+ const textPaddingLeft =
+ OVERVIEW_HEADER_TEXT_PADDING_LEFT * this._pixelRatio;
+ const textPaddingTop = OVERVIEW_HEADER_TEXT_PADDING_TOP * this._pixelRatio;
+
+ const tickInterval = TickUtils.findOptimalTickInterval({
+ ticksMultiple: OVERVIEW_HEADER_TICKS_MULTIPLE,
+ ticksSpacingMin: OVERVIEW_HEADER_TICKS_SPACING_MIN,
+ dataScale: dataScale,
+ });
+
+ ctx.textBaseline = "middle";
+ ctx.font = fontSize + "px " + fontFamily;
+ ctx.fillStyle = this.headerTextColor;
+ ctx.strokeStyle = this.headerTimelineStrokeColor;
+ ctx.beginPath();
+
+ for (let x = 0; x < canvasWidth; x += tickInterval) {
+ const lineLeft = x;
+ const textLeft = lineLeft + textPaddingLeft;
+ const time = Math.round(x / dataScale);
+ const label = ProfilerGlobal.L10N.getFormatStr("timeline.tick", time);
+ ctx.fillText(label, textLeft, headerHeight / 2 + textPaddingTop);
+ ctx.moveTo(lineLeft, 0);
+ ctx.lineTo(lineLeft, canvasHeight);
+ }
+
+ ctx.stroke();
+
+ // Draw the timeline markers.
+
+ for (const [, { definition, batch }] of this._paintBatches) {
+ const group = this._groupMap[definition.group];
+ const top = headerHeight + group * groupHeight + groupPadding / 2;
+ const height = groupHeight - groupPadding;
+
+ const color = getColor(definition.colorName, this.theme);
+ ctx.fillStyle = color;
+ ctx.beginPath();
+
+ for (const { start, end } of batch) {
+ const left = start * dataScale;
+ const width = Math.max(
+ (end - start) * dataScale,
+ OVERVIEW_MARKER_WIDTH_MIN
+ );
+ ctx.rect(left, top, width, height);
+ }
+
+ ctx.fill();
+
+ // Since all the markers in this batch (thus sharing the same style) have
+ // been drawn, empty it. The next time new markers will be available,
+ // they will be sorted and drawn again.
+ batch.length = 0;
+ }
+
+ return canvas;
+ },
+
+ /**
+ * Sets the theme via `theme` to either "light" or "dark",
+ * and updates the internal styling to match. Requires a redraw
+ * to see the effects.
+ */
+ setTheme: function(theme) {
+ this.theme = theme = theme || "light";
+ this.backgroundColor = getColor("body-background", theme);
+ this.selectionBackgroundColor = colorUtils.setAlpha(
+ getColor("selection-background", theme),
+ 0.25
+ );
+ this.selectionStripesColor = colorUtils.setAlpha("#fff", 0.1);
+ this.headerBackgroundColor = getColor("body-background", theme);
+ this.headerTextColor = getColor("body-color", theme);
+ this.headerTimelineStrokeColor = colorUtils.setAlpha(
+ getColor("text-color-alt", theme),
+ 0.25
+ );
+ this.alternatingBackgroundColor = colorUtils.setAlpha(
+ getColor("body-color", theme),
+ 0.05
+ );
+ },
+});
+
+exports.MarkersOverview = MarkersOverview;
diff --git a/devtools/client/performance/modules/widgets/moz.build b/devtools/client/performance/modules/widgets/moz.build
new file mode 100644
index 0000000000..d04890425c
--- /dev/null
+++ b/devtools/client/performance/modules/widgets/moz.build
@@ -0,0 +1,11 @@
+# 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(
+ "graphs.js",
+ "marker-details.js",
+ "markers-overview.js",
+ "tree-view.js",
+)
diff --git a/devtools/client/performance/modules/widgets/tree-view.js b/devtools/client/performance/modules/widgets/tree-view.js
new file mode 100644
index 0000000000..79ad8229ff
--- /dev/null
+++ b/devtools/client/performance/modules/widgets/tree-view.js
@@ -0,0 +1,461 @@
+/* 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 tree view, displaying all the samples and frames
+ * received from the proviler in a tree-like structure.
+ */
+
+const { L10N } = require("devtools/client/performance/modules/global");
+const { extend } = require("devtools/shared/extend");
+const {
+ AbstractTreeItem,
+} = require("resource://devtools/client/shared/widgets/AbstractTreeItem.jsm");
+
+const URL_LABEL_TOOLTIP = L10N.getStr("table.url.tooltiptext");
+const VIEW_OPTIMIZATIONS_TOOLTIP = L10N.getStr(
+ "table.view-optimizations.tooltiptext2"
+);
+
+const CALL_TREE_INDENTATION = 16; // px
+
+// Used for rendering values in cells
+const FORMATTERS = {
+ TIME: value =>
+ L10N.getFormatStr("table.ms2", L10N.numberWithDecimals(value, 2)),
+ PERCENT: value =>
+ L10N.getFormatStr("table.percentage3", L10N.numberWithDecimals(value, 2)),
+ NUMBER: value => value || 0,
+ BYTESIZE: value => L10N.getFormatStr("table.bytes", value || 0),
+};
+
+/**
+ * Definitions for rendering cells. Triads of class name, property name from
+ * `frame.getInfo()`, and a formatter function.
+ */
+const CELLS = {
+ duration: ["duration", "totalDuration", FORMATTERS.TIME],
+ percentage: ["percentage", "totalPercentage", FORMATTERS.PERCENT],
+ selfDuration: ["self-duration", "selfDuration", FORMATTERS.TIME],
+ selfPercentage: ["self-percentage", "selfPercentage", FORMATTERS.PERCENT],
+ samples: ["samples", "samples", FORMATTERS.NUMBER],
+
+ selfSize: ["self-size", "selfSize", FORMATTERS.BYTESIZE],
+ selfSizePercentage: [
+ "self-size-percentage",
+ "selfSizePercentage",
+ FORMATTERS.PERCENT,
+ ],
+ selfCount: ["self-count", "selfCount", FORMATTERS.NUMBER],
+ selfCountPercentage: [
+ "self-count-percentage",
+ "selfCountPercentage",
+ FORMATTERS.PERCENT,
+ ],
+ size: ["size", "totalSize", FORMATTERS.BYTESIZE],
+ sizePercentage: [
+ "size-percentage",
+ "totalSizePercentage",
+ FORMATTERS.PERCENT,
+ ],
+ count: ["count", "totalCount", FORMATTERS.NUMBER],
+ countPercentage: [
+ "count-percentage",
+ "totalCountPercentage",
+ FORMATTERS.PERCENT,
+ ],
+};
+const CELL_TYPES = Object.keys(CELLS);
+
+const DEFAULT_SORTING_PREDICATE = (frameA, frameB) => {
+ const dataA = frameA.getDisplayedData();
+ const dataB = frameB.getDisplayedData();
+ const isAllocations = "totalSize" in dataA;
+
+ if (isAllocations) {
+ if (this.inverted && dataA.selfSize !== dataB.selfSize) {
+ return dataA.selfSize < dataB.selfSize ? 1 : -1;
+ }
+ return dataA.totalSize < dataB.totalSize ? 1 : -1;
+ }
+
+ if (this.inverted && dataA.selfPercentage !== dataB.selfPercentage) {
+ return dataA.selfPercentage < dataB.selfPercentage ? 1 : -1;
+ }
+ return dataA.totalPercentage < dataB.totalPercentage ? 1 : -1;
+};
+
+// depth
+const DEFAULT_AUTO_EXPAND_DEPTH = 3;
+const DEFAULT_VISIBLE_CELLS = {
+ duration: true,
+ percentage: true,
+ selfDuration: true,
+ selfPercentage: true,
+ samples: true,
+ function: true,
+
+ // allocation columns
+ count: false,
+ selfCount: false,
+ size: false,
+ selfSize: false,
+ countPercentage: false,
+ selfCountPercentage: false,
+ sizePercentage: false,
+ selfSizePercentage: false,
+};
+
+/**
+ * An item in a call tree view, which looks like this:
+ *
+ * Time (ms) | Cost | Calls | Function
+ * ============================================================================
+ * 1,000.00 | 100.00% | | ▼ (root)
+ * 500.12 | 50.01% | 300 | ▼ foo Categ. 1
+ * 300.34 | 30.03% | 1500 | ▼ bar Categ. 2
+ * 10.56 | 0.01% | 42 | ▶ call_with_children Categ. 3
+ * 90.78 | 0.09% | 25 | call_without_children Categ. 4
+ *
+ * Every instance of a `CallView` represents a row in the call tree. The same
+ * parent node is used for all rows.
+ *
+ * @param CallView caller
+ * The CallView considered the "caller" frame. This newly created
+ * instance will be represent the "callee". Should be null for root nodes.
+ * @param ThreadNode | FrameNode frame
+ * Details about this function, like { samples, duration, calls } etc.
+ * @param number level [optional]
+ * The indentation level in the call tree. The root node is at level 0.
+ * @param boolean hidden [optional]
+ * Whether this node should be hidden and not contribute to depth/level
+ * calculations. Defaults to false.
+ * @param boolean inverted [optional]
+ * Whether the call tree has been inverted (bottom up, rather than
+ * top-down). Defaults to false.
+ * @param function sortingPredicate [optional]
+ * The predicate used to sort the tree items when created. Defaults to
+ * the caller's `sortingPredicate` if a caller exists, otherwise defaults
+ * to DEFAULT_SORTING_PREDICATE. The two passed arguments are FrameNodes.
+ * @param number autoExpandDepth [optional]
+ * The depth to which the tree should automatically expand. Defualts to
+ * the caller's `autoExpandDepth` if a caller exists, otherwise defaults
+ * to DEFAULT_AUTO_EXPAND_DEPTH.
+ * @param object visibleCells
+ * An object specifying which cells are visible in the tree. Defaults to
+ * the caller's `visibleCells` if a caller exists, otherwise defaults
+ * to DEFAULT_VISIBLE_CELLS.
+ * @param boolean showOptimizationHint [optional]
+ * Whether or not to show an icon indicating if the frame has optimization
+ * data.
+ */
+function CallView({
+ caller,
+ frame,
+ level,
+ hidden,
+ inverted,
+ sortingPredicate,
+ autoExpandDepth,
+ visibleCells,
+ showOptimizationHint,
+}) {
+ AbstractTreeItem.call(this, {
+ parent: caller,
+ level: level | (0 - (hidden ? 1 : 0)),
+ });
+
+ if (sortingPredicate != null) {
+ this.sortingPredicate = sortingPredicate;
+ } else if (caller) {
+ this.sortingPredicate = caller.sortingPredicate;
+ } else {
+ this.sortingPredicate = DEFAULT_SORTING_PREDICATE;
+ }
+
+ if (autoExpandDepth != null) {
+ this.autoExpandDepth = autoExpandDepth;
+ } else if (caller) {
+ this.autoExpandDepth = caller.autoExpandDepth;
+ } else {
+ this.autoExpandDepth = DEFAULT_AUTO_EXPAND_DEPTH;
+ }
+
+ if (visibleCells != null) {
+ this.visibleCells = visibleCells;
+ } else if (caller) {
+ this.visibleCells = caller.visibleCells;
+ } else {
+ this.visibleCells = Object.create(DEFAULT_VISIBLE_CELLS);
+ }
+
+ this.caller = caller;
+ this.frame = frame;
+ this.hidden = hidden;
+ this.inverted = inverted;
+ this.showOptimizationHint = showOptimizationHint;
+
+ this._onUrlClick = this._onUrlClick.bind(this);
+}
+
+CallView.prototype = extend(AbstractTreeItem.prototype, {
+ /**
+ * Creates the view for this tree node.
+ * @param Node document
+ * @param Node arrowNode
+ * @return Node
+ */
+ _displaySelf: function(document, arrowNode) {
+ const frameInfo = this.getDisplayedData();
+ const cells = [];
+
+ for (const type of CELL_TYPES) {
+ if (this.visibleCells[type]) {
+ // Inline for speed, but pass in the formatted value via
+ // cell definition, as well as the element type.
+ cells.push(
+ this._createCell(
+ document,
+ CELLS[type][2](frameInfo[CELLS[type][1]]),
+ CELLS[type][0]
+ )
+ );
+ }
+ }
+
+ if (this.visibleCells.function) {
+ cells.push(
+ this._createFunctionCell(
+ document,
+ arrowNode,
+ frameInfo.name,
+ frameInfo,
+ this.level
+ )
+ );
+ }
+
+ const targetNode = document.createXULElement("hbox");
+ targetNode.className = "call-tree-item";
+ targetNode.setAttribute(
+ "origin",
+ frameInfo.isContent ? "content" : "chrome"
+ );
+ targetNode.setAttribute("category", frameInfo.categoryData.abbrev || "");
+ targetNode.setAttribute("tooltiptext", frameInfo.tooltiptext);
+
+ if (this.hidden) {
+ targetNode.style.display = "none";
+ }
+
+ for (let i = 0; i < cells.length; i++) {
+ targetNode.appendChild(cells[i]);
+ }
+
+ return targetNode;
+ },
+
+ /**
+ * Populates this node in the call tree with the corresponding "callees".
+ * These are defined in the `frame` data source for this call view.
+ * @param array:AbstractTreeItem children
+ */
+ _populateSelf: function(children) {
+ const newLevel = this.level + 1;
+
+ for (const newFrame of this.frame.calls) {
+ children.push(
+ new CallView({
+ caller: this,
+ frame: newFrame,
+ level: newLevel,
+ inverted: this.inverted,
+ })
+ );
+ }
+
+ // Sort the "callees" asc. by samples, before inserting them in the tree,
+ // if no other sorting predicate was specified on this on the root item.
+ children.sort(this.sortingPredicate.bind(this));
+ },
+
+ /**
+ * Functions creating each cell in this call view.
+ * Invoked by `_displaySelf`.
+ */
+ _createCell: function(doc, value, type) {
+ const cell = doc.createXULElement("description");
+ cell.className = "plain call-tree-cell";
+ cell.setAttribute("type", type);
+ cell.setAttribute("crop", "end");
+ // Add a tabulation to the cell text in case it's is selected and copied.
+ cell.textContent = value + "\t";
+ return cell;
+ },
+
+ _createFunctionCell: function(
+ doc,
+ arrowNode,
+ frameName,
+ frameInfo,
+ frameLevel
+ ) {
+ const cell = doc.createXULElement("hbox");
+ cell.className = "call-tree-cell";
+ cell.style.marginInlineStart = frameLevel * CALL_TREE_INDENTATION + "px";
+ cell.setAttribute("type", "function");
+ cell.appendChild(arrowNode);
+
+ // Render optimization hint if this frame has opt data.
+ if (
+ this.root.showOptimizationHint &&
+ frameInfo.hasOptimizations &&
+ !frameInfo.isMetaCategory
+ ) {
+ const icon = doc.createXULElement("description");
+ icon.setAttribute("tooltiptext", VIEW_OPTIMIZATIONS_TOOLTIP);
+ icon.className = "opt-icon";
+ cell.appendChild(icon);
+ }
+
+ // Don't render a name label node if there's no function name. A different
+ // location label node will be rendered instead.
+ if (frameName) {
+ const nameNode = doc.createXULElement("description");
+ nameNode.className = "plain call-tree-name";
+ nameNode.textContent = frameName;
+ cell.appendChild(nameNode);
+ }
+
+ // Don't render detailed labels for meta category frames
+ if (!frameInfo.isMetaCategory) {
+ this._appendFunctionDetailsCells(doc, cell, frameInfo);
+ }
+
+ // Don't render an expando-arrow for leaf nodes.
+ const hasDescendants = Object.keys(this.frame.calls).length > 0;
+ if (!hasDescendants) {
+ arrowNode.setAttribute("invisible", "");
+ }
+
+ // Add a line break to the last description of the row in case it's selected
+ // and copied.
+ const lastDescription = cell.querySelector("description:last-of-type");
+ lastDescription.textContent = lastDescription.textContent + "\n";
+
+ // Add spaces as frameLevel indicators in case the row is selected and
+ // copied. These spaces won't be displayed in the cell content.
+ const firstDescription = cell.querySelector("description:first-of-type");
+ const levelIndicator = frameLevel > 0 ? " ".repeat(frameLevel) : "";
+ firstDescription.textContent =
+ levelIndicator + firstDescription.textContent;
+
+ return cell;
+ },
+
+ _appendFunctionDetailsCells: function(doc, cell, frameInfo) {
+ if (frameInfo.fileName) {
+ const urlNode = doc.createXULElement("description");
+ urlNode.className = "plain call-tree-url";
+ urlNode.textContent = frameInfo.fileName;
+ urlNode.setAttribute(
+ "tooltiptext",
+ URL_LABEL_TOOLTIP + " → " + frameInfo.url
+ );
+ urlNode.addEventListener("mousedown", this._onUrlClick);
+ cell.appendChild(urlNode);
+ }
+
+ if (frameInfo.line) {
+ const lineNode = doc.createXULElement("description");
+ lineNode.className = "plain call-tree-line";
+ lineNode.textContent = ":" + frameInfo.line;
+ cell.appendChild(lineNode);
+ }
+
+ if (frameInfo.column) {
+ const columnNode = doc.createXULElement("description");
+ columnNode.className = "plain call-tree-column";
+ columnNode.textContent = ":" + frameInfo.column;
+ cell.appendChild(columnNode);
+ }
+
+ if (frameInfo.host) {
+ const hostNode = doc.createXULElement("description");
+ hostNode.className = "plain call-tree-host";
+ hostNode.textContent = frameInfo.host;
+ cell.appendChild(hostNode);
+ }
+
+ if (frameInfo.categoryData.label) {
+ const categoryNode = doc.createXULElement("description");
+ categoryNode.className = "plain call-tree-category";
+ categoryNode.style.color = frameInfo.categoryData.color;
+ categoryNode.textContent = frameInfo.categoryData.label;
+ cell.appendChild(categoryNode);
+ }
+ },
+
+ /**
+ * Gets the data displayed about this tree item, based on the FrameNode
+ * model associated with this view.
+ *
+ * @return object
+ */
+ getDisplayedData: function() {
+ if (this._cachedDisplayedData) {
+ return this._cachedDisplayedData;
+ }
+
+ this._cachedDisplayedData = this.frame.getInfo({
+ root: this.root.frame,
+ allocations: this.visibleCells.count || this.visibleCells.selfCount,
+ });
+
+ return this._cachedDisplayedData;
+
+ /**
+ * When inverting call tree, the costs and times are dependent on position
+ * in the tree. We must only count leaf nodes with self cost, and total costs
+ * dependent on how many times the leaf node was found with a full stack path.
+ *
+ * Total | Self | Calls | Function
+ * ============================================================================
+ * 100% | 100% | 100 | ▼ C
+ * 50% | 0% | 50 | ▼ B
+ * 50% | 0% | 50 | ▼ A
+ * 50% | 0% | 50 | ▼ B
+ *
+ * Every instance of a `CallView` represents a row in the call tree. The same
+ * container node is used for all rows.
+ */
+ },
+
+ /**
+ * Toggles the category information hidden or visible.
+ * @param boolean visible
+ */
+ toggleCategories: function(visible) {
+ if (!visible) {
+ this.container.setAttribute("categories-hidden", "");
+ } else {
+ this.container.removeAttribute("categories-hidden");
+ }
+ },
+
+ /**
+ * Handler for the "click" event on the url node of this call view.
+ */
+ _onUrlClick: function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ // Only emit for left click events
+ if (e.button === 0) {
+ this.root.emit("link", this);
+ }
+ },
+});
+
+exports.CallView = CallView;
diff --git a/devtools/client/performance/moz.build b/devtools/client/performance/moz.build
new file mode 100644
index 0000000000..bb8d3ac342
--- /dev/null
+++ b/devtools/client/performance/moz.build
@@ -0,0 +1,25 @@
+# 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/.
+
+DIRS += [
+ "components",
+ "modules",
+ "test",
+ "views",
+]
+
+DevToolsModules(
+ "events.js",
+ "initializer.js",
+ "panel.js",
+ "performance-controller.js",
+ "performance-view.js",
+)
+
+BROWSER_CHROME_MANIFESTS += ["test/browser.ini"]
+XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.ini"]
+
+with Files("**"):
+ BUG_COMPONENT = ("DevTools", "Performance Tools (Profiler/Timeline)")
diff --git a/devtools/client/performance/panel.js b/devtools/client/performance/panel.js
new file mode 100644
index 0000000000..28fef793f0
--- /dev/null
+++ b/devtools/client/performance/panel.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";
+
+loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
+
+function PerformancePanel(iframeWindow, toolbox) {
+ this.panelWin = iframeWindow;
+ this.toolbox = toolbox;
+ this._targetAvailablePromise = Promise.resolve();
+
+ this._onTargetAvailable = this._onTargetAvailable.bind(this);
+
+ EventEmitter.decorate(this);
+}
+
+exports.PerformancePanel = PerformancePanel;
+
+PerformancePanel.prototype = {
+ /**
+ * Open is effectively an asynchronous constructor.
+ *
+ * @return object
+ * A promise that is resolved when the Performance tool
+ * completes opening.
+ */
+ async open() {
+ if (this._opening) {
+ return this._opening;
+ }
+
+ this._checkRecordingStatus = this._checkRecordingStatus.bind(this);
+
+ const { PerformanceController, EVENTS } = this.panelWin;
+ PerformanceController.on(
+ EVENTS.RECORDING_ADDED,
+ this._checkRecordingStatus
+ );
+ PerformanceController.on(
+ EVENTS.RECORDING_STATE_CHANGE,
+ this._checkRecordingStatus
+ );
+
+ // In case that the target is switched across process, the corresponding front also
+ // will be changed. In order to detect that, watch the change.
+ // Also, we wait for `watchTargets` to end. Indeed the function `_onTargetAvailable
+ // will be called synchronously with current target as a parameter by
+ // the `watchTargets` function.
+ // So this `await` waits for initialization with current target, happening
+ // in `_onTargetAvailable`.
+ await this.toolbox.targetList.watchTargets(
+ [this.toolbox.targetList.TYPES.FRAME],
+ this._onTargetAvailable
+ );
+
+ // Fire this once incase we have an in-progress recording (console profile)
+ // that caused this start up, and no state change yet, so we can highlight the
+ // tab if we need.
+ this._checkRecordingStatus();
+
+ this.isReady = true;
+ this.emit("ready");
+
+ this._opening = new Promise(resolve => {
+ resolve(this);
+ });
+ return this._opening;
+ },
+
+ // DevToolPanel API
+
+ get target() {
+ return this.toolbox.target;
+ },
+
+ async destroy() {
+ // Make sure this panel is not already destroyed.
+ if (this._destroyed) {
+ return;
+ }
+
+ const { PerformanceController, PerformanceView, EVENTS } = this.panelWin;
+ PerformanceController.off(
+ EVENTS.RECORDING_ADDED,
+ this._checkRecordingStatus
+ );
+ PerformanceController.off(
+ EVENTS.RECORDING_STATE_CHANGE,
+ this._checkRecordingStatus
+ );
+
+ this.toolbox.targetList.unwatchTargets(
+ [this.toolbox.targetList.TYPES.FRAME],
+ this._onTargetAvailable
+ );
+ await PerformanceController.destroy();
+ await PerformanceView.destroy();
+ PerformanceController.disableFrontEventListeners();
+
+ this.emit("destroyed");
+ this._destroyed = true;
+ },
+
+ _checkRecordingStatus: function() {
+ if (this.panelWin.PerformanceController.isRecording()) {
+ this.toolbox.highlightTool("performance");
+ } else {
+ this.toolbox.unhighlightTool("performance");
+ }
+ },
+
+ /**
+ * This function executes actual logic for the target-switching.
+ *
+ * @param {TargetFront} - targetFront
+ * As we are watching only FRAME type for this panel,
+ * the target should be a instance of BrowsingContextTarget.
+ */
+ async _handleTargetAvailable({ targetFront }) {
+ if (targetFront.isTopLevel) {
+ const { PerformanceController, PerformanceView } = this.panelWin;
+ const performanceFront = await targetFront.getFront("performance");
+
+ if (!this._isPanelInitialized) {
+ await PerformanceController.initialize(
+ this.toolbox,
+ targetFront,
+ performanceFront
+ );
+ await PerformanceView.initialize();
+ PerformanceController.enableFrontEventListeners();
+ this._isPanelInitialized = true;
+ } else {
+ const isRecording = PerformanceController.isRecording();
+ if (isRecording) {
+ await PerformanceController.stopRecording();
+ }
+
+ PerformanceView.resetBufferStatus();
+ PerformanceController.updateFronts(targetFront, performanceFront);
+
+ if (isRecording) {
+ await PerformanceController.startRecording();
+ }
+ }
+ }
+ },
+
+ /**
+ * This function is called for every target is available.
+ */
+ _onTargetAvailable(parameters) {
+ // As this function is called asynchronous, while previous processing, this might be
+ // called. Thus, we wait until finishing previous one before starting next.
+ this._targetAvailablePromise = this._targetAvailablePromise.then(() =>
+ this._handleTargetAvailable(parameters)
+ );
+
+ return this._targetAvailablePromise;
+ },
+};
diff --git a/devtools/client/performance/performance-controller.js b/devtools/client/performance/performance-controller.js
new file mode 100644
index 0000000000..1fb7682661
--- /dev/null
+++ b/devtools/client/performance/performance-controller.js
@@ -0,0 +1,542 @@
+/* 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/. */
+/* globals $ */
+"use strict";
+
+const { PrefObserver } = require("devtools/client/shared/prefs");
+
+// Events emitted by various objects in the panel.
+const EVENTS = require("devtools/client/performance/events");
+
+const Services = require("Services");
+const EventEmitter = require("devtools/shared/event-emitter");
+const flags = require("devtools/shared/flags");
+
+// Logic modules
+const {
+ PerformanceTelemetry,
+} = require("devtools/client/performance/modules/logic/telemetry");
+const {
+ PerformanceView,
+} = require("devtools/client/performance/performance-view");
+const { DetailsView } = require("devtools/client/performance/views/details");
+const {
+ RecordingsView,
+} = require("devtools/client/performance/views/recordings");
+const { ToolbarView } = require("devtools/client/performance/views/toolbar");
+
+/**
+ * Functions handling target-related lifetime events and
+ * UI interaction.
+ */
+const PerformanceController = {
+ _recordings: [],
+ _currentRecording: null,
+
+ /**
+ * Listen for events emitted by the current tab target and
+ * main UI events.
+ */
+ async initialize(toolbox, targetFront, performanceFront) {
+ this.toolbox = toolbox;
+ this.target = targetFront;
+ this.front = performanceFront;
+
+ this._telemetry = new PerformanceTelemetry(this);
+ this.startRecording = this.startRecording.bind(this);
+ this.stopRecording = this.stopRecording.bind(this);
+ this.importRecording = this.importRecording.bind(this);
+ this.exportRecording = this.exportRecording.bind(this);
+ this.clearRecordings = this.clearRecordings.bind(this);
+ this._onRecordingSelectFromView = this._onRecordingSelectFromView.bind(
+ this
+ );
+ this._onPrefChanged = this._onPrefChanged.bind(this);
+ this._onThemeChanged = this._onThemeChanged.bind(this);
+ this._onDetailsViewSelected = this._onDetailsViewSelected.bind(this);
+ this._onProfilerStatus = this._onProfilerStatus.bind(this);
+ this._onRecordingStarted = this._emitRecordingStateChange.bind(
+ this,
+ "recording-started"
+ );
+ this._onRecordingStopping = this._emitRecordingStateChange.bind(
+ this,
+ "recording-stopping"
+ );
+ this._onRecordingStopped = this._emitRecordingStateChange.bind(
+ this,
+ "recording-stopped"
+ );
+
+ // Store data regarding if e10s is enabled.
+ this._e10s = Services.appinfo.browserTabsRemoteAutostart;
+ this._setMultiprocessAttributes();
+
+ this._prefs = require("devtools/client/performance/modules/global").PREFS;
+ this._prefs.registerObserver();
+ this._prefs.on("pref-changed", this._onPrefChanged);
+
+ ToolbarView.on(EVENTS.UI_PREF_CHANGED, this._onPrefChanged);
+ PerformanceView.on(EVENTS.UI_START_RECORDING, this.startRecording);
+ PerformanceView.on(EVENTS.UI_STOP_RECORDING, this.stopRecording);
+ PerformanceView.on(EVENTS.UI_IMPORT_RECORDING, this.importRecording);
+ PerformanceView.on(EVENTS.UI_CLEAR_RECORDINGS, this.clearRecordings);
+ RecordingsView.on(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
+ RecordingsView.on(
+ EVENTS.UI_RECORDING_SELECTED,
+ this._onRecordingSelectFromView
+ );
+ DetailsView.on(
+ EVENTS.UI_DETAILS_VIEW_SELECTED,
+ this._onDetailsViewSelected
+ );
+
+ this._prefObserver = new PrefObserver("devtools.");
+ this._prefObserver.on("devtools.theme", this._onThemeChanged);
+ },
+
+ /**
+ * Remove events handled by the PerformanceController
+ */
+ destroy: function() {
+ this._prefs.off("pref-changed", this._onPrefChanged);
+ this._prefs.unregisterObserver();
+
+ ToolbarView.off(EVENTS.UI_PREF_CHANGED, this._onPrefChanged);
+ PerformanceView.off(EVENTS.UI_START_RECORDING, this.startRecording);
+ PerformanceView.off(EVENTS.UI_STOP_RECORDING, this.stopRecording);
+ PerformanceView.off(EVENTS.UI_IMPORT_RECORDING, this.importRecording);
+ PerformanceView.off(EVENTS.UI_CLEAR_RECORDINGS, this.clearRecordings);
+ RecordingsView.off(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
+ RecordingsView.off(
+ EVENTS.UI_RECORDING_SELECTED,
+ this._onRecordingSelectFromView
+ );
+ DetailsView.off(
+ EVENTS.UI_DETAILS_VIEW_SELECTED,
+ this._onDetailsViewSelected
+ );
+
+ this._prefObserver.off("devtools.theme", this._onThemeChanged);
+ this._prefObserver.destroy();
+
+ this._telemetry.destroy();
+ },
+
+ updateFronts(targetFront, performanceFront) {
+ this.target = targetFront;
+ this.front = performanceFront;
+ this.enableFrontEventListeners();
+ },
+
+ /**
+ * Enables front event listeners.
+ *
+ * The rationale behind this is given by the async intialization of all the
+ * frontend components. Even though the panel is considered "open" only after
+ * both the controller and the view are created, and even though their
+ * initialization is sequential (controller, then view), the controller might
+ * start handling backend events before the view finishes if the event
+ * listeners are added too soon.
+ */
+ enableFrontEventListeners: function() {
+ this.front.on("profiler-status", this._onProfilerStatus);
+ this.front.on("recording-started", this._onRecordingStarted);
+ this.front.on("recording-stopping", this._onRecordingStopping);
+ this.front.on("recording-stopped", this._onRecordingStopped);
+ },
+
+ /**
+ * Disables front event listeners.
+ */
+ disableFrontEventListeners: function() {
+ this.front.off("profiler-status", this._onProfilerStatus);
+ this.front.off("recording-started", this._onRecordingStarted);
+ this.front.off("recording-stopping", this._onRecordingStopping);
+ this.front.off("recording-stopped", this._onRecordingStopped);
+ },
+
+ /**
+ * Returns the current devtools theme.
+ */
+ getTheme: function() {
+ return Services.prefs.getCharPref("devtools.theme");
+ },
+
+ /**
+ * Get a boolean preference setting from `prefName` via the underlying
+ * OptionsView in the ToolbarView. This preference is guaranteed to be
+ * displayed in the UI.
+ *
+ * @param string prefName
+ * @return boolean
+ */
+ getOption: function(prefName) {
+ return ToolbarView.optionsView.getPref(prefName);
+ },
+
+ /**
+ * Get a preference setting from `prefName`. This preference can be of
+ * any type and might not be displayed in the UI.
+ *
+ * @param string prefName
+ * @return any
+ */
+ getPref: function(prefName) {
+ return this._prefs[prefName];
+ },
+
+ /**
+ * Set a preference setting from `prefName`. This preference can be of
+ * any type and might not be displayed in the UI.
+ *
+ * @param string prefName
+ * @param any prefValue
+ */
+ setPref: function(prefName, prefValue) {
+ this._prefs[prefName] = prefValue;
+ },
+
+ /**
+ * Checks whether or not a new recording is supported by the PerformanceFront.
+ * @return Promise:boolean
+ */
+ async canCurrentlyRecord() {
+ const hasActor = await this.target.hasActor("performance");
+ if (!hasActor) {
+ return true;
+ }
+ return (await this.front.canCurrentlyRecord()).success;
+ },
+
+ /**
+ * Starts recording with the PerformanceFront.
+ */
+ async startRecording() {
+ const options = {
+ withMarkers: true,
+ withTicks: this.getOption("enable-framerate"),
+ withMemory: this.getOption("enable-memory"),
+ withFrames: true,
+ withGCEvents: true,
+ withAllocations: this.getOption("enable-allocations"),
+ allocationsSampleProbability: this.getPref("memory-sample-probability"),
+ allocationsMaxLogLength: this.getPref("memory-max-log-length"),
+ bufferSize: this.getPref("profiler-buffer-size"),
+ sampleFrequency: this.getPref("profiler-sample-frequency"),
+ };
+
+ const recordingStarted = await this.front.startRecording(options);
+
+ // In some cases, like when the target has a private browsing tab,
+ // recording is not currently supported because of the profiler module.
+ // Present a notification in this case alerting the user of this issue.
+ if (!recordingStarted) {
+ this.emit(EVENTS.BACKEND_FAILED_AFTER_RECORDING_START);
+ PerformanceView.setState("unavailable");
+ } else {
+ this.emit(EVENTS.BACKEND_READY_AFTER_RECORDING_START);
+ }
+ },
+
+ /**
+ * Stops recording with the PerformanceFront.
+ */
+ async stopRecording() {
+ const recording = this.getLatestManualRecording();
+
+ if (!this.front.isDestroyed()) {
+ await this.front.stopRecording(recording);
+ } else {
+ // As the front was destroyed, we do stop sequence manually without the actor.
+ recording._recording = false;
+ recording._completed = true;
+ await this._onRecordingStopped(recording);
+ }
+
+ this.emit(EVENTS.BACKEND_READY_AFTER_RECORDING_STOP);
+ },
+
+ /**
+ * Saves the given recording to a file. Emits `EVENTS.RECORDING_EXPORTED`
+ * when the file was saved.
+ *
+ * @param PerformanceRecording recording
+ * The model that holds the recording data.
+ * @param nsIFile file
+ * The file to stream the data into.
+ */
+ async exportRecording(recording, file) {
+ await recording.exportRecording(file);
+ this.emit(EVENTS.RECORDING_EXPORTED, recording, file);
+ },
+
+ /**
+ * Clears all completed recordings from the list as well as the current non-console
+ * recording. Emits `EVENTS.RECORDING_DELETED` when complete so other components can
+ * clean up.
+ */
+ async clearRecordings() {
+ for (let i = this._recordings.length - 1; i >= 0; i--) {
+ const model = this._recordings[i];
+ if (!model.isConsole() && model.isRecording()) {
+ await this.stopRecording();
+ }
+ // If last recording is not recording, but finalizing itself,
+ // wait for that to finish
+ if (!model.isRecording() && !model.isCompleted()) {
+ await this.waitForStateChangeOnRecording(model, "recording-stopped");
+ }
+ // If recording is completed,
+ // clean it up from UI and remove it from the _recordings array.
+ if (model.isCompleted()) {
+ this.emit(EVENTS.RECORDING_DELETED, model);
+ this._recordings.splice(i, 1);
+ }
+ }
+ if (this._recordings.length > 0) {
+ if (!this._recordings.includes(this.getCurrentRecording())) {
+ this.setCurrentRecording(this._recordings[0]);
+ }
+ } else {
+ this.setCurrentRecording(null);
+ }
+ },
+
+ /**
+ * Loads a recording from a file, adding it to the recordings list. Emits
+ * `EVENTS.RECORDING_IMPORTED` when the file was loaded.
+ *
+ * @param nsIFile file
+ * The file to import the data from.
+ */
+ async importRecording(file) {
+ const recording = await this.front.importRecording(file);
+ this._addRecordingIfUnknown(recording);
+
+ this.emit(EVENTS.RECORDING_IMPORTED, recording);
+ },
+
+ /**
+ * Sets the currently active PerformanceRecording. Should rarely be called directly,
+ * as RecordingsView handles this when manually selected a recording item. Exceptions
+ * are when clearing the view.
+ * @param PerformanceRecording recording
+ */
+ setCurrentRecording: function(recording) {
+ if (this._currentRecording !== recording) {
+ this._currentRecording = recording;
+ this.emit(EVENTS.RECORDING_SELECTED, recording);
+ }
+ },
+
+ /**
+ * Gets the currently active PerformanceRecording.
+ * @return PerformanceRecording
+ */
+ getCurrentRecording: function() {
+ return this._currentRecording;
+ },
+
+ /**
+ * Get most recently added recording that was triggered manually (via UI).
+ * @return PerformanceRecording
+ */
+ getLatestManualRecording: function() {
+ for (let i = this._recordings.length - 1; i >= 0; i--) {
+ const model = this._recordings[i];
+ if (!model.isConsole() && !model.isImported()) {
+ return this._recordings[i];
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Fired from RecordingsView, we listen on the PerformanceController so we can
+ * set it here and re-emit on the controller, where all views can listen.
+ */
+ _onRecordingSelectFromView: function(recording) {
+ this.setCurrentRecording(recording);
+ },
+
+ /**
+ * Fired when the ToolbarView fires a PREF_CHANGED event.
+ * with the value.
+ */
+ _onPrefChanged: function(prefName, prefValue) {
+ this.emit(EVENTS.PREF_CHANGED, prefName, prefValue);
+ },
+
+ /*
+ * Called when the developer tools theme changes.
+ */
+ _onThemeChanged: function() {
+ const newValue = Services.prefs.getCharPref("devtools.theme");
+ this.emit(EVENTS.THEME_CHANGED, newValue);
+ },
+
+ _onProfilerStatus: function(status) {
+ this.emit(EVENTS.RECORDING_PROFILER_STATUS_UPDATE, status);
+ },
+
+ _emitRecordingStateChange(eventName, recordingModel) {
+ this._addRecordingIfUnknown(recordingModel);
+ this.emit(EVENTS.RECORDING_STATE_CHANGE, eventName, recordingModel);
+ },
+
+ /**
+ * Stores a recording internally.
+ *
+ * @param {PerformanceRecordingFront} recording
+ */
+ _addRecordingIfUnknown: function(recording) {
+ if (!this._recordings.includes(recording)) {
+ this._recordings.push(recording);
+ this.emit(EVENTS.RECORDING_ADDED, recording);
+ }
+ },
+
+ /**
+ * Takes a recording and returns a value between 0 and 1 indicating how much
+ * of the buffer is used.
+ */
+ getBufferUsageForRecording: function(recording) {
+ return this.front.getBufferUsageForRecording(recording);
+ },
+
+ /**
+ * Returns a boolean indicating if any recordings are currently in progress or not.
+ */
+ isRecording: function() {
+ return this._recordings.some(r => r.isRecording());
+ },
+
+ /**
+ * Returns the internal store of recording models.
+ */
+ getRecordings: function() {
+ return this._recordings;
+ },
+
+ /**
+ * Returns traits from the front.
+ */
+ getTraits: function() {
+ return this.front.traits;
+ },
+
+ viewSourceInDebugger(url, line, column) {
+ // Currently, the line and column values are strings, so we have to convert
+ // them to numbers before passing them on to the toolbox.
+ return this.toolbox.viewSourceInDebugger(url, +line, +column);
+ },
+
+ /**
+ * Utility method taking a string or an array of strings of feature names (like
+ * "withAllocations" or "withMarkers"), and returns whether or not the current
+ * recording supports that feature, based off of UI preferences and server support.
+ *
+ * @option {Array<string>|string} features
+ * A string or array of strings indicating what configuration is needed on the
+ * recording model, like `withTicks`, or `withMemory`.
+ *
+ * @return boolean
+ */
+ isFeatureSupported: function(features) {
+ if (!features) {
+ return true;
+ }
+
+ const recording = this.getCurrentRecording();
+ if (!recording) {
+ return false;
+ }
+
+ const config = recording.getConfiguration();
+ return [].concat(features).every(f => config[f]);
+ },
+
+ /**
+ * Takes an array of PerformanceRecordingFronts and adds them to the internal
+ * store of the UI. Used by the toolbox to lazily seed recordings that
+ * were observed before the panel was loaded in the scenario where `console.profile()`
+ * is used before the tool is loaded.
+ *
+ * @param {Array<PerformanceRecordingFront>} recordings
+ */
+ populateWithRecordings: function(recordings = []) {
+ for (const recording of recordings) {
+ PerformanceController._addRecordingIfUnknown(recording);
+ }
+ this.emit(EVENTS.RECORDINGS_SEEDED);
+ },
+
+ /**
+ * Returns an object with `supported` and `enabled` properties indicating
+ * whether or not the platform is capable of turning on e10s and whether or not
+ * it's already enabled, respectively.
+ *
+ * @return {object}
+ */
+ getMultiprocessStatus: function() {
+ // If testing, set enabled to true so we have realtime rendering tests
+ // in non-e10s. This function is overridden wholesale in tests
+ // when we want to test multiprocess support
+ // specifically.
+ if (flags.testing) {
+ return { enabled: true };
+ }
+ // This is only checked on tool startup -- requires a restart if
+ // e10s subsequently enabled.
+ const enabled = this._e10s;
+ return { enabled };
+ },
+
+ /**
+ * Takes a PerformanceRecording and a state, and waits for
+ * the event to be emitted from the front for that recording.
+ *
+ * @param {PerformanceRecordingFront} recording
+ * @param {string} expectedState
+ * @return {Promise}
+ */
+ async waitForStateChangeOnRecording(recording, expectedState) {
+ await new Promise(resolve => {
+ this.on(EVENTS.RECORDING_STATE_CHANGE, function handler(state, model) {
+ if (state === expectedState && model === recording) {
+ this.off(EVENTS.RECORDING_STATE_CHANGE, handler);
+ resolve();
+ }
+ });
+ });
+ },
+
+ /**
+ * Called on init, sets an `e10s` attribute on the main view container with
+ * "disabled" if e10s is possible on the platform and just not on, or "unsupported"
+ * if e10s is not possible on the platform. If e10s is on, no attribute is set.
+ */
+ _setMultiprocessAttributes: function() {
+ const { enabled } = this.getMultiprocessStatus();
+ if (!enabled) {
+ $("#performance-view").setAttribute("e10s", "disabled");
+ }
+ },
+
+ /**
+ * Pipes EVENTS.UI_DETAILS_VIEW_SELECTED to the PerformanceController.
+ */
+ _onDetailsViewSelected: function(...data) {
+ this.emit(EVENTS.UI_DETAILS_VIEW_SELECTED, ...data);
+ },
+
+ toString: () => "[object PerformanceController]",
+};
+
+/**
+ * Convenient way of emitting events from the controller.
+ */
+EventEmitter.decorate(PerformanceController);
+exports.PerformanceController = PerformanceController;
diff --git a/devtools/client/performance/performance-view.js b/devtools/client/performance/performance-view.js
new file mode 100644
index 0000000000..98775327b6
--- /dev/null
+++ b/devtools/client/performance/performance-view.js
@@ -0,0 +1,490 @@
+/* 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/. */
+/* globals $, $$, PerformanceController */
+"use strict";
+
+const EventEmitter = require("devtools/shared/event-emitter");
+
+const React = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+
+const RecordingControls = React.createFactory(
+ require("devtools/client/performance/components/RecordingControls")
+);
+const RecordingButton = React.createFactory(
+ require("devtools/client/performance/components/RecordingButton")
+);
+
+const EVENTS = require("devtools/client/performance/events");
+const PerformanceUtils = require("devtools/client/performance/modules/utils");
+const { DetailsView } = require("devtools/client/performance/views/details");
+const { OverviewView } = require("devtools/client/performance/views/overview");
+const {
+ RecordingsView,
+} = require("devtools/client/performance/views/recordings");
+const { ToolbarView } = require("devtools/client/performance/views/toolbar");
+
+const { L10N } = require("devtools/client/performance/modules/global");
+/**
+ * Master view handler for the performance tool.
+ */
+var PerformanceView = {
+ _state: null,
+
+ // Set to true if the front emits a "buffer-status" event, indicating
+ // that the server has support for determining buffer status.
+ _bufferStatusSupported: false,
+
+ // Mapping of state to selectors for different properties and their values,
+ // from the main profiler view. Used in `PerformanceView.setState()`
+ states: {
+ unavailable: [
+ {
+ sel: "#performance-view",
+ opt: "selectedPanel",
+ val: () => $("#unavailable-notice"),
+ },
+ {
+ sel: "#performance-view-content",
+ opt: "hidden",
+ val: () => true,
+ },
+ ],
+ empty: [
+ {
+ sel: "#performance-view",
+ opt: "selectedPanel",
+ val: () => $("#empty-notice"),
+ },
+ {
+ sel: "#performance-view-content",
+ opt: "hidden",
+ val: () => true,
+ },
+ ],
+ recording: [
+ {
+ sel: "#performance-view",
+ opt: "selectedPanel",
+ val: () => $("#performance-view-content"),
+ },
+ {
+ sel: "#performance-view-content",
+ opt: "hidden",
+ val: () => false,
+ },
+ {
+ sel: "#details-pane-container",
+ opt: "selectedPanel",
+ val: () => $("#recording-notice"),
+ },
+ ],
+ "console-recording": [
+ {
+ sel: "#performance-view",
+ opt: "selectedPanel",
+ val: () => $("#performance-view-content"),
+ },
+ {
+ sel: "#performance-view-content",
+ opt: "hidden",
+ val: () => false,
+ },
+ {
+ sel: "#details-pane-container",
+ opt: "selectedPanel",
+ val: () => $("#console-recording-notice"),
+ },
+ ],
+ recorded: [
+ {
+ sel: "#performance-view",
+ opt: "selectedPanel",
+ val: () => $("#performance-view-content"),
+ },
+ {
+ sel: "#performance-view-content",
+ opt: "hidden",
+ val: () => false,
+ },
+ {
+ sel: "#details-pane-container",
+ opt: "selectedPanel",
+ val: () => $("#details-pane"),
+ },
+ ],
+ loading: [
+ {
+ sel: "#performance-view",
+ opt: "selectedPanel",
+ val: () => $("#performance-view-content"),
+ },
+ {
+ sel: "#performance-view-content",
+ opt: "hidden",
+ val: () => false,
+ },
+ {
+ sel: "#details-pane-container",
+ opt: "selectedPanel",
+ val: () => $("#loading-notice"),
+ },
+ ],
+ },
+
+ /**
+ * Sets up the view with event binding and main subviews.
+ */
+ async initialize() {
+ this._onRecordButtonClick = this._onRecordButtonClick.bind(this);
+ this._onImportButtonClick = this._onImportButtonClick.bind(this);
+ this._onClearButtonClick = this._onClearButtonClick.bind(this);
+ this._onRecordingSelected = this._onRecordingSelected.bind(this);
+ this._onProfilerStatusUpdated = this._onProfilerStatusUpdated.bind(this);
+ this._onRecordingStateChange = this._onRecordingStateChange.bind(this);
+ this._onNewRecordingFailed = this._onNewRecordingFailed.bind(this);
+
+ // Bind to controller events to unlock the record button
+ PerformanceController.on(
+ EVENTS.RECORDING_SELECTED,
+ this._onRecordingSelected
+ );
+ PerformanceController.on(
+ EVENTS.RECORDING_PROFILER_STATUS_UPDATE,
+ this._onProfilerStatusUpdated
+ );
+ PerformanceController.on(
+ EVENTS.RECORDING_STATE_CHANGE,
+ this._onRecordingStateChange
+ );
+ PerformanceController.on(
+ EVENTS.RECORDING_ADDED,
+ this._onRecordingStateChange
+ );
+ PerformanceController.on(
+ EVENTS.BACKEND_FAILED_AFTER_RECORDING_START,
+ this._onNewRecordingFailed
+ );
+
+ if (await PerformanceController.canCurrentlyRecord()) {
+ this.setState("empty");
+ } else {
+ this.setState("unavailable");
+ }
+
+ // Initialize the ToolbarView first, because other views may need access
+ // to the OptionsView via the controller, to read prefs.
+ await ToolbarView.initialize();
+ await RecordingsView.initialize();
+ await OverviewView.initialize();
+ await DetailsView.initialize();
+
+ // DE-XUL: Begin migrating the toolbar to React. Temporarily hold state here.
+ this._recordingControlsState = {
+ onRecordButtonClick: this._onRecordButtonClick,
+ onImportButtonClick: this._onImportButtonClick,
+ onClearButtonClick: this._onClearButtonClick,
+ isRecording: false,
+ isDisabled: false,
+ };
+ // Mount to an HTML element.
+ const { createHtmlMount } = PerformanceUtils;
+ this._recordingControlsMount = createHtmlMount(
+ $("#recording-controls-mount")
+ );
+ this._recordingButtonsMounts = Array.from(
+ $$(".recording-button-mount")
+ ).map(createHtmlMount);
+
+ this._renderRecordingControls();
+ },
+
+ /**
+ * DE-XUL: Render the recording controls and buttons using React.
+ */
+ _renderRecordingControls: function() {
+ ReactDOM.render(
+ RecordingControls(this._recordingControlsState),
+ this._recordingControlsMount
+ );
+ for (const button of this._recordingButtonsMounts) {
+ ReactDOM.render(RecordingButton(this._recordingControlsState), button);
+ }
+ },
+
+ /**
+ * Unbinds events and destroys subviews.
+ */
+ async destroy() {
+ PerformanceController.off(
+ EVENTS.RECORDING_SELECTED,
+ this._onRecordingSelected
+ );
+ PerformanceController.off(
+ EVENTS.RECORDING_PROFILER_STATUS_UPDATE,
+ this._onProfilerStatusUpdated
+ );
+ PerformanceController.off(
+ EVENTS.RECORDING_STATE_CHANGE,
+ this._onRecordingStateChange
+ );
+ PerformanceController.off(
+ EVENTS.RECORDING_ADDED,
+ this._onRecordingStateChange
+ );
+ PerformanceController.off(
+ EVENTS.BACKEND_FAILED_AFTER_RECORDING_START,
+ this._onNewRecordingFailed
+ );
+
+ await ToolbarView.destroy();
+ await RecordingsView.destroy();
+ await OverviewView.destroy();
+ await DetailsView.destroy();
+ },
+
+ /**
+ * Sets the state of the profiler view. Possible options are "unavailable",
+ * "empty", "recording", "console-recording", "recorded".
+ */
+ setState: function(state) {
+ // Make sure that the focus isn't captured on a hidden iframe. This fixes a
+ // XUL bug where shortcuts stop working.
+ const iframes = window.document.querySelectorAll("iframe");
+ for (const iframe of iframes) {
+ iframe.blur();
+ }
+ window.focus();
+
+ const viewConfig = this.states[state];
+ if (!viewConfig) {
+ throw new Error(`Invalid state for PerformanceView: ${state}`);
+ }
+ for (const { sel, opt, val } of viewConfig) {
+ for (const el of $$(sel)) {
+ el[opt] = val();
+ }
+ }
+
+ this._state = state;
+
+ if (state === "console-recording") {
+ const recording = PerformanceController.getCurrentRecording();
+ let label = recording.getLabel() || "";
+
+ // Wrap the label in quotes if it exists for the commands.
+ label = label ? `"${label}"` : "";
+
+ const startCommand = $(
+ ".console-profile-recording-notice .console-profile-command"
+ );
+ const stopCommand = $(
+ ".console-profile-stop-notice .console-profile-command"
+ );
+
+ startCommand.value = `console.profile(${label})`;
+ stopCommand.value = `console.profileEnd(${label})`;
+ }
+
+ this.updateBufferStatus();
+ this.emit(EVENTS.UI_STATE_CHANGED, state);
+ },
+
+ /**
+ * Returns the state of the PerformanceView.
+ */
+ getState: function() {
+ return this._state;
+ },
+
+ /**
+ * Reset the displayed buffer status.
+ * Called for every target-switching.
+ */
+ resetBufferStatus() {
+ this._bufferStatusSupported = false;
+ $("#details-pane-container").removeAttribute("buffer-status");
+ },
+
+ /**
+ * Updates the displayed buffer status.
+ */
+ updateBufferStatus: function() {
+ // If we've never seen a "buffer-status" event from the front, ignore
+ // and keep the buffer elements hidden.
+ if (!this._bufferStatusSupported) {
+ return;
+ }
+
+ const recording = PerformanceController.getCurrentRecording();
+ if (!recording || !recording.isRecording()) {
+ return;
+ }
+
+ const bufferUsage =
+ PerformanceController.getBufferUsageForRecording(recording) || 0;
+
+ // Normalize to a percentage value
+ const percent = Math.floor(bufferUsage * 100);
+
+ const $container = $("#details-pane-container");
+ const $bufferLabel = $(".buffer-status-message", $container.selectedPanel);
+
+ // Be a little flexible on the buffer status, although not sure how
+ // this could happen, as RecordingModel clamps.
+ if (percent >= 99) {
+ $container.setAttribute("buffer-status", "full");
+ } else {
+ $container.setAttribute("buffer-status", "in-progress");
+ }
+
+ $bufferLabel.value = L10N.getFormatStr("profiler.bufferFull", percent);
+ this.emit(EVENTS.UI_RECORDING_PROFILER_STATUS_RENDERED, percent);
+ },
+
+ /**
+ * Toggles the `locked` attribute on the record buttons based
+ * on `lock`.
+ *
+ * @param {boolean} lock
+ */
+ _lockRecordButtons: function(lock) {
+ this._recordingControlsState.isLocked = lock;
+ this._renderRecordingControls();
+ },
+
+ /*
+ * Toggles the `checked` attribute on the record buttons based
+ * on `activate`.
+ *
+ * @param {boolean} activate
+ */
+ _toggleRecordButtons: function(activate) {
+ this._recordingControlsState.isRecording = activate;
+ this._renderRecordingControls();
+ },
+
+ /**
+ * When a recording has started.
+ */
+ _onRecordingStateChange: function() {
+ const currentRecording = PerformanceController.getCurrentRecording();
+ const recordings = PerformanceController.getRecordings();
+
+ this._toggleRecordButtons(
+ !!recordings.find(r => !r.isConsole() && r.isRecording())
+ );
+ this._lockRecordButtons(
+ !!recordings.find(r => !r.isConsole() && r.isFinalizing())
+ );
+
+ if (currentRecording && currentRecording.isFinalizing()) {
+ this.setState("loading");
+ }
+ if (currentRecording && currentRecording.isCompleted()) {
+ this.setState("recorded");
+ }
+ if (currentRecording && currentRecording.isRecording()) {
+ this.updateBufferStatus();
+ }
+ },
+
+ /**
+ * When starting a recording has failed.
+ */
+ _onNewRecordingFailed: function() {
+ this._lockRecordButtons(false);
+ this._toggleRecordButtons(false);
+ },
+
+ /**
+ * Handler for clicking the clear button.
+ */
+ _onClearButtonClick: function(e) {
+ this.emit(EVENTS.UI_CLEAR_RECORDINGS);
+ },
+
+ /**
+ * Handler for clicking the record button.
+ */
+ _onRecordButtonClick: function(e) {
+ if (this._recordingControlsState.isRecording) {
+ this.emit(EVENTS.UI_STOP_RECORDING);
+ } else {
+ this._lockRecordButtons(true);
+ this._toggleRecordButtons(true);
+ this.emit(EVENTS.UI_START_RECORDING);
+ }
+ },
+
+ /**
+ * Handler for clicking the import button.
+ */
+ _onImportButtonClick: function(e) {
+ const fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ fp.init(
+ window,
+ L10N.getStr("recordingsList.importDialogTitle"),
+ Ci.nsIFilePicker.modeOpen
+ );
+ fp.appendFilter(
+ L10N.getStr("recordingsList.saveDialogJSONFilter"),
+ "*.json"
+ );
+ fp.appendFilter(L10N.getStr("recordingsList.saveDialogAllFilter"), "*.*");
+
+ fp.open(rv => {
+ if (rv == Ci.nsIFilePicker.returnOK) {
+ this.emit(EVENTS.UI_IMPORT_RECORDING, fp.file);
+ }
+ });
+ },
+
+ /**
+ * Fired when a recording is selected. Used to toggle the profiler view state.
+ */
+ _onRecordingSelected: function(recording) {
+ if (!recording) {
+ this.setState("empty");
+ } else if (recording.isRecording() && recording.isConsole()) {
+ this.setState("console-recording");
+ } else if (recording.isRecording()) {
+ this.setState("recording");
+ } else {
+ this.setState("recorded");
+ }
+ },
+
+ /**
+ * Fired when the controller has updated information on the buffer's status.
+ * Update the buffer status display if shown.
+ */
+ _onProfilerStatusUpdated: function(profilerStatus) {
+ // We only care about buffer status here, so check to see
+ // if it has position.
+ if (!profilerStatus || profilerStatus.position === void 0) {
+ return;
+ }
+ // If this is our first buffer event, set the status and add a class
+ if (!this._bufferStatusSupported) {
+ this._bufferStatusSupported = true;
+ $("#details-pane-container").setAttribute("buffer-status", "in-progress");
+ }
+
+ if (!this.getState("recording") && !this.getState("console-recording")) {
+ return;
+ }
+
+ this.updateBufferStatus();
+ },
+
+ toString: () => "[object PerformanceView]",
+};
+
+/**
+ * Convenient way of emitting events from the view.
+ */
+EventEmitter.decorate(PerformanceView);
+
+exports.PerformanceView = PerformanceView;
diff --git a/devtools/client/performance/test/.eslintrc.js b/devtools/client/performance/test/.eslintrc.js
new file mode 100644
index 0000000000..3d0bd99e1b
--- /dev/null
+++ b/devtools/client/performance/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ extends: "../../../.eslintrc.mochitests.js",
+};
diff --git a/devtools/client/performance/test/browser.ini b/devtools/client/performance/test/browser.ini
new file mode 100644
index 0000000000..c85ae87ce9
--- /dev/null
+++ b/devtools/client/performance/test/browser.ini
@@ -0,0 +1,148 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+skip-if = os == 'linux' && e10s && (asan || debug) # Bug 1254821
+support-files =
+ doc_allocs.html
+ doc_innerHTML.html
+ doc_markers.html
+ doc_simple-test.html
+ doc_worker.html
+ js_simpleWorker.js
+ head.js
+ !/devtools/client/shared/test/telemetry-test-helpers.js
+
+[browser_aaa-run-first-leaktest.js]
+[browser_perf-button-states.js]
+[browser_perf-calltree-js-categories.js]
+skip-if = (os == 'win' && os_version == '10.0' && bits == 64 && !asan) # Bug 1466377
+[browser_perf-calltree-js-columns.js]
+[browser_perf-calltree-js-events.js]
+fail-if = a11y_checks # bug 1687790 call-tree-item is not accessible
+[browser_perf-calltree-memory-columns.js]
+[browser_perf-console-record-01.js]
+[browser_perf-console-record-02.js]
+[browser_perf-console-record-03.js]
+[browser_perf-console-record-04.js]
+[browser_perf-console-record-05.js]
+[browser_perf-console-record-06.js]
+[browser_perf-console-record-07.js]
+[browser_perf-console-record-08.js]
+[browser_perf-console-record-09.js]
+[browser_perf-details-01-toggle.js]
+[browser_perf-details-02-utility-fun.js]
+[browser_perf-details-03-without-allocations.js]
+[browser_perf-details-04-toolbar-buttons.js]
+[browser_perf-details-05-preserve-view.js]
+[browser_perf-details-06-rerender-on-selection.js]
+[browser_perf-details-07-bleed-events.js]
+[browser_perf-details-render-00-waterfall.js]
+[browser_perf-details-render-01-js-calltree.js]
+[browser_perf-details-render-02-js-flamegraph.js]
+[browser_perf-details-render-03-memory-calltree.js]
+[browser_perf-details-render-04-memory-flamegraph.js]
+[browser_perf-docload.js]
+[browser_perf-fission-switch-target.js]
+[browser_perf-highlighted.js]
+[browser_perf-loading-01.js]
+[browser_perf-loading-02.js]
+[browser_perf-marker-details.js]
+disabled=TODO bug 1256350
+[browser_perf-options-01-toggle-throw.js]
+[browser_perf-options-02-toggle-throw-alt.js]
+[browser_perf-options-03-toggle-meta.js]
+[browser_perf-options-enable-framerate-01.js]
+[browser_perf-options-enable-framerate-02.js]
+[browser_perf-options-enable-memory-01.js]
+[browser_perf-options-enable-memory-02.js]
+[browser_perf-options-flatten-tree-recursion-01.js]
+[browser_perf-options-flatten-tree-recursion-02.js]
+[browser_perf-options-invert-call-tree-01.js]
+[browser_perf-options-invert-call-tree-02.js]
+[browser_perf-options-invert-flame-graph-01.js]
+[browser_perf-options-invert-flame-graph-02.js]
+[browser_perf-options-propagate-allocations.js]
+[browser_perf-options-propagate-profiler.js]
+[browser_perf-options-show-idle-blocks-01.js]
+[browser_perf-options-show-idle-blocks-02.js]
+[browser_perf-options-show-jit-optimizations.js]
+disabled=TODO bug 1256350
+[browser_perf-options-show-platform-data-01.js]
+[browser_perf-options-show-platform-data-02.js]
+[browser_perf-overview-render-01.js]
+[browser_perf-overview-render-02.js]
+[browser_perf-overview-render-03.js]
+[browser_perf-overview-render-04.js]
+[browser_perf-overview-selection-01.js]
+[browser_perf-overview-selection-02.js]
+[browser_perf-overview-selection-03.js]
+[browser_perf-overview-time-interval.js]
+[browser_perf-private-browsing.js]
+disabled=TODO bug 1256350
+[browser_perf-range-changed-render.js]
+[browser_perf-recording-notices-01.js]
+[browser_perf-recording-notices-02.js]
+[browser_perf-recording-notices-03.js]
+skip-if = (debug && (bits == 32)) # debug 32 bit: bug 1273374
+[browser_perf-recording-notices-04.js]
+[browser_perf-recording-notices-05.js]
+[browser_perf-recording-selected-01.js]
+[browser_perf-recording-selected-02.js]
+[browser_perf-recording-selected-03.js]
+[browser_perf-recording-selected-04.js]
+[browser_perf-recordings-clear-01.js]
+[browser_perf-recordings-clear-02.js]
+[browser_perf-recordings-io-01.js]
+disabled=TODO bug 1256350
+[browser_perf-recordings-io-02.js]
+disabled=TODO bug 1256350
+[browser_perf-recordings-io-03.js]
+disabled=TODO bug 1256350
+[browser_perf-recordings-io-04.js]
+disabled=TODO bug 1256350
+[browser_perf-recordings-io-05.js]
+disabled=TODO bug 1256350
+[browser_perf-recordings-io-06.js]
+disabled=TODO bug 1256350
+[browser_perf-refresh.js]
+[browser_perf-states.js]
+[browser_perf-telemetry-01.js]
+[browser_perf-telemetry-02.js]
+[browser_perf-telemetry-03.js]
+[browser_perf-telemetry-04.js]
+[browser_perf-theme-toggle.js]
+disabled=TODO bug 1256350
+[browser_perf-tree-abstract-01.js]
+skip-if = (verify && debug && (os == 'win' || os == 'mac'))
+[browser_perf-tree-abstract-02.js]
+skip-if = (verify && debug && (os == 'win' || os == 'mac'))
+[browser_perf-tree-abstract-03.js]
+skip-if = (verify && debug && (os == 'win' || os == 'mac'))
+[browser_perf-tree-abstract-04.js]
+skip-if = (verify && debug && (os == 'win' || os == 'mac'))
+[browser_perf-tree-abstract-05.js]
+[browser_perf-tree-view-01.js]
+[browser_perf-tree-view-02.js]
+[browser_perf-tree-view-03.js]
+[browser_perf-tree-view-04.js]
+[browser_perf-tree-view-05.js]
+[browser_perf-tree-view-06.js]
+[browser_perf-tree-view-07.js]
+[browser_perf-tree-view-08.js]
+[browser_perf-tree-view-09.js]
+[browser_perf-tree-view-10.js]
+[browser_perf-tree-view-11.js]
+disabled=TODO bug 1256350
+[browser_perf-ui-recording.js]
+[browser_timeline-filters-01.js]
+disabled=TODO bug 1256350
+[browser_timeline-filters-02.js]
+disabled=TODO bug 1256350
+[browser_timeline-waterfall-background.js]
+[browser_timeline-waterfall-generic.js]
+[browser_timeline-waterfall-rerender.js]
+disabled=TODO bug 1256350
+[browser_timeline-waterfall-sidebar.js]
+disabled=TODO bug 1256350
+[browser_timeline-waterfall-workers.js]
+disabled=TODO bug 1256350
diff --git a/devtools/client/performance/test/browser_aaa-run-first-leaktest.js b/devtools/client/performance/test/browser_aaa-run-first-leaktest.js
new file mode 100644
index 0000000000..b7d5735c72
--- /dev/null
+++ b/devtools/client/performance/test/browser_aaa-run-first-leaktest.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the performance tool leaks on initialization and sudden destruction.
+ * You can also use this initialization format as a template for other tests.
+ */
+
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+
+add_task(async function() {
+ const { target, toolbox, panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ ok(target, "Should have a target available.");
+ ok(toolbox, "Should have a toolbox available.");
+ ok(panel, "Should have a panel available.");
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-button-states.js b/devtools/client/performance/test/browser_perf-button-states.js
new file mode 100644
index 0000000000..aeb74b3915
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-button-states.js
@@ -0,0 +1,105 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the recording button states are set as expected.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const {
+ $,
+ $$,
+ EVENTS,
+ PerformanceController,
+ PerformanceView,
+ } = panel.panelWin;
+
+ const recordButton = $("#main-record-button");
+
+ checkRecordButtonsStates(false, false);
+
+ const uiStartClick = once(PerformanceView, EVENTS.UI_START_RECORDING);
+ const recordingStarted = once(
+ PerformanceController,
+ EVENTS.RECORDING_STATE_CHANGE,
+ {
+ expectedArgs: ["recording-started"],
+ }
+ );
+ const backendStartReady = once(
+ PerformanceController,
+ EVENTS.BACKEND_READY_AFTER_RECORDING_START
+ );
+ const uiStateRecording = once(PerformanceView, EVENTS.UI_STATE_CHANGED, {
+ expectedArgs: ["recording"],
+ });
+
+ await click(recordButton);
+ await uiStartClick;
+
+ checkRecordButtonsStates(true, true);
+
+ await recordingStarted;
+
+ checkRecordButtonsStates(true, false);
+
+ await backendStartReady;
+ await uiStateRecording;
+
+ const uiStopClick = once(PerformanceView, EVENTS.UI_STOP_RECORDING);
+ const recordingStopped = once(
+ PerformanceController,
+ EVENTS.RECORDING_STATE_CHANGE,
+ {
+ expectedArgs: ["recording-stopped"],
+ }
+ );
+ const backendStopReady = once(
+ PerformanceController,
+ EVENTS.BACKEND_READY_AFTER_RECORDING_STOP
+ );
+ const uiStateRecorded = once(PerformanceView, EVENTS.UI_STATE_CHANGED, {
+ expectedArgs: ["recorded"],
+ });
+
+ await click(recordButton);
+ await uiStopClick;
+ await recordingStopped;
+
+ checkRecordButtonsStates(false, false);
+
+ await backendStopReady;
+ await uiStateRecorded;
+
+ await teardownToolboxAndRemoveTab(panel);
+
+ function checkRecordButtonsStates(checked, locked) {
+ for (const button of $$(".record-button")) {
+ is(
+ button.classList.contains("checked"),
+ checked,
+ "The record button checked state should be " + checked
+ );
+ is(
+ button.disabled,
+ locked,
+ "The record button locked state should be " + locked
+ );
+ }
+ }
+});
diff --git a/devtools/client/performance/test/browser_perf-calltree-js-categories.js b/devtools/client/performance/test/browser_perf-calltree-js-categories.js
new file mode 100644
index 0000000000..cdcdbaf920
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-calltree-js-categories.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the categories are shown in the js call tree when
+ * platform data is enabled.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ UI_SHOW_PLATFORM_DATA_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ busyWait,
+} = require("devtools/client/performance/test/helpers/wait-utils");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { EVENTS, $, $$, DetailsView, JsCallTreeView } = panel.panelWin;
+
+ // Enable platform data to show the categories in the tree.
+ Services.prefs.setBoolPref(UI_SHOW_PLATFORM_DATA_PREF, true);
+
+ await startRecording(panel);
+ // To show the `Gecko` category in the tree.
+ await busyWait(100);
+ await stopRecording(panel);
+
+ const rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ await DetailsView.selectView("js-calltree");
+ await rendered;
+
+ is(
+ $(".call-tree-cells-container").hasAttribute("categories-hidden"),
+ false,
+ "The call tree cells container should show the categories now."
+ );
+ ok(
+ geckoCategoryPresent($$),
+ "A category node with the text `Gecko` is displayed in the tree."
+ );
+
+ // Disable platform data to hide the categories.
+ Services.prefs.setBoolPref(UI_SHOW_PLATFORM_DATA_PREF, false);
+
+ is(
+ $(".call-tree-cells-container").getAttribute("categories-hidden"),
+ "",
+ "The call tree cells container should hide the categories now."
+ );
+ ok(
+ !geckoCategoryPresent($$),
+ "A category node with the text `Gecko` doesn't exist in the tree anymore."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
+
+function geckoCategoryPresent($$) {
+ for (const elem of $$(".call-tree-category")) {
+ if (elem.textContent.trim() == "Gecko") {
+ return true;
+ }
+ }
+ return false;
+}
diff --git a/devtools/client/performance/test/browser_perf-calltree-js-columns.js b/devtools/client/performance/test/browser_perf-calltree-js-columns.js
new file mode 100644
index 0000000000..8fd330c36b
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-calltree-js-columns.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the js call tree view renders the correct columns.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ UI_SHOW_PLATFORM_DATA_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ busyWait,
+} = require("devtools/client/performance/test/helpers/wait-utils");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { EVENTS, $, $$, DetailsView, JsCallTreeView } = panel.panelWin;
+
+ // Enable platform data to show the platform functions in the tree.
+ Services.prefs.setBoolPref(UI_SHOW_PLATFORM_DATA_PREF, true);
+
+ await startRecording(panel);
+ // To show the `busyWait` function in the tree.
+ await busyWait(100);
+ await stopRecording(panel);
+
+ const rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ await DetailsView.selectView("js-calltree");
+ await rendered;
+
+ ok(
+ DetailsView.isViewSelected(JsCallTreeView),
+ "The call tree is now selected."
+ );
+
+ testCells($, $$, {
+ duration: true,
+ percentage: true,
+ allocations: false,
+ "self-duration": true,
+ "self-percentage": true,
+ "self-allocations": false,
+ samples: true,
+ function: true,
+ });
+
+ await teardownToolboxAndRemoveTab(panel);
+});
+
+function testCells($, $$, visibleCells) {
+ for (const cell in visibleCells) {
+ if (visibleCells[cell]) {
+ ok(
+ $(`.call-tree-cell[type=${cell}]`),
+ `At least one ${cell} column was visible in the tree.`
+ );
+ } else {
+ ok(
+ !$(`.call-tree-cell[type=${cell}]`),
+ `No ${cell} columns were visible in the tree.`
+ );
+ }
+ }
+
+ is(
+ $$(".call-tree-cell", $(".call-tree-item")).length,
+ Object.keys(visibleCells).filter(e => visibleCells[e]).length,
+ "The correct number of cells were found in the tree."
+ );
+}
diff --git a/devtools/client/performance/test/browser_perf-calltree-js-events.js b/devtools/client/performance/test/browser_perf-calltree-js-events.js
new file mode 100644
index 0000000000..b9651bba49
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-calltree-js-events.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the call tree up/down events work for js calltrees.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ synthesizeProfile,
+} = require("devtools/client/performance/test/helpers/synth-utils");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+const {
+ ThreadNode,
+} = require("devtools/client/performance/modules/logic/tree-model");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const {
+ EVENTS,
+ $,
+ DetailsView,
+ OverviewView,
+ JsCallTreeView,
+ } = panel.panelWin;
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ const rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ await DetailsView.selectView("js-calltree");
+ await rendered;
+
+ // Mock the profile used so we can get a deterministic tree created.
+ const profile = synthesizeProfile();
+ const threadNode = new ThreadNode(
+ profile.threads[0],
+ OverviewView.getTimeInterval()
+ );
+ JsCallTreeView._populateCallTree(threadNode);
+ JsCallTreeView.emit(EVENTS.UI_JS_CALL_TREE_RENDERED);
+
+ const firstTreeItem = $("#js-calltree-view .call-tree-item");
+
+ // DE-XUL: There are focus issues with XUL. Focus first, then synthesize the clicks
+ // so that keyboard events work correctly.
+ firstTreeItem.focus();
+
+ let count = 0;
+ const onFocus = () => count++;
+ JsCallTreeView.on("focus", onFocus);
+
+ await click(firstTreeItem);
+
+ key("VK_DOWN");
+ key("VK_DOWN");
+ key("VK_DOWN");
+ key("VK_DOWN");
+
+ JsCallTreeView.off("focus", onFocus);
+ is(count, 4, "Several focus events are fired for the calltree.");
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-calltree-memory-columns.js b/devtools/client/performance/test/browser_perf-calltree-memory-columns.js
new file mode 100644
index 0000000000..5abcf857a3
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-calltree-memory-columns.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the memory call tree view renders the correct columns.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ UI_ENABLE_ALLOCATIONS_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { EVENTS, $, $$, DetailsView, MemoryCallTreeView } = panel.panelWin;
+
+ // Enable allocations to test.
+ Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ const rendered = once(
+ MemoryCallTreeView,
+ EVENTS.UI_MEMORY_CALL_TREE_RENDERED
+ );
+ await DetailsView.selectView("memory-calltree");
+ await rendered;
+
+ ok(
+ DetailsView.isViewSelected(MemoryCallTreeView),
+ "The call tree is now selected."
+ );
+
+ testCells($, $$, {
+ duration: false,
+ percentage: false,
+ count: true,
+ "count-percentage": true,
+ size: true,
+ "size-percentage": true,
+ "self-duration": false,
+ "self-percentage": false,
+ "self-count": true,
+ "self-count-percentage": true,
+ "self-size": true,
+ "self-size-percentage": true,
+ samples: false,
+ function: true,
+ });
+
+ await teardownToolboxAndRemoveTab(panel);
+});
+
+function testCells($, $$, visibleCells) {
+ for (const cell in visibleCells) {
+ if (visibleCells[cell]) {
+ ok(
+ $(`.call-tree-cell[type=${cell}]`),
+ `At least one ${cell} column was visible in the tree.`
+ );
+ } else {
+ ok(
+ !$(`.call-tree-cell[type=${cell}]`),
+ `No ${cell} columns were visible in the tree.`
+ );
+ }
+ }
+
+ is(
+ $$(".call-tree-cell", $(".call-tree-item")).length,
+ Object.keys(visibleCells).filter(e => visibleCells[e]).length,
+ "The correct number of cells were found in the tree."
+ );
+}
diff --git a/devtools/client/performance/test/browser_perf-console-record-01.js b/devtools/client/performance/test/browser_perf-console-record-01.js
new file mode 100644
index 0000000000..4a3c12630d
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-console-record-01.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the profiler is populated by console recordings that have finished
+ * before it was opened.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInTab,
+ initConsoleInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ waitUntil,
+} = require("devtools/client/performance/test/helpers/wait-utils");
+const {
+ getSelectedRecording,
+} = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(async function() {
+ const { target, console } = await initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ await console.profile("rust");
+ await console.profileEnd("rust");
+
+ const { panel } = await initPerformanceInTab({ tab: target.localTab });
+ const { PerformanceController, WaterfallView } = panel.panelWin;
+
+ await waitUntil(() => PerformanceController.getRecordings().length == 1);
+ await waitUntil(() => WaterfallView.wasRenderedAtLeastOnce);
+
+ const recordings = PerformanceController.getRecordings();
+ is(recordings.length, 1, "One recording found in the performance panel.");
+ is(recordings[0].isConsole(), true, "Recording came from console.profile.");
+ is(recordings[0].getLabel(), "rust", "Correct label in the recording model.");
+
+ const selected = getSelectedRecording(panel);
+
+ is(
+ selected,
+ recordings[0],
+ "The profile from console should be selected as it's the only one."
+ );
+ is(
+ selected.getLabel(),
+ "rust",
+ "The profile label for the first recording is correct."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-console-record-02.js b/devtools/client/performance/test/browser_perf-console-record-02.js
new file mode 100644
index 0000000000..4e6f590bef
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-console-record-02.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the profiler is populated by in-progress console recordings
+ * when it is opened.
+ */
+
+const { Constants } = require("devtools/client/performance/modules/constants");
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInTab,
+ initConsoleInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ waitForRecordingStoppedEvents,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ waitUntil,
+} = require("devtools/client/performance/test/helpers/wait-utils");
+const {
+ times,
+} = require("devtools/client/performance/test/helpers/event-utils");
+const {
+ getSelectedRecording,
+} = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(async function() {
+ const { target, console } = await initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ await console.profile("rust");
+ await console.profile("rust2");
+
+ const { panel } = await initPerformanceInTab({ tab: target.localTab });
+ const { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
+
+ await waitUntil(() => PerformanceController.getRecordings().length == 2);
+
+ const recordings = PerformanceController.getRecordings();
+ is(recordings.length, 2, "Two recordings found in the performance panel.");
+ is(
+ recordings[0].isConsole(),
+ true,
+ "Recording came from console.profile (1)."
+ );
+ is(
+ recordings[0].getLabel(),
+ "rust",
+ "Correct label in the recording model (1)."
+ );
+ is(recordings[0].isRecording(), true, "Recording is still recording (1).");
+ is(
+ recordings[1].isConsole(),
+ true,
+ "Recording came from console.profile (2)."
+ );
+ is(
+ recordings[1].getLabel(),
+ "rust2",
+ "Correct label in the recording model (2)."
+ );
+ is(recordings[1].isRecording(), true, "Recording is still recording (2).");
+
+ const selected = getSelectedRecording(panel);
+ is(
+ selected,
+ recordings[0],
+ "The first console recording should be selected."
+ );
+ is(
+ selected.getLabel(),
+ "rust",
+ "The profile label for the first recording is correct."
+ );
+
+ // Ensure overview is still rendering.
+ await times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
+ expectedArgs: [Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL],
+ });
+
+ let stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ });
+ await console.profileEnd("rust");
+ await stopped;
+
+ stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when a finished recording is selected
+ skipWaitingForOverview: true,
+ skipWaitingForSubview: true,
+ });
+ await console.profileEnd("rust2");
+ await stopped;
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-console-record-03.js b/devtools/client/performance/test/browser_perf-console-record-03.js
new file mode 100644
index 0000000000..287de27f5b
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-console-record-03.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the profiler is populated by in-progress console recordings, and
+ * also console recordings that have finished before it was opened.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInTab,
+ initConsoleInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ waitForRecordingStoppedEvents,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ waitUntil,
+} = require("devtools/client/performance/test/helpers/wait-utils");
+const {
+ getSelectedRecording,
+} = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(async function() {
+ const { target, console } = await initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ await console.profile("rust");
+ await console.profileEnd("rust");
+ await console.profile("rust2");
+
+ const { panel } = await initPerformanceInTab({ tab: target.localTab });
+ const { PerformanceController, WaterfallView } = panel.panelWin;
+
+ await waitUntil(() => PerformanceController.getRecordings().length == 2);
+ await waitUntil(() => WaterfallView.wasRenderedAtLeastOnce);
+
+ const recordings = PerformanceController.getRecordings();
+ is(recordings.length, 2, "Two recordings found in the performance panel.");
+ is(
+ recordings[0].isConsole(),
+ true,
+ "Recording came from console.profile (1)."
+ );
+ is(
+ recordings[0].getLabel(),
+ "rust",
+ "Correct label in the recording model (1)."
+ );
+ is(recordings[0].isRecording(), false, "Recording is still recording (1).");
+ is(
+ recordings[1].isConsole(),
+ true,
+ "Recording came from console.profile (2)."
+ );
+ is(
+ recordings[1].getLabel(),
+ "rust2",
+ "Correct label in the recording model (2)."
+ );
+ is(recordings[1].isRecording(), true, "Recording is still recording (2).");
+
+ const selected = getSelectedRecording(panel);
+ is(
+ selected,
+ recordings[0],
+ "The first console recording should be selected."
+ );
+ is(
+ selected.getLabel(),
+ "rust",
+ "The profile label for the first recording is correct."
+ );
+
+ const stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when a finished recording is selected
+ skipWaitingForOverview: true,
+ skipWaitingForSubview: true,
+ });
+ await console.profileEnd("rust2");
+ await stopped;
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-console-record-04.js b/devtools/client/performance/test/browser_perf-console-record-04.js
new file mode 100644
index 0000000000..48c78d5273
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-console-record-04.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the profiler can handle creation and stopping of console profiles
+ * after being opened.
+ */
+
+const { Constants } = require("devtools/client/performance/modules/constants");
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInTab,
+ initConsoleInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ waitForRecordingStartedEvents,
+ waitForRecordingStoppedEvents,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ times,
+} = require("devtools/client/performance/test/helpers/event-utils");
+const {
+ getSelectedRecording,
+} = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(async function() {
+ const { target, console } = await initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { panel } = await initPerformanceInTab({ tab: target.localTab });
+ const { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
+
+ const started = waitForRecordingStartedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ });
+ await console.profile("rust");
+ await started;
+
+ const recordings = PerformanceController.getRecordings();
+ is(recordings.length, 1, "One recording found in the performance panel.");
+ is(recordings[0].isConsole(), true, "Recording came from console.profile.");
+ is(recordings[0].getLabel(), "rust", "Correct label in the recording model.");
+ is(recordings[0].isRecording(), true, "Recording is still recording.");
+
+ const selected = getSelectedRecording(panel);
+ is(
+ selected,
+ recordings[0],
+ "The profile from console should be selected as it's the only one."
+ );
+ is(
+ selected.getLabel(),
+ "rust",
+ "The profile label for the first recording is correct."
+ );
+
+ // Ensure overview is still rendering.
+ await times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
+ expectedArgs: [Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL],
+ });
+
+ const stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ });
+ await console.profileEnd("rust");
+ await stopped;
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-console-record-05.js b/devtools/client/performance/test/browser_perf-console-record-05.js
new file mode 100644
index 0000000000..41cfdf63d8
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-console-record-05.js
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that multiple recordings with the same label (non-overlapping) appear
+ * in the recording list.
+ */
+
+const { Constants } = require("devtools/client/performance/modules/constants");
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInTab,
+ initConsoleInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ waitForRecordingStartedEvents,
+ waitForRecordingStoppedEvents,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ times,
+} = require("devtools/client/performance/test/helpers/event-utils");
+const {
+ getSelectedRecording,
+} = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(async function() {
+ const { target, console } = await initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { panel } = await initPerformanceInTab({ tab: target.localTab });
+ const { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
+
+ let started = waitForRecordingStartedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ });
+ await console.profile("rust");
+ await started;
+
+ let recordings = PerformanceController.getRecordings();
+ is(recordings.length, 1, "One recording found in the performance panel.");
+ is(
+ recordings[0].isConsole(),
+ true,
+ "Recording came from console.profile (1)."
+ );
+ is(
+ recordings[0].getLabel(),
+ "rust",
+ "Correct label in the recording model (1)."
+ );
+ is(recordings[0].isRecording(), true, "Recording is still recording (1).");
+
+ let selected = getSelectedRecording(panel);
+ is(
+ selected,
+ recordings[0],
+ "The profile from console should be selected as it's the only one."
+ );
+ is(
+ selected.getLabel(),
+ "rust",
+ "The profile label for the first recording is correct."
+ );
+
+ // Ensure overview is still rendering.
+ await times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
+ expectedArgs: [Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL],
+ });
+
+ let stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ });
+ await console.profileEnd("rust");
+ await stopped;
+
+ started = waitForRecordingStartedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when an in-progress recording is selected
+ skipWaitingForOverview: true,
+ // the view state won't switch to "console-recording" unless the new
+ // in-progress recording is selected, which won't happen
+ skipWaitingForViewState: true,
+ });
+ await console.profile("rust");
+ await started;
+
+ recordings = PerformanceController.getRecordings();
+ is(recordings.length, 2, "Two recordings found in the performance panel.");
+ is(
+ recordings[1].isConsole(),
+ true,
+ "Recording came from console.profile (2)."
+ );
+ is(
+ recordings[1].getLabel(),
+ "rust",
+ "Correct label in the recording model (2)."
+ );
+ is(recordings[1].isRecording(), true, "Recording is still recording (2).");
+
+ selected = getSelectedRecording(panel);
+ is(
+ selected,
+ recordings[0],
+ "The profile from console should still be selected"
+ );
+ is(
+ selected.getLabel(),
+ "rust",
+ "The profile label for the first recording is correct."
+ );
+
+ stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when a finished recording is selected
+ skipWaitingForOverview: true,
+ skipWaitingForSubview: true,
+ });
+ await console.profileEnd("rust");
+ await stopped;
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-console-record-06.js b/devtools/client/performance/test/browser_perf-console-record-06.js
new file mode 100644
index 0000000000..f2bc8ea458
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-console-record-06.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that console recordings can overlap (not completely nested).
+ */
+
+const { Constants } = require("devtools/client/performance/modules/constants");
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInTab,
+ initConsoleInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ waitForRecordingStartedEvents,
+ waitForRecordingStoppedEvents,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ times,
+} = require("devtools/client/performance/test/helpers/event-utils");
+const {
+ getSelectedRecording,
+} = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(async function() {
+ const { target, console } = await initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { panel } = await initPerformanceInTab({ tab: target.localTab });
+ const { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
+
+ let started = waitForRecordingStartedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ });
+ await console.profile("rust");
+ await started;
+
+ let recordings = PerformanceController.getRecordings();
+ is(recordings.length, 1, "A recording found in the performance panel.");
+ is(
+ getSelectedRecording(panel),
+ recordings[0],
+ "The first console recording should be selected."
+ );
+
+ // Ensure overview is still rendering.
+ await times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
+ expectedArgs: [Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL],
+ });
+
+ started = waitForRecordingStartedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when an in-progress recording is selected
+ skipWaitingForOverview: true,
+ // the view state won't switch to "console-recording" unless the new
+ // in-progress recording is selected, which won't happen
+ skipWaitingForViewState: true,
+ });
+ await console.profile("golang");
+ await started;
+
+ recordings = PerformanceController.getRecordings();
+ is(recordings.length, 2, "Two recordings found in the performance panel.");
+ is(
+ getSelectedRecording(panel),
+ recordings[0],
+ "The first console recording should still be selected."
+ );
+
+ // Ensure overview is still rendering.
+ await times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
+ expectedArgs: [Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL],
+ });
+
+ let stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ });
+ await console.profileEnd("rust");
+ await stopped;
+
+ recordings = PerformanceController.getRecordings();
+ is(recordings.length, 2, "Two recordings found in the performance panel.");
+ is(
+ getSelectedRecording(panel),
+ recordings[0],
+ "The first console recording should still be selected."
+ );
+ is(
+ recordings[0].isRecording(),
+ false,
+ "The first console recording should no longer be recording."
+ );
+
+ stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when a finished recording is selected
+ skipWaitingForOverview: true,
+ skipWaitingForSubview: true,
+ });
+ await console.profileEnd("golang");
+ await stopped;
+
+ recordings = PerformanceController.getRecordings();
+ is(recordings.length, 2, "Two recordings found in the performance panel.");
+ is(
+ getSelectedRecording(panel),
+ recordings[0],
+ "The first console recording should still be selected."
+ );
+ is(
+ recordings[1].isRecording(),
+ false,
+ "The second console recording should no longer be recording."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-console-record-07.js b/devtools/client/performance/test/browser_perf-console-record-07.js
new file mode 100644
index 0000000000..16fc0de781
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-console-record-07.js
@@ -0,0 +1,247 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that a call to console.profileEnd() with no label ends the
+ * most recent console recording, and console.profileEnd() with a label that
+ * does not match any pending recordings does nothing.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInTab,
+ initConsoleInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ waitForRecordingStartedEvents,
+ waitForRecordingStoppedEvents,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ idleWait,
+} = require("devtools/client/performance/test/helpers/wait-utils");
+const {
+ getSelectedRecording,
+} = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(async function() {
+ const { target, console } = await initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { panel } = await initPerformanceInTab({ tab: target.localTab });
+ const { PerformanceController } = panel.panelWin;
+
+ let started = waitForRecordingStartedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ });
+ await console.profile();
+ await started;
+
+ started = waitForRecordingStartedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when an in-progress recording is selected
+ skipWaitingForOverview: true,
+ // the view state won't switch to "console-recording" unless the new
+ // in-progress recording is selected, which won't happen
+ skipWaitingForViewState: true,
+ });
+ await console.profile("1");
+ await started;
+
+ started = waitForRecordingStartedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when an in-progress recording is selected
+ skipWaitingForOverview: true,
+ // the view state won't switch to "console-recording" unless the new
+ // in-progress recording is selected, which won't happen
+ skipWaitingForViewState: true,
+ });
+ await console.profile("2");
+ await started;
+
+ let recordings = PerformanceController.getRecordings();
+ let selected = getSelectedRecording(panel);
+ is(recordings.length, 3, "Three recordings found in the performance panel.");
+ is(recordings[0].getLabel(), "", "Checking label of recording 1");
+ is(recordings[1].getLabel(), "1", "Checking label of recording 2");
+ is(recordings[2].getLabel(), "2", "Checking label of recording 3");
+ is(
+ selected,
+ recordings[0],
+ "The first console recording should be selected."
+ );
+
+ is(
+ recordings[0].isRecording(),
+ true,
+ "All recordings should now be started. (1)"
+ );
+ is(
+ recordings[1].isRecording(),
+ true,
+ "All recordings should now be started. (2)"
+ );
+ is(
+ recordings[2].isRecording(),
+ true,
+ "All recordings should now be started. (3)"
+ );
+
+ let stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when a finished recording is selected
+ skipWaitingForOverview: true,
+ skipWaitingForSubview: true,
+ // the view state won't switch to "recorded" unless the new
+ // finished recording is selected, which won't happen
+ skipWaitingForViewState: true,
+ });
+ await console.profileEnd();
+ await stopped;
+
+ selected = getSelectedRecording(panel);
+ recordings = PerformanceController.getRecordings();
+ is(recordings.length, 3, "Three recordings found in the performance panel.");
+ is(
+ selected,
+ recordings[0],
+ "The first console recording should still be selected."
+ );
+
+ is(
+ recordings[0].isRecording(),
+ true,
+ "The not most recent recording should not stop " +
+ "when calling console.profileEnd with no args."
+ );
+ is(
+ recordings[1].isRecording(),
+ true,
+ "The not most recent recording should not stop " +
+ "when calling console.profileEnd with no args."
+ );
+ is(
+ recordings[2].isRecording(),
+ false,
+ "Only the most recent recording should stop " +
+ "when calling console.profileEnd with no args."
+ );
+
+ info("Trying to `profileEnd` a non-existent console recording.");
+ console.profileEnd("fxos");
+ await idleWait(1000);
+
+ selected = getSelectedRecording(panel);
+ recordings = PerformanceController.getRecordings();
+ is(recordings.length, 3, "Three recordings found in the performance panel.");
+ is(
+ selected,
+ recordings[0],
+ "The first console recording should still be selected."
+ );
+
+ is(
+ recordings[0].isRecording(),
+ true,
+ "The first recording should not be ended yet."
+ );
+ is(
+ recordings[1].isRecording(),
+ true,
+ "The second recording should not be ended yet."
+ );
+ is(
+ recordings[2].isRecording(),
+ false,
+ "The third recording should still be ended."
+ );
+
+ stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when a finished recording is selected
+ skipWaitingForOverview: true,
+ skipWaitingForSubview: true,
+ // the view state won't switch to "recorded" unless the new
+ // finished recording is selected, which won't happen
+ skipWaitingForViewState: true,
+ });
+ await console.profileEnd();
+ await stopped;
+
+ selected = getSelectedRecording(panel);
+ recordings = PerformanceController.getRecordings();
+ is(recordings.length, 3, "Three recordings found in the performance panel.");
+ is(
+ selected,
+ recordings[0],
+ "The first console recording should still be selected."
+ );
+
+ is(
+ recordings[0].isRecording(),
+ true,
+ "The first recording should not be ended yet."
+ );
+ is(
+ recordings[1].isRecording(),
+ false,
+ "The second recording should not be ended yet."
+ );
+ is(
+ recordings[2].isRecording(),
+ false,
+ "The third recording should still be ended."
+ );
+
+ stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ });
+ await console.profileEnd();
+ await stopped;
+
+ selected = getSelectedRecording(panel);
+ recordings = PerformanceController.getRecordings();
+ is(recordings.length, 3, "Three recordings found in the performance panel.");
+ is(
+ selected,
+ recordings[0],
+ "The first console recording should be selected."
+ );
+
+ is(
+ recordings[0].isRecording(),
+ false,
+ "All recordings should now be ended. (1)"
+ );
+ is(
+ recordings[1].isRecording(),
+ false,
+ "All recordings should now be ended. (2)"
+ );
+ is(
+ recordings[2].isRecording(),
+ false,
+ "All recordings should now be ended. (3)"
+ );
+
+ info("Trying to `profileEnd` with no pending recordings.");
+ console.profileEnd();
+ await idleWait(1000);
+
+ ok(
+ true,
+ "Calling console.profileEnd() with no argument and no pending recordings " +
+ "does not throw."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-console-record-08.js b/devtools/client/performance/test/browser_perf-console-record-08.js
new file mode 100644
index 0000000000..9ac2f731f9
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-console-record-08.js
@@ -0,0 +1,311 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the profiler can correctly handle simultaneous console and manual
+ * recordings (via `console.profile` and clicking the record button).
+ */
+
+const { Constants } = require("devtools/client/performance/modules/constants");
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInTab,
+ initConsoleInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ waitForRecordingStartedEvents,
+ waitForRecordingStoppedEvents,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+ times,
+} = require("devtools/client/performance/test/helpers/event-utils");
+const {
+ setSelectedRecording,
+} = require("devtools/client/performance/test/helpers/recording-utils");
+
+/**
+ * The following are bit flag constants that are used to represent the state of a
+ * recording.
+ */
+
+// Represents a manually recorded profile, if a user hit the record button.
+const MANUAL = 0;
+// Represents a recorded profile from console.profile().
+const CONSOLE = 1;
+// Represents a profile that is currently recording.
+const RECORDING = 2;
+// Represents a profile that is currently selected.
+const SELECTED = 4;
+
+/**
+ * Utility function to provide a meaningful inteface for testing that the bits
+ * match for the recording state.
+ * @param {integer} expected - The expected bit values packed in an integer.
+ * @param {integer} actual - The actual bit values packed in an integer.
+ */
+function hasBitFlag(expected, actual) {
+ return !!(expected & actual);
+}
+
+add_task(async function() {
+ // This test seems to take a very long time to finish on Linux VMs.
+ requestLongerTimeout(4);
+
+ const { target, console } = await initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { panel } = await initPerformanceInTab({ tab: target.localTab });
+ const { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
+
+ info("Recording 1 - Starting console.profile()...");
+ let started = waitForRecordingStartedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ });
+ await console.profile("rust");
+ await started;
+ testRecordings(PerformanceController, [CONSOLE + SELECTED + RECORDING]);
+
+ info("Recording 2 - Starting manual recording...");
+ await startRecording(panel);
+ testRecordings(PerformanceController, [
+ CONSOLE + RECORDING,
+ MANUAL + RECORDING + SELECTED,
+ ]);
+
+ info('Recording 3 - Starting console.profile("3")...');
+ started = waitForRecordingStartedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when an in-progress recording is selected
+ skipWaitingForOverview: true,
+ // the view state won't switch to "console-recording" unless the new
+ // in-progress recording is selected, which won't happen
+ skipWaitingForViewState: true,
+ });
+ await console.profile("3");
+ await started;
+ testRecordings(PerformanceController, [
+ CONSOLE + RECORDING,
+ MANUAL + RECORDING + SELECTED,
+ CONSOLE + RECORDING,
+ ]);
+
+ info('Recording 4 - Starting console.profile("4")...');
+ started = waitForRecordingStartedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when an in-progress recording is selected
+ skipWaitingForOverview: true,
+ // the view state won't switch to "console-recording" unless the new
+ // in-progress recording is selected, which won't happen
+ skipWaitingForViewState: true,
+ });
+ await console.profile("4");
+ await started;
+ testRecordings(PerformanceController, [
+ CONSOLE + RECORDING,
+ MANUAL + RECORDING + SELECTED,
+ CONSOLE + RECORDING,
+ CONSOLE + RECORDING,
+ ]);
+
+ info("Recording 4 - Ending console.profileEnd()...");
+ let stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when a finished recording is selected
+ skipWaitingForOverview: true,
+ skipWaitingForSubview: true,
+ // the view state won't switch to "recorded" unless the new
+ // finished recording is selected, which won't happen
+ skipWaitingForViewState: true,
+ });
+ await console.profileEnd();
+ await stopped;
+ testRecordings(PerformanceController, [
+ CONSOLE + RECORDING,
+ MANUAL + RECORDING + SELECTED,
+ CONSOLE + RECORDING,
+ CONSOLE,
+ ]);
+
+ info("Recording 4 - Select last recording...");
+ let recordingSelected = once(
+ PerformanceController,
+ EVENTS.RECORDING_SELECTED
+ );
+ setSelectedRecording(panel, 3);
+ await recordingSelected;
+ testRecordings(PerformanceController, [
+ CONSOLE + RECORDING,
+ MANUAL + RECORDING,
+ CONSOLE + RECORDING,
+ CONSOLE + SELECTED,
+ ]);
+ ok(
+ !OverviewView.isRendering(),
+ "Stop rendering overview when a completed recording is selected."
+ );
+
+ info("Recording 2 - Stop manual recording.");
+
+ await stopRecording(panel);
+ testRecordings(PerformanceController, [
+ CONSOLE + RECORDING,
+ MANUAL + SELECTED,
+ CONSOLE + RECORDING,
+ CONSOLE,
+ ]);
+ ok(
+ !OverviewView.isRendering(),
+ "Stop rendering overview when a completed recording is selected."
+ );
+
+ info("Recording 1 - Select first recording.");
+ recordingSelected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+ setSelectedRecording(panel, 0);
+ await recordingSelected;
+ testRecordings(PerformanceController, [
+ CONSOLE + RECORDING + SELECTED,
+ MANUAL,
+ CONSOLE + RECORDING,
+ CONSOLE,
+ ]);
+ ok(
+ OverviewView.isRendering(),
+ "Should be rendering overview a recording in progress is selected."
+ );
+
+ // Ensure overview is still rendering.
+ await times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
+ expectedArgs: [Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL],
+ });
+
+ info("Ending console.profileEnd()...");
+ stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when a finished recording is selected
+ skipWaitingForOverview: true,
+ skipWaitingForSubview: true,
+ // the view state won't switch to "recorded" unless the new
+ // finished recording is selected, which won't happen
+ skipWaitingForViewState: true,
+ });
+ await console.profileEnd();
+ await stopped;
+ testRecordings(PerformanceController, [
+ CONSOLE + RECORDING + SELECTED,
+ MANUAL,
+ CONSOLE,
+ CONSOLE,
+ ]);
+ ok(
+ OverviewView.isRendering(),
+ "Should be rendering overview a recording in progress is selected."
+ );
+
+ // Ensure overview is still rendering.
+ await times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
+ expectedArgs: [Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL],
+ });
+
+ info("Recording 5 - Start one more manual recording.");
+ await startRecording(panel);
+ testRecordings(PerformanceController, [
+ CONSOLE + RECORDING,
+ MANUAL,
+ CONSOLE,
+ CONSOLE,
+ MANUAL + RECORDING + SELECTED,
+ ]);
+ ok(
+ OverviewView.isRendering(),
+ "Should be rendering overview a recording in progress is selected."
+ );
+
+ // Ensure overview is still rendering.
+ await times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
+ expectedArgs: [Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL],
+ });
+
+ info("Recording 5 - Stop manual recording.");
+ await stopRecording(panel);
+ testRecordings(PerformanceController, [
+ CONSOLE + RECORDING,
+ MANUAL,
+ CONSOLE,
+ CONSOLE,
+ MANUAL + SELECTED,
+ ]);
+ ok(
+ !OverviewView.isRendering(),
+ "Stop rendering overview when a completed recording is selected."
+ );
+
+ info("Recording 1 - Ending console.profileEnd()...");
+ stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when a finished recording is selected
+ skipWaitingForOverview: true,
+ skipWaitingForSubview: true,
+ // the view state won't switch to "recorded" unless the new
+ // in-progress recording is selected, which won't happen
+ skipWaitingForViewState: true,
+ });
+ await console.profileEnd();
+ await stopped;
+ testRecordings(PerformanceController, [
+ CONSOLE,
+ MANUAL,
+ CONSOLE,
+ CONSOLE,
+ MANUAL + SELECTED,
+ ]);
+ ok(
+ !OverviewView.isRendering(),
+ "Stop rendering overview when a completed recording is selected."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
+
+function testRecordings(controller, expectedBitFlags) {
+ const recordings = controller.getRecordings();
+ const current = controller.getCurrentRecording();
+ is(
+ recordings.length,
+ expectedBitFlags.length,
+ "Expected number of recordings."
+ );
+
+ recordings.forEach((recording, i) => {
+ const expected = expectedBitFlags[i];
+ is(
+ recording.isConsole(),
+ hasBitFlag(expected, CONSOLE),
+ `Recording ${i + 1} has expected console state.`
+ );
+ is(
+ recording.isRecording(),
+ hasBitFlag(expected, RECORDING),
+ `Recording ${i + 1} has expected console state.`
+ );
+ is(
+ recording == current,
+ hasBitFlag(expected, SELECTED),
+ `Recording ${i + 1} has expected selected state.`
+ );
+ });
+}
diff --git a/devtools/client/performance/test/browser_perf-console-record-09.js b/devtools/client/performance/test/browser_perf-console-record-09.js
new file mode 100644
index 0000000000..1f394fda7c
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-console-record-09.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that an error is not thrown when clearing out the recordings if there's
+ * an in-progress console profile and that console profiles are not cleared
+ * if in progress.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInTab,
+ initConsoleInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ waitForRecordingStartedEvents,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ idleWait,
+} = require("devtools/client/performance/test/helpers/wait-utils");
+
+add_task(async function() {
+ const { target, console } = await initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { panel } = await initPerformanceInTab({ tab: target.localTab });
+ const { PerformanceController } = panel.panelWin;
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ info("Starting console.profile()...");
+ const started = waitForRecordingStartedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when an in-progress recording is selected
+ skipWaitingForOverview: true,
+ // the view state won't switch to "console-recording" unless the new
+ // in-progress recording is selected, which won't happen
+ skipWaitingForViewState: true,
+ });
+ await console.profile();
+ await started;
+
+ await PerformanceController.clearRecordings();
+ let recordings = PerformanceController.getRecordings();
+ is(recordings.length, 1, "One recording found in the performance panel.");
+ is(recordings[0].isConsole(), true, "Recording came from console.profile.");
+ is(recordings[0].getLabel(), "", "Correct label in the recording model.");
+ is(
+ PerformanceController.getCurrentRecording(),
+ recordings[0],
+ "There current recording should be the first one."
+ );
+
+ info("Attempting to end console.profileEnd()...");
+ await console.profileEnd();
+ await idleWait(1000);
+
+ ok(
+ true,
+ "Stopping an in-progress console profile after clearing recordings does not throw."
+ );
+
+ await PerformanceController.clearRecordings();
+ recordings = PerformanceController.getRecordings();
+ is(recordings.length, 0, "No recordings found");
+ is(
+ PerformanceController.getCurrentRecording(),
+ null,
+ "There should be no current recording."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-details-01-toggle.js b/devtools/client/performance/test/browser_perf-details-01-toggle.js
new file mode 100644
index 0000000000..2d8e95b0f2
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-01-toggle.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the details view toggles subviews.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+const {
+ command,
+} = require("devtools/client/performance/test/helpers/input-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { EVENTS, $, DetailsView } = panel.panelWin;
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ info("Checking views on startup.");
+ checkViews(DetailsView, $, "waterfall");
+
+ // Select calltree view.
+ let viewChanged = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED, {
+ spreadArgs: true,
+ });
+ command($("toolbarbutton[data-view='js-calltree']"));
+ let [viewName] = await viewChanged;
+ is(viewName, "js-calltree", "UI_DETAILS_VIEW_SELECTED fired with view name");
+ checkViews(DetailsView, $, "js-calltree");
+
+ // Select js flamegraph view.
+ viewChanged = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED, {
+ spreadArgs: true,
+ });
+ command($("toolbarbutton[data-view='js-flamegraph']"));
+ [viewName] = await viewChanged;
+ is(
+ viewName,
+ "js-flamegraph",
+ "UI_DETAILS_VIEW_SELECTED fired with view name"
+ );
+ checkViews(DetailsView, $, "js-flamegraph");
+
+ // Select waterfall view.
+ viewChanged = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED, {
+ spreadArgs: true,
+ });
+ command($("toolbarbutton[data-view='waterfall']"));
+ [viewName] = await viewChanged;
+ is(viewName, "waterfall", "UI_DETAILS_VIEW_SELECTED fired with view name");
+ checkViews(DetailsView, $, "waterfall");
+
+ await teardownToolboxAndRemoveTab(panel);
+});
+
+function checkViews(DetailsView, $, currentView) {
+ for (const viewName in DetailsView.components) {
+ const button = $(`toolbarbutton[data-view="${viewName}"]`);
+
+ is(
+ DetailsView.el.selectedPanel.id,
+ DetailsView.components[currentView].id,
+ `DetailsView correctly has ${currentView} selected.`
+ );
+
+ if (viewName == currentView) {
+ ok(button.getAttribute("checked"), `${viewName} button checked.`);
+ } else {
+ ok(!button.getAttribute("checked"), `${viewName} button not checked.`);
+ }
+ }
+}
diff --git a/devtools/client/performance/test/browser_perf-details-02-utility-fun.js b/devtools/client/performance/test/browser_perf-details-02-utility-fun.js
new file mode 100644
index 0000000000..79a516b9aa
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-02-utility-fun.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the details view utility functions work as advertised.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const {
+ EVENTS,
+ DetailsView,
+ WaterfallView,
+ JsCallTreeView,
+ JsFlameGraphView,
+ } = panel.panelWin;
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ ok(
+ DetailsView.isViewSelected(WaterfallView),
+ "The waterfall view is selected by default in the details view."
+ );
+
+ // Select js calltree view.
+ let selected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
+ await DetailsView.selectView("js-calltree");
+ await selected;
+
+ ok(
+ DetailsView.isViewSelected(JsCallTreeView),
+ "The js calltree view is now selected in the details view."
+ );
+
+ // Select js flamegraph view.
+ selected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
+ await DetailsView.selectView("js-flamegraph");
+ await selected;
+
+ ok(
+ DetailsView.isViewSelected(JsFlameGraphView),
+ "The js flamegraph view is now selected in the details view."
+ );
+
+ // Select waterfall view.
+ selected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
+ await DetailsView.selectView("waterfall");
+ await selected;
+
+ ok(
+ DetailsView.isViewSelected(WaterfallView),
+ "The waterfall view is now selected in the details view."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-details-03-without-allocations.js b/devtools/client/performance/test/browser_perf-details-03-without-allocations.js
new file mode 100644
index 0000000000..c76cde4c8e
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-03-without-allocations.js
@@ -0,0 +1,176 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the details view hides the allocations buttons when a recording
+ * does not have allocations data ("withAllocations": false), and that when an
+ * allocations panel is selected to a panel that does not have allocations goes
+ * to a default panel instead.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ UI_ENABLE_ALLOCATIONS_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+const {
+ setSelectedRecording,
+} = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const {
+ EVENTS,
+ $,
+ DetailsView,
+ WaterfallView,
+ MemoryCallTreeView,
+ MemoryFlameGraphView,
+ } = panel.panelWin;
+
+ const flameBtn = $("toolbarbutton[data-view='memory-flamegraph']");
+ const callBtn = $("toolbarbutton[data-view='memory-calltree']");
+
+ // Disable allocations to prevent recording them.
+ Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, false);
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ ok(
+ DetailsView.isViewSelected(WaterfallView),
+ "The waterfall view is selected by default in the details view."
+ );
+
+ // Re-enable allocations to test.
+ Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
+
+ // The toolbar buttons will always be hidden when a recording isn't available,
+ // so make sure we have one that's finished.
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ ok(
+ DetailsView.isViewSelected(WaterfallView),
+ "The waterfall view is still selected in the details view."
+ );
+
+ is(
+ callBtn.hidden,
+ false,
+ "The `memory-calltree` button is shown when recording has memory data."
+ );
+ is(
+ flameBtn.hidden,
+ false,
+ "The `memory-flamegraph` button is shown when recording has memory data."
+ );
+
+ let selected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
+ let rendered = once(MemoryCallTreeView, EVENTS.UI_MEMORY_CALL_TREE_RENDERED);
+ DetailsView.selectView("memory-calltree");
+ await selected;
+ await rendered;
+
+ ok(
+ DetailsView.isViewSelected(MemoryCallTreeView),
+ "The memory call tree view can now be selected."
+ );
+
+ selected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
+ rendered = once(MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
+ DetailsView.selectView("memory-flamegraph");
+ await selected;
+ await rendered;
+
+ ok(
+ DetailsView.isViewSelected(MemoryFlameGraphView),
+ "The memory flamegraph view can now be selected."
+ );
+
+ // Select the first recording with no memory data.
+ selected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
+ rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ setSelectedRecording(panel, 0);
+ await selected;
+ await rendered;
+
+ ok(
+ DetailsView.isViewSelected(WaterfallView),
+ "The waterfall view is now selected " +
+ "when switching back to a recording that does not have memory data."
+ );
+
+ is(
+ callBtn.hidden,
+ true,
+ "The `memory-calltree` button is hidden when recording has no memory data."
+ );
+ is(
+ flameBtn.hidden,
+ true,
+ "The `memory-flamegraph` button is hidden when recording has no memory data."
+ );
+
+ // Go back to the recording with memory data.
+ rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ setSelectedRecording(panel, 1);
+ await rendered;
+
+ ok(
+ DetailsView.isViewSelected(WaterfallView),
+ "The waterfall view is still selected in the details view."
+ );
+
+ is(
+ callBtn.hidden,
+ false,
+ "The `memory-calltree` button is shown when recording has memory data."
+ );
+ is(
+ flameBtn.hidden,
+ false,
+ "The `memory-flamegraph` button is shown when recording has memory data."
+ );
+
+ selected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
+ rendered = once(MemoryCallTreeView, EVENTS.UI_MEMORY_CALL_TREE_RENDERED);
+ DetailsView.selectView("memory-calltree");
+ await selected;
+ await rendered;
+
+ ok(
+ DetailsView.isViewSelected(MemoryCallTreeView),
+ "The memory call tree view can be " +
+ "selected again after going back to the view with memory data."
+ );
+
+ selected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
+ rendered = once(MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
+ DetailsView.selectView("memory-flamegraph");
+ await selected;
+ await rendered;
+
+ ok(
+ DetailsView.isViewSelected(MemoryFlameGraphView),
+ "The memory flamegraph view can " +
+ "be selected again after going back to the view with memory data."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-details-04-toolbar-buttons.js b/devtools/client/performance/test/browser_perf-details-04-toolbar-buttons.js
new file mode 100644
index 0000000000..34784ea7bd
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-04-toolbar-buttons.js
@@ -0,0 +1,253 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the details view hides the toolbar buttons when a recording
+ * doesn't exist or is in progress.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+const {
+ setSelectedRecording,
+ getSelectedRecordingIndex,
+} = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { EVENTS, $, PerformanceController, WaterfallView } = panel.panelWin;
+
+ const waterfallBtn = $("toolbarbutton[data-view='waterfall']");
+ const jsFlameBtn = $("toolbarbutton[data-view='js-flamegraph']");
+ const jsCallBtn = $("toolbarbutton[data-view='js-calltree']");
+ const memFlameBtn = $("toolbarbutton[data-view='memory-flamegraph']");
+ const memCallBtn = $("toolbarbutton[data-view='memory-calltree']");
+
+ is(
+ waterfallBtn.hidden,
+ true,
+ "The `waterfall` button is hidden when tool starts."
+ );
+ is(
+ jsFlameBtn.hidden,
+ true,
+ "The `js-flamegraph` button is hidden when tool starts."
+ );
+ is(
+ jsCallBtn.hidden,
+ true,
+ "The `js-calltree` button is hidden when tool starts."
+ );
+ is(
+ memFlameBtn.hidden,
+ true,
+ "The `memory-flamegraph` button is hidden when tool starts."
+ );
+ is(
+ memCallBtn.hidden,
+ true,
+ "The `memory-calltree` button is hidden when tool starts."
+ );
+
+ await startRecording(panel);
+
+ is(
+ waterfallBtn.hidden,
+ true,
+ "The `waterfall` button is hidden when recording starts."
+ );
+ is(
+ jsFlameBtn.hidden,
+ true,
+ "The `js-flamegraph` button is hidden when recording starts."
+ );
+ is(
+ jsCallBtn.hidden,
+ true,
+ "The `js-calltree` button is hidden when recording starts."
+ );
+ is(
+ memFlameBtn.hidden,
+ true,
+ "The `memory-flamegraph` button is hidden when recording starts."
+ );
+ is(
+ memCallBtn.hidden,
+ true,
+ "The `memory-calltree` button is hidden when recording starts."
+ );
+
+ await stopRecording(panel);
+
+ is(
+ waterfallBtn.hidden,
+ false,
+ "The `waterfall` button is visible when recording ends."
+ );
+ is(
+ jsFlameBtn.hidden,
+ false,
+ "The `js-flamegraph` button is visible when recording ends."
+ );
+ is(
+ jsCallBtn.hidden,
+ false,
+ "The `js-calltree` button is visible when recording ends."
+ );
+ is(
+ memFlameBtn.hidden,
+ true,
+ "The `memory-flamegraph` button is hidden when recording ends."
+ );
+ is(
+ memCallBtn.hidden,
+ true,
+ "The `memory-calltree` button is hidden when recording ends."
+ );
+
+ await startRecording(panel);
+
+ is(
+ waterfallBtn.hidden,
+ true,
+ "The `waterfall` button is hidden when another recording starts."
+ );
+ is(
+ jsFlameBtn.hidden,
+ true,
+ "The `js-flamegraph` button is hidden when another recording starts."
+ );
+ is(
+ jsCallBtn.hidden,
+ true,
+ "The `js-calltree` button is hidden when another recording starts."
+ );
+ is(
+ memFlameBtn.hidden,
+ true,
+ "The `memory-flamegraph` button is hidden when another recording starts."
+ );
+ is(
+ memCallBtn.hidden,
+ true,
+ "The `memory-calltree` button is hidden when another recording starts."
+ );
+
+ let selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+ let rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ setSelectedRecording(panel, 0);
+ await selected;
+ await rendered;
+
+ let selectedIndex = getSelectedRecordingIndex(panel);
+ is(selectedIndex, 0, "The first recording was selected again.");
+
+ is(
+ waterfallBtn.hidden,
+ false,
+ "The `waterfall` button is visible when first recording selected."
+ );
+ is(
+ jsFlameBtn.hidden,
+ false,
+ "The `js-flamegraph` button is visible when first recording selected."
+ );
+ is(
+ jsCallBtn.hidden,
+ false,
+ "The `js-calltree` button is visible when first recording selected."
+ );
+ is(
+ memFlameBtn.hidden,
+ true,
+ "The `memory-flamegraph` button is hidden when first recording selected."
+ );
+ is(
+ memCallBtn.hidden,
+ true,
+ "The `memory-calltree` button is hidden when first recording selected."
+ );
+
+ selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+ setSelectedRecording(panel, 1);
+ await selected;
+
+ selectedIndex = getSelectedRecordingIndex(panel);
+ is(selectedIndex, 1, "The second recording was selected again.");
+
+ is(
+ waterfallBtn.hidden,
+ true,
+ "The `waterfall button` still is hidden when second recording selected."
+ );
+ is(
+ jsFlameBtn.hidden,
+ true,
+ "The `js-flamegraph button` still is hidden when second recording selected."
+ );
+ is(
+ jsCallBtn.hidden,
+ true,
+ "The `js-calltree button` still is hidden when second recording selected."
+ );
+ is(
+ memFlameBtn.hidden,
+ true,
+ "The `memory-flamegraph button` still is hidden when second recording selected."
+ );
+ is(
+ memCallBtn.hidden,
+ true,
+ "The `memory-calltree button` still is hidden when second recording selected."
+ );
+
+ rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ await stopRecording(panel);
+ await rendered;
+
+ selectedIndex = getSelectedRecordingIndex(panel);
+ is(selectedIndex, 1, "The second recording is still selected.");
+
+ is(
+ waterfallBtn.hidden,
+ false,
+ "The `waterfall` button is visible when second recording finished."
+ );
+ is(
+ jsFlameBtn.hidden,
+ false,
+ "The `js-flamegraph` button is visible when second recording finished."
+ );
+ is(
+ jsCallBtn.hidden,
+ false,
+ "The `js-calltree` button is visible when second recording finished."
+ );
+ is(
+ memFlameBtn.hidden,
+ true,
+ "The `memory-flamegraph` button is hidden when second recording finished."
+ );
+ is(
+ memCallBtn.hidden,
+ true,
+ "The `memory-calltree` button is hidden when second recording finished."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-details-05-preserve-view.js b/devtools/client/performance/test/browser_perf-details-05-preserve-view.js
new file mode 100644
index 0000000000..515f405d9e
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-05-preserve-view.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the same details view is selected after recordings are cleared
+ * and a new recording starts.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const {
+ EVENTS,
+ PerformanceController,
+ DetailsView,
+ JsCallTreeView,
+ } = panel.panelWin;
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ const selected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
+ const rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ await DetailsView.selectView("js-calltree");
+ await selected;
+ await rendered;
+
+ ok(
+ DetailsView.isViewSelected(JsCallTreeView),
+ "The js calltree view is now selected in the details view."
+ );
+
+ const cleared = once(PerformanceController, EVENTS.RECORDING_SELECTED, {
+ expectedArgs: [null],
+ });
+ await PerformanceController.clearRecordings();
+ await cleared;
+
+ await startRecording(panel);
+ await stopRecording(panel, {
+ expectedViewClass: "JsCallTreeView",
+ expectedViewEvent: "UI_JS_CALL_TREE_RENDERED",
+ });
+
+ ok(
+ DetailsView.isViewSelected(JsCallTreeView),
+ "The js calltree view is still selected in the details view."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-details-06-rerender-on-selection.js b/devtools/client/performance/test/browser_perf-details-06-rerender-on-selection.js
new file mode 100644
index 0000000000..d60afb9eab
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-06-rerender-on-selection.js
@@ -0,0 +1,105 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that when flame chart views scroll to change selection,
+ * other detail views are rerendered.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+const {
+ scrollCanvasGraph,
+ HORIZONTAL_AXIS,
+} = require("devtools/client/performance/test/helpers/input-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const {
+ EVENTS,
+ OverviewView,
+ DetailsView,
+ WaterfallView,
+ JsCallTreeView,
+ JsFlameGraphView,
+ } = panel.panelWin;
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ const waterfallRendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ OverviewView.setTimeInterval({ startTime: 10, endTime: 20 });
+ await waterfallRendered;
+
+ // Select the call tree to make sure it's initialized and ready to receive
+ // redrawing requests once reselected.
+ const callTreeRendered = once(
+ JsCallTreeView,
+ EVENTS.UI_JS_CALL_TREE_RENDERED
+ );
+ await DetailsView.selectView("js-calltree");
+ await callTreeRendered;
+
+ // Switch to the flamegraph and perform a scroll over the visualization.
+ // The waterfall and call tree should get rerendered when reselected.
+ const flamegraphRendered = once(
+ JsFlameGraphView,
+ EVENTS.UI_JS_FLAMEGRAPH_RENDERED
+ );
+ await DetailsView.selectView("js-flamegraph");
+ await flamegraphRendered;
+
+ const overviewRangeSelected = once(
+ OverviewView,
+ EVENTS.UI_OVERVIEW_RANGE_SELECTED
+ );
+ const waterfallRerendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ const callTreeRerendered = once(
+ JsCallTreeView,
+ EVENTS.UI_JS_CALL_TREE_RENDERED
+ );
+
+ once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED).then(() => {
+ ok(
+ false,
+ "FlameGraphView should not publicly rerender, the internal state " +
+ "and drawing should be handled by the underlying widget."
+ );
+ });
+
+ // Reset the range to full view, trigger a "selection" event as if
+ // our mouse has done this
+ scrollCanvasGraph(JsFlameGraphView.graph, {
+ axis: HORIZONTAL_AXIS,
+ wheel: 200,
+ x: 10,
+ });
+
+ await overviewRangeSelected;
+ ok(true, "Overview range was changed.");
+
+ await DetailsView.selectView("waterfall");
+ await waterfallRerendered;
+ ok(true, "Waterfall rerendered by flame graph changing interval.");
+
+ await DetailsView.selectView("js-calltree");
+ await callTreeRerendered;
+ ok(true, "CallTree rerendered by flame graph changing interval.");
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-details-07-bleed-events.js b/devtools/client/performance/test/browser_perf-details-07-bleed-events.js
new file mode 100644
index 0000000000..c5202666f1
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-07-bleed-events.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that events don't bleed between detail views.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { EVENTS, DetailsView, JsCallTreeView } = panel.panelWin;
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ // The waterfall should render by default, and we want to make
+ // sure that the render events don't bleed between detail views
+ // so test that's the case after both views have been created.
+ const callTreeRendered = once(
+ JsCallTreeView,
+ EVENTS.UI_JS_CALL_TREE_RENDERED
+ );
+ await DetailsView.selectView("js-calltree");
+ await callTreeRendered;
+
+ const waterfallSelected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
+ await DetailsView.selectView("waterfall");
+ await waterfallSelected;
+
+ once(JsCallTreeView, EVENTS.UI_WATERFALL_RENDERED).then(() =>
+ ok(false, "JsCallTreeView should not receive UI_WATERFALL_RENDERED event.")
+ );
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ const callTreeRerendered = once(
+ JsCallTreeView,
+ EVENTS.UI_JS_CALL_TREE_RENDERED
+ );
+ await DetailsView.selectView("js-calltree");
+ await callTreeRerendered;
+
+ ok(true, "Test passed.");
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-details-render-00-waterfall.js b/devtools/client/performance/test/browser_perf-details-render-00-waterfall.js
new file mode 100644
index 0000000000..8b73e8c06f
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-render-00-waterfall.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the waterfall view renders content after recording.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { DetailsView, WaterfallView } = panel.panelWin;
+
+ await startRecording(panel);
+ // Already waits for EVENTS.UI_WATERFALL_RENDERED.
+ await stopRecording(panel);
+
+ ok(
+ DetailsView.isViewSelected(WaterfallView),
+ "The waterfall view is selected by default in the details view."
+ );
+
+ ok(true, "WaterfallView rendered after recording is stopped.");
+
+ await startRecording(panel);
+ // Already waits for EVENTS.UI_WATERFALL_RENDERED.
+ await stopRecording(panel);
+
+ ok(
+ DetailsView.isViewSelected(WaterfallView),
+ "The waterfall view is still selected in the details view."
+ );
+
+ ok(
+ true,
+ "WaterfallView rendered again after recording completed a second time."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-details-render-01-js-calltree.js b/devtools/client/performance/test/browser_perf-details-render-01-js-calltree.js
new file mode 100644
index 0000000000..b74cd7c087
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-render-01-js-calltree.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the js call tree view renders content after recording.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { EVENTS, DetailsView, JsCallTreeView } = panel.panelWin;
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ const rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ await DetailsView.selectView("js-calltree");
+ await rendered;
+
+ ok(true, "JsCallTreeView rendered after recording is stopped.");
+
+ await startRecording(panel);
+ await stopRecording(panel, {
+ expectedViewClass: "JsCallTreeView",
+ expectedViewEvent: "UI_JS_CALL_TREE_RENDERED",
+ });
+
+ ok(
+ true,
+ "JsCallTreeView rendered again after recording completed a second time."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-details-render-02-js-flamegraph.js b/devtools/client/performance/test/browser_perf-details-render-02-js-flamegraph.js
new file mode 100644
index 0000000000..fef2be5da5
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-render-02-js-flamegraph.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the js flamegraph view renders content after recording.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { EVENTS, DetailsView, JsFlameGraphView } = panel.panelWin;
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ const rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ await DetailsView.selectView("js-flamegraph");
+ await rendered;
+
+ ok(true, "JsFlameGraphView rendered after recording is stopped.");
+
+ await startRecording(panel);
+ await stopRecording(panel, {
+ expectedViewClass: "JsFlameGraphView",
+ expectedViewEvent: "UI_JS_FLAMEGRAPH_RENDERED",
+ });
+
+ ok(
+ true,
+ "JsFlameGraphView rendered again after recording completed a second time."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-details-render-03-memory-calltree.js b/devtools/client/performance/test/browser_perf-details-render-03-memory-calltree.js
new file mode 100644
index 0000000000..7c58fb789e
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-render-03-memory-calltree.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the memory call tree view renders content after recording.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ UI_ENABLE_ALLOCATIONS_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { EVENTS, DetailsView, MemoryCallTreeView } = panel.panelWin;
+
+ // Enable allocations to test.
+ Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ const rendered = once(
+ MemoryCallTreeView,
+ EVENTS.UI_MEMORY_CALL_TREE_RENDERED
+ );
+ await DetailsView.selectView("memory-calltree");
+ await rendered;
+
+ ok(true, "MemoryCallTreeView rendered after recording is stopped.");
+
+ await startRecording(panel);
+ await stopRecording(panel, {
+ expectedViewClass: "MemoryCallTreeView",
+ expectedViewEvent: "UI_MEMORY_CALL_TREE_RENDERED",
+ });
+
+ ok(
+ true,
+ "MemoryCallTreeView rendered again after recording completed a second time."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-details-render-04-memory-flamegraph.js b/devtools/client/performance/test/browser_perf-details-render-04-memory-flamegraph.js
new file mode 100644
index 0000000000..7a44974326
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-render-04-memory-flamegraph.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the memory call tree view renders content after recording.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ UI_ENABLE_ALLOCATIONS_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { EVENTS, DetailsView, MemoryFlameGraphView } = panel.panelWin;
+
+ // Enable allocations to test.
+ Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ const rendered = once(
+ MemoryFlameGraphView,
+ EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED
+ );
+ await DetailsView.selectView("memory-flamegraph");
+ await rendered;
+
+ ok(true, "MemoryFlameGraphView rendered after recording is stopped.");
+
+ await startRecording(panel);
+ await stopRecording(panel, {
+ expectedViewClass: "MemoryFlameGraphView",
+ expectedViewEvent: "UI_MEMORY_FLAMEGRAPH_RENDERED",
+ });
+
+ ok(
+ true,
+ "MemoryFlameGraphView rendered again after recording completed a second time."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-docload.js b/devtools/client/performance/test/browser_perf-docload.js
new file mode 100644
index 0000000000..141248bcb6
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-docload.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the sidebar is updated with "DOMContentLoaded" and "load" markers.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+ reload,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ waitUntil,
+} = require("devtools/client/performance/test/helpers/wait-utils");
+
+add_task(async function() {
+ const { panel, target } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { PerformanceController } = panel.panelWin;
+
+ await startRecording(panel);
+ await reload(target);
+
+ await waitUntil(() => {
+ // Wait until we get the necessary markers.
+ const markers = PerformanceController.getCurrentRecording().getMarkers();
+ if (
+ !markers.some(m => m.name == "document::DOMContentLoaded") ||
+ !markers.some(m => m.name == "document::Load")
+ ) {
+ return false;
+ }
+
+ ok(
+ markers.filter(m => m.name == "document::DOMContentLoaded").length == 1,
+ "There should only be one `DOMContentLoaded` marker."
+ );
+ ok(
+ markers.filter(m => m.name == "document::Load").length == 1,
+ "There should only be one `load` marker."
+ );
+
+ return true;
+ });
+
+ await stopRecording(panel);
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-fission-switch-target.js b/devtools/client/performance/test/browser_perf-fission-switch-target.js
new file mode 100644
index 0000000000..4b93ad6cce
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-fission-switch-target.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Test behavior while target-switching.
+ */
+
+const {
+ MAIN_PROCESS_URL,
+ SIMPLE_URL: CONTENT_PROCESS_URL,
+} = require("devtools/client/performance/test/helpers/urls");
+const {
+ BrowserTestUtils,
+} = require("resource://testing-common/BrowserTestUtils.jsm");
+const {
+ addTab,
+} = require("devtools/client/performance/test/helpers/tab-utils");
+const {
+ initPerformanceInTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+
+add_task(async function() {
+ info("Open a page running on content process");
+ const tab = await addTab({
+ url: CONTENT_PROCESS_URL,
+ win: window,
+ });
+
+ info("Open the performance panel");
+ const { panel } = await initPerformanceInTab({ tab });
+ const { PerformanceController, PerformanceView, EVENTS } = panel.panelWin;
+
+ info("Start recording");
+ await startRecording(panel);
+ await PerformanceView.once(EVENTS.UI_RECORDING_PROFILER_STATUS_RENDERED);
+
+ info("Navigate to a page running on main process");
+ BrowserTestUtils.loadURI(tab.linkedBrowser, MAIN_PROCESS_URL);
+ await PerformanceView.once(EVENTS.UI_RECORDING_PROFILER_STATUS_RENDERED);
+
+ info("Return to a page running on content process again");
+ BrowserTestUtils.loadURI(tab.linkedBrowser, CONTENT_PROCESS_URL);
+ await PerformanceView.once(EVENTS.UI_RECORDING_PROFILER_STATUS_RENDERED);
+
+ info("Stop recording");
+ await stopRecording(panel);
+
+ const recordings = PerformanceController.getRecordings();
+ is(recordings.length, 3, "Have a record for every target-switching");
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-gc-snap.js b/devtools/client/performance/test/browser_perf-gc-snap.js
new file mode 100644
index 0000000000..9e42476b59
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-gc-snap.js
@@ -0,0 +1,146 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/* eslint-disable */
+/**
+ * Tests that the marker details on GC markers displays allocation
+ * buttons and snaps to the correct range
+ */
+async function spawnTest() {
+ let { panel } = await initPerformance(ALLOCS_URL);
+ let { $, $$, EVENTS, PerformanceController, OverviewView, DetailsView, WaterfallView, MemoryCallTreeView } = panel.panelWin;
+ let EPSILON = 0.00001;
+
+ Services.prefs.setBoolPref(ALLOCATIONS_PREF, true);
+
+ await startRecording(panel);
+ await idleWait(1000);
+ await stopRecording(panel);
+
+ injectGCMarkers(PerformanceController, WaterfallView);
+
+ // Select everything
+ let rendered = WaterfallView.once(EVENTS.UI_WATERFALL_RENDERED);
+ OverviewView.setTimeInterval({ startTime: 0, endTime: Number.MAX_VALUE });
+ await rendered;
+
+ let bars = $$(".waterfall-marker-bar");
+ let gcMarkers = PerformanceController.getCurrentRecording().getMarkers();
+ ok(gcMarkers.length === 9, "should have 9 GC markers");
+ ok(bars.length === 9, "should have 9 GC markers rendered");
+
+ /**
+ * Check when it's the second marker of the first GC cycle.
+ */
+
+ let targetMarker = gcMarkers[1];
+ let targetBar = bars[1];
+ info(`Clicking GC Marker of type ${targetMarker.causeName} ${targetMarker.start}:${targetMarker.end}`);
+ await EventUtils.sendMouseEvent({ type: "mousedown" }, targetBar);
+ let showAllocsButton;
+ // On slower machines this can not be found immediately?
+ await waitUntil(() => showAllocsButton = $("#waterfall-details .custom-button[type='show-allocations']"));
+ ok(showAllocsButton, "GC buttons when allocations are enabled");
+
+ rendered = once(MemoryCallTreeView, EVENTS.UI_MEMORY_CALL_TREE_RENDERED);
+ await EventUtils.sendMouseEvent({ type: "click" }, showAllocsButton);
+ await rendered;
+
+ is(OverviewView.getTimeInterval().startTime, 0, "When clicking first GC, should use 0 as start time");
+ within(OverviewView.getTimeInterval().endTime, targetMarker.start, EPSILON, "Correct end time range");
+
+ let duration = PerformanceController.getCurrentRecording().getDuration();
+ rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ OverviewView.setTimeInterval({ startTime: 0, endTime: duration });
+ await DetailsView.selectView("waterfall");
+ await rendered;
+
+ /**
+ * Check when there is a previous GC cycle
+ */
+
+ bars = $$(".waterfall-marker-bar");
+ targetMarker = gcMarkers[4];
+ targetBar = bars[4];
+
+ info(`Clicking GC Marker of type ${targetMarker.causeName} ${targetMarker.start}:${targetMarker.end}`);
+ await EventUtils.sendMouseEvent({ type: "mousedown" }, targetBar);
+ // On slower machines this can not be found immediately?
+ await waitUntil(() => showAllocsButton = $("#waterfall-details .custom-button[type='show-allocations']"));
+ ok(showAllocsButton, "GC buttons when allocations are enabled");
+
+ rendered = once(MemoryCallTreeView, EVENTS.UI_MEMORY_CALL_TREE_RENDERED);
+ await EventUtils.sendMouseEvent({ type: "click" }, showAllocsButton);
+ await rendered;
+
+ within(OverviewView.getTimeInterval().startTime, gcMarkers[2].end, EPSILON,
+ "selection start range is last marker from previous GC cycle.");
+ within(OverviewView.getTimeInterval().endTime, targetMarker.start, EPSILON,
+ "selection end range is current GC marker's start time");
+
+ /**
+ * Now with allocations disabled
+ */
+
+ // Reselect the entire recording -- due to bug 1196945, the new recording
+ // won't reset the selection
+ duration = PerformanceController.getCurrentRecording().getDuration();
+ rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ OverviewView.setTimeInterval({ startTime: 0, endTime: duration });
+ await rendered;
+
+ Services.prefs.setBoolPref(ALLOCATIONS_PREF, false);
+ await startRecording(panel);
+ rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ await stopRecording(panel);
+ await rendered;
+
+ injectGCMarkers(PerformanceController, WaterfallView);
+
+ // Select everything
+ rendered = WaterfallView.once(EVENTS.UI_WATERFALL_RENDERED);
+ OverviewView.setTimeInterval({ startTime: 0, endTime: Number.MAX_VALUE });
+ await rendered;
+
+ ok(true, "WaterfallView rendered after recording is stopped.");
+
+ bars = $$(".waterfall-marker-bar");
+ gcMarkers = PerformanceController.getCurrentRecording().getMarkers();
+
+ await EventUtils.sendMouseEvent({ type: "mousedown" }, bars[0]);
+ showAllocsButton = $("#waterfall-details .custom-button[type='show-allocations']");
+ ok(!showAllocsButton, "No GC buttons when allocations are disabled");
+
+
+ await teardown(panel);
+ finish();
+}
+
+function injectGCMarkers(controller, waterfall) {
+ // Push some fake GC markers into the recording
+ let realMarkers = controller.getCurrentRecording().getMarkers();
+ // Invalidate marker cache
+ waterfall._cache.delete(realMarkers);
+ realMarkers.length = 0;
+ for (let gcMarker of GC_MARKERS) {
+ realMarkers.push(gcMarker);
+ }
+}
+
+var GC_MARKERS = [
+ { causeName: "TOO_MUCH_MALLOC", cycle: 1 },
+ { causeName: "TOO_MUCH_MALLOC", cycle: 1 },
+ { causeName: "TOO_MUCH_MALLOC", cycle: 1 },
+ { causeName: "ALLOC_TRIGGER", cycle: 2 },
+ { causeName: "ALLOC_TRIGGER", cycle: 2 },
+ { causeName: "ALLOC_TRIGGER", cycle: 2 },
+ { causeName: "SET_NEW_DOCUMENT", cycle: 3 },
+ { causeName: "SET_NEW_DOCUMENT", cycle: 3 },
+ { causeName: "SET_NEW_DOCUMENT", cycle: 3 },
+].map((marker, i) => {
+ marker.name = "GarbageCollection";
+ marker.start = 50 + (i * 10);
+ marker.end = marker.start + 9;
+ return marker;
+});
+/* eslint-enable */
diff --git a/devtools/client/performance/test/browser_perf-highlighted.js b/devtools/client/performance/test/browser_perf-highlighted.js
new file mode 100644
index 0000000000..ad7e5333d8
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-highlighted.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the toolbox tab for performance is highlighted when recording,
+ * whether already loaded, or via console.profile with an unloaded performance tools.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInTab,
+ initConsoleInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ waitUntil,
+} = require("devtools/client/performance/test/helpers/wait-utils");
+
+add_task(async function() {
+ const { target, toolbox, console } = await initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const tab = toolbox.doc.getElementById("toolbox-tab-performance");
+
+ await console.profile("rust");
+ await waitUntil(() => tab.classList.contains("highlighted"));
+
+ ok(
+ tab.classList.contains("highlighted"),
+ "Performance tab is highlighted during " +
+ "recording from console.profile when unloaded."
+ );
+
+ await console.profileEnd("rust");
+ await waitUntil(() => !tab.classList.contains("highlighted"));
+
+ ok(
+ !tab.classList.contains("highlighted"),
+ "Performance tab is no longer highlighted when console.profile recording finishes."
+ );
+
+ const { panel } = await initPerformanceInTab({ tab: target.localTab });
+
+ await startRecording(panel);
+
+ ok(
+ tab.classList.contains("highlighted"),
+ "Performance tab is highlighted during recording while in performance tool."
+ );
+
+ await stopRecording(panel);
+
+ ok(
+ !tab.classList.contains("highlighted"),
+ "Performance tab is no longer highlighted when recording finishes."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-loading-01.js b/devtools/client/performance/test/browser_perf-loading-01.js
new file mode 100644
index 0000000000..02f97c3a64
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-loading-01.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the recordings view shows the right label while recording, after
+ * recording, and once the record has loaded.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+const {
+ getSelectedRecording,
+ getDurationLabelText,
+} = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { EVENTS, PerformanceController } = panel.panelWin;
+ const { L10N } = require("devtools/client/performance/modules/global");
+
+ await startRecording(panel);
+
+ is(
+ getDurationLabelText(panel, 0),
+ L10N.getStr("recordingsList.recordingLabel"),
+ "The duration node should show the 'recording' message while recording"
+ );
+
+ const recordingStopping = once(
+ PerformanceController,
+ EVENTS.RECORDING_STATE_CHANGE,
+ {
+ expectedArgs: ["recording-stopping"],
+ }
+ );
+ const recordingStopped = once(
+ PerformanceController,
+ EVENTS.RECORDING_STATE_CHANGE,
+ {
+ expectedArgs: ["recording-stopped"],
+ }
+ );
+ const everythingStopped = stopRecording(panel);
+
+ await recordingStopping;
+ is(
+ getDurationLabelText(panel, 0),
+ L10N.getStr("recordingsList.loadingLabel"),
+ "The duration node should show the 'loading' message while stopping"
+ );
+
+ await recordingStopped;
+ const selected = getSelectedRecording(panel);
+ is(
+ getDurationLabelText(panel, 0),
+ L10N.getFormatStr(
+ "recordingsList.durationLabel",
+ selected.getDuration().toFixed(0)
+ ),
+ "The duration node should show the duration after the record has stopped"
+ );
+
+ await everythingStopped;
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-loading-02.js b/devtools/client/performance/test/browser_perf-loading-02.js
new file mode 100644
index 0000000000..710e8b9e29
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-loading-02.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the details view is locked after recording has stopped and before
+ * the recording has finished loading.
+ * Also test that the details view isn't locked if the recording that is being
+ * stopped isn't the active one.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+const {
+ getSelectedRecordingIndex,
+ setSelectedRecording,
+} = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { EVENTS, $, PerformanceController } = panel.panelWin;
+ const detailsContainer = $("#details-pane-container");
+ const recordingNotice = $("#recording-notice");
+ const loadingNotice = $("#loading-notice");
+ const detailsPane = $("#details-pane");
+
+ await startRecording(panel);
+
+ is(
+ detailsContainer.selectedPanel,
+ recordingNotice,
+ "The recording-notice is shown while recording."
+ );
+
+ let recordingStopping = once(
+ PerformanceController,
+ EVENTS.RECORDING_STATE_CHANGE,
+ {
+ expectedArgs: ["recording-stopping"],
+ }
+ );
+ let recordingStopped = once(
+ PerformanceController,
+ EVENTS.RECORDING_STATE_CHANGE,
+ {
+ expectedArgs: ["recording-stopped"],
+ }
+ );
+ let everythingStopped = stopRecording(panel);
+
+ await recordingStopping;
+ is(
+ detailsContainer.selectedPanel,
+ loadingNotice,
+ "The loading-notice is shown while the record is stopping."
+ );
+
+ await recordingStopped;
+ is(
+ detailsContainer.selectedPanel,
+ detailsPane,
+ "The details panel is shown after the record has stopped."
+ );
+
+ await everythingStopped;
+ await startRecording(panel);
+
+ info("While the 2nd record is still going, switch to the first one.");
+ const recordingSelected = once(
+ PerformanceController,
+ EVENTS.RECORDING_SELECTED
+ );
+ setSelectedRecording(panel, 0);
+ await recordingSelected;
+
+ recordingStopping = once(
+ PerformanceController,
+ EVENTS.RECORDING_STATE_CHANGE,
+ {
+ expectedArgs: ["recording-stopping"],
+ }
+ );
+ recordingStopped = once(
+ PerformanceController,
+ EVENTS.RECORDING_STATE_CHANGE,
+ {
+ expectedArgs: ["recording-stopped"],
+ }
+ );
+ everythingStopped = stopRecording(panel);
+
+ await recordingStopping;
+ is(
+ detailsContainer.selectedPanel,
+ detailsPane,
+ "The details panel is still shown while the 2nd record is being stopped."
+ );
+ is(
+ getSelectedRecordingIndex(panel),
+ 0,
+ "The first record is still selected."
+ );
+
+ await recordingStopped;
+
+ is(
+ detailsContainer.selectedPanel,
+ detailsPane,
+ "The details panel is still shown after the 2nd record has stopped."
+ );
+ is(getSelectedRecordingIndex(panel), 1, "The second record is now selected.");
+
+ await everythingStopped;
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-marker-details.js b/devtools/client/performance/test/browser_perf-marker-details.js
new file mode 100644
index 0000000000..9f55497a10
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-marker-details.js
@@ -0,0 +1,143 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/* eslint-disable */
+/**
+ * Tests if the Marker Details view renders all properties expected
+ * for each marker.
+ */
+
+async function spawnTest() {
+ let { target, panel } = await initPerformance(MARKERS_URL);
+ let { $, $$, EVENTS, PerformanceController, OverviewView, WaterfallView } = panel.panelWin;
+
+ // Hijack the markers massaging part of creating the waterfall view,
+ // to prevent collapsing markers and allowing this test to verify
+ // everything individually. A better solution would be to just expand
+ // all markers first and then skip the meta nodes, but I'm lazy.
+ WaterfallView._prepareWaterfallTree = markers => {
+ return { submarkers: markers };
+ };
+
+ const MARKER_TYPES = [
+ "Styles", "Reflow", "ConsoleTime", "TimeStamp"
+ ];
+
+ await startRecording(panel);
+ ok(true, "Recording has started.");
+
+ await waitUntil(() => {
+ // Wait until we get all the different markers.
+ let markers = PerformanceController.getCurrentRecording().getMarkers();
+ return MARKER_TYPES.every(type => markers.some(m => m.name === type));
+ });
+
+ await stopRecording(panel);
+ ok(true, "Recording has ended.");
+
+ info("No need to select everything in the timeline.");
+ info("All the markers should be displayed by default.");
+
+ let bars = Array.prototype.filter.call($$(".waterfall-marker-bar"),
+ (bar) => MARKER_TYPES.includes(bar.getAttribute("type")));
+ let markers = PerformanceController.getCurrentRecording().getMarkers()
+ .filter(m => MARKER_TYPES.includes(m.name));
+
+ info(`Got ${bars.length} bars and ${markers.length} markers.`);
+ info("Markers types from datasrc: " + Array.from(markers, e => e.name));
+ info("Markers names from sidebar: " + Array.from(bars, e => e.parentNode.parentNode.querySelector(".waterfall-marker-name").getAttribute("value")));
+
+ ok(bars.length >= MARKER_TYPES.length, `Got at least ${MARKER_TYPES.length} markers (1)`);
+ ok(markers.length >= MARKER_TYPES.length, `Got at least ${MARKER_TYPES.length} markers (2)`);
+
+ // Sanity check that markers are in chronologically ascending order
+ markers.reduce((previous, m) => {
+ if (m.start <= previous) {
+ ok(false, "Markers are not in order");
+ info(markers);
+ }
+ return m.start;
+ }, 0);
+
+ // Override the timestamp marker's stack with our own recursive stack, which
+ // can happen for unknown reasons (bug 1246555); we should not cause a crash
+ // when attempting to render a recursive stack trace
+ let timestampMarker = markers.find(m => m.name === "ConsoleTime");
+ ok(typeof timestampMarker.stack === "number", "ConsoleTime marker has a stack before overwriting.");
+ let frames = PerformanceController.getCurrentRecording().getFrames();
+ let frameIndex = timestampMarker.stack = frames.length;
+ frames.push({ line: 1, column: 1, source: "file.js", functionDisplayName: "test", parent: frameIndex + 1});
+ frames.push({ line: 1, column: 1, source: "file.js", functionDisplayName: "test", parent: frameIndex + 2 });
+ frames.push({ line: 1, column: 1, source: "file.js", functionDisplayName: "test", parent: frameIndex });
+
+ const tests = {
+ ConsoleTime: function (marker) {
+ info("Got `ConsoleTime` marker with data: " + JSON.stringify(marker));
+ ok(marker.stack === frameIndex, "Should have the ConsoleTime marker with recursive stack");
+ shouldHaveStack($, "startStack", marker);
+ shouldHaveStack($, "endStack", marker);
+ shouldHaveLabel($, "Timer Name:", "!!!", marker);
+ return true;
+ },
+ TimeStamp: function (marker) {
+ info("Got `TimeStamp` marker with data: " + JSON.stringify(marker));
+ shouldHaveLabel($, "Label:", "go", marker);
+ shouldHaveStack($, "stack", marker);
+ return true;
+ },
+ Styles: function (marker) {
+ info("Got `Styles` marker with data: " + JSON.stringify(marker));
+ if (marker.stack) {
+ shouldHaveStack($, "stack", marker);
+ return true;
+ }
+ },
+ Reflow: function (marker) {
+ info("Got `Reflow` marker with data: " + JSON.stringify(marker));
+ if (marker.stack) {
+ shouldHaveStack($, "stack", marker);
+ return true;
+ }
+ }
+ };
+
+ // Keep track of all marker tests that are finished so we only
+ // run through each marker test once, so we don't spam 500 redundant
+ // tests.
+ let testsDone = [];
+
+ for (let i = 0; i < bars.length; i++) {
+ let bar = bars[i];
+ let m = markers[i];
+ await EventUtils.sendMouseEvent({ type: "mousedown" }, bar);
+
+ if (tests[m.name]) {
+ if (!testsDone.includes(m.name)) {
+ let fullTestComplete = tests[m.name](m);
+ if (fullTestComplete) {
+ testsDone.push(m.name);
+ }
+ }
+ } else {
+ throw new Error(`No tests for ${m.name} -- should be filtered out.`);
+ }
+
+ if (testsDone.length === Object.keys(tests).length) {
+ break;
+ }
+ }
+
+ await teardown(panel);
+ finish();
+}
+
+function shouldHaveStack($, type, marker) {
+ ok($(`#waterfall-details .marker-details-stack[type=${type}]`), `${marker.name} has a stack: ${type}`);
+}
+
+function shouldHaveLabel($, name, value, marker) {
+ let $name = $(`#waterfall-details .marker-details-labelcontainer .marker-details-labelname[value="${name}"]`);
+ let $value = $name.parentNode.querySelector(".marker-details-labelvalue");
+ is($value.getAttribute("value"), value, `${marker.name} has correct label for ${name}:${value}`);
+}
+/* eslint-enable */
diff --git a/devtools/client/performance/test/browser_perf-options-01-toggle-throw.js b/devtools/client/performance/test/browser_perf-options-01-toggle-throw.js
new file mode 100644
index 0000000000..a110d1b2c1
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-01-toggle-throw.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that toggling preferences before there are any recordings does not throw.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { DetailsView, JsCallTreeView } = panel.panelWin;
+
+ await DetailsView.selectView("js-calltree");
+
+ // Manually call the _onPrefChanged function so we can catch an error.
+ try {
+ JsCallTreeView._onPrefChanged(null, "invert-call-tree", true);
+ ok(
+ true,
+ "Toggling preferences before there are any recordings should not fail."
+ );
+ } catch (e) {
+ ok(
+ false,
+ "Toggling preferences before there are any recordings should not fail."
+ );
+ }
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-02-toggle-throw-alt.js b/devtools/client/performance/test/browser_perf-options-02-toggle-throw-alt.js
new file mode 100644
index 0000000000..a8dc39390d
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-02-toggle-throw-alt.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that toggling preferences during a recording does not throw.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { DetailsView, JsCallTreeView } = panel.panelWin;
+
+ await DetailsView.selectView("js-calltree");
+ await startRecording(panel);
+
+ // Manually call the _onPrefChanged function so we can catch an error.
+ try {
+ JsCallTreeView._onPrefChanged(null, "invert-call-tree", true);
+ ok(true, "Toggling preferences during a recording should not fail.");
+ } catch (e) {
+ ok(false, "Toggling preferences during a recording should not fail.");
+ }
+
+ await stopRecording(panel, {
+ expectedViewClass: "JsCallTreeView",
+ expectedViewEvent: "UI_JS_CALL_TREE_RENDERED",
+ });
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-03-toggle-meta.js b/devtools/client/performance/test/browser_perf-options-03-toggle-meta.js
new file mode 100644
index 0000000000..9d0428a97c
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-03-toggle-meta.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that toggling meta option prefs change visibility of other options.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ UI_EXPERIMENTAL_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+
+add_task(async function() {
+ Services.prefs.setBoolPref(UI_EXPERIMENTAL_PREF, false);
+
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { $ } = panel.panelWin;
+ const $body = $(".theme-body");
+ const $menu = $("#performance-options-menupopup");
+
+ ok(
+ !$body.classList.contains("experimental-enabled"),
+ "The body node does not have `experimental-enabled` on start."
+ );
+ ok(
+ !$menu.classList.contains("experimental-enabled"),
+ "The menu popup does not have `experimental-enabled` on start."
+ );
+
+ Services.prefs.setBoolPref(UI_EXPERIMENTAL_PREF, true);
+
+ ok(
+ $body.classList.contains("experimental-enabled"),
+ "The body node has `experimental-enabled` after toggle."
+ );
+ ok(
+ $menu.classList.contains("experimental-enabled"),
+ "The menu popup has `experimental-enabled` after toggle."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-enable-framerate-01.js b/devtools/client/performance/test/browser_perf-options-enable-framerate-01.js
new file mode 100644
index 0000000000..3bc7a686a3
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-enable-framerate-01.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that `enable-framerate` toggles the visibility of the fps graph,
+ * as well as enabling ticks data on the PerformanceFront.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ UI_ENABLE_FRAMERATE_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ isVisible,
+} = require("devtools/client/performance/test/helpers/dom-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { $, PerformanceController } = panel.panelWin;
+
+ // Disable framerate to test.
+ Services.prefs.setBoolPref(UI_ENABLE_FRAMERATE_PREF, false);
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ is(
+ PerformanceController.getCurrentRecording().getConfiguration().withTicks,
+ false,
+ "PerformanceFront started without ticks recording."
+ );
+ ok(
+ !isVisible($("#time-framerate")),
+ "The fps graph is hidden when ticks disabled."
+ );
+
+ // Re-enable framerate.
+ Services.prefs.setBoolPref(UI_ENABLE_FRAMERATE_PREF, true);
+
+ is(
+ PerformanceController.getCurrentRecording().getConfiguration().withTicks,
+ false,
+ "PerformanceFront still marked without ticks recording."
+ );
+ ok(
+ !isVisible($("#time-framerate")),
+ "The fps graph is still hidden if recording does not contain ticks."
+ );
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ is(
+ PerformanceController.getCurrentRecording().getConfiguration().withTicks,
+ true,
+ "PerformanceFront started with ticks recording."
+ );
+ ok(
+ isVisible($("#time-framerate")),
+ "The fps graph is not hidden when ticks enabled before recording."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-enable-framerate-02.js b/devtools/client/performance/test/browser_perf-options-enable-framerate-02.js
new file mode 100644
index 0000000000..2c4da5c89f
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-enable-framerate-02.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that toggling `enable-memory` during a recording doesn't change that
+ * recording's state and does not break.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ UI_ENABLE_FRAMERATE_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { PerformanceController } = panel.panelWin;
+
+ // Test starting without framerate, and stopping with it.
+ Services.prefs.setBoolPref(UI_ENABLE_FRAMERATE_PREF, false);
+ await startRecording(panel);
+
+ Services.prefs.setBoolPref(UI_ENABLE_FRAMERATE_PREF, true);
+ await stopRecording(panel);
+
+ is(
+ PerformanceController.getCurrentRecording().getConfiguration().withTicks,
+ false,
+ "The recording finished without tracking framerate."
+ );
+
+ // Test starting with framerate, and stopping without it.
+ await startRecording(panel);
+
+ Services.prefs.setBoolPref(UI_ENABLE_FRAMERATE_PREF, false);
+ await stopRecording(panel);
+
+ is(
+ PerformanceController.getCurrentRecording().getConfiguration().withTicks,
+ true,
+ "The recording finished with tracking framerate."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-enable-memory-01.js b/devtools/client/performance/test/browser_perf-options-enable-memory-01.js
new file mode 100644
index 0000000000..e32ad8b84c
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-enable-memory-01.js
@@ -0,0 +1,96 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that `enable-memory` toggles the visibility of the memory graph,
+ * as well as enabling memory data on the PerformanceFront.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ UI_ENABLE_MEMORY_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ isVisible,
+} = require("devtools/client/performance/test/helpers/dom-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { $, PerformanceController } = panel.panelWin;
+
+ // Disable memory to test.
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, false);
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ is(
+ PerformanceController.getCurrentRecording().getConfiguration().withMemory,
+ false,
+ "PerformanceFront started without memory recording."
+ );
+ is(
+ PerformanceController.getCurrentRecording().getConfiguration()
+ .withAllocations,
+ false,
+ "PerformanceFront started without allocations recording."
+ );
+ ok(
+ !isVisible($("#memory-overview")),
+ "The memory graph is hidden when memory disabled."
+ );
+
+ // Re-enable memory.
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, true);
+
+ is(
+ PerformanceController.getCurrentRecording().getConfiguration().withMemory,
+ false,
+ "PerformanceFront still marked without memory recording."
+ );
+ is(
+ PerformanceController.getCurrentRecording().getConfiguration()
+ .withAllocations,
+ false,
+ "PerformanceFront still marked without allocations recording."
+ );
+ ok(
+ !isVisible($("#memory-overview")),
+ "memory graph is still hidden after enabling " +
+ "if recording did not start recording memory"
+ );
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ is(
+ PerformanceController.getCurrentRecording().getConfiguration().withMemory,
+ true,
+ "PerformanceFront started with memory recording."
+ );
+ is(
+ PerformanceController.getCurrentRecording().getConfiguration()
+ .withAllocations,
+ false,
+ "PerformanceFront did not record with allocations."
+ );
+ ok(
+ isVisible($("#memory-overview")),
+ "The memory graph is not hidden when memory enabled before recording."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-enable-memory-02.js b/devtools/client/performance/test/browser_perf-options-enable-memory-02.js
new file mode 100644
index 0000000000..b71fef4ed9
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-enable-memory-02.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that toggling `enable-memory` during a recording doesn't change that
+ * recording's state and does not break.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ UI_ENABLE_MEMORY_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { PerformanceController } = panel.panelWin;
+
+ // Test starting without memory, and stopping with it.
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, false);
+ await startRecording(panel);
+
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, true);
+ await stopRecording(panel);
+
+ is(
+ PerformanceController.getCurrentRecording().getConfiguration().withMemory,
+ false,
+ "The recording finished without tracking memory."
+ );
+ is(
+ PerformanceController.getCurrentRecording().getConfiguration()
+ .withAllocations,
+ false,
+ "The recording finished without tracking allocations."
+ );
+
+ // Test starting with memory, and stopping without it.
+ await startRecording(panel);
+
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, false);
+ await stopRecording(panel);
+
+ is(
+ PerformanceController.getCurrentRecording().getConfiguration().withMemory,
+ true,
+ "The recording finished with tracking memory."
+ );
+ is(
+ PerformanceController.getCurrentRecording().getConfiguration()
+ .withAllocations,
+ false,
+ "The recording still is not recording allocations."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-flatten-tree-recursion-01.js b/devtools/client/performance/test/browser_perf-options-flatten-tree-recursion-01.js
new file mode 100644
index 0000000000..07bdbf9e69
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-flatten-tree-recursion-01.js
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the js flamegraphs get rerendered when toggling `flatten-tree-recursion`.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ UI_FLATTEN_RECURSION_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const {
+ EVENTS,
+ PerformanceController,
+ DetailsView,
+ JsFlameGraphView,
+ } = panel.panelWin;
+ const {
+ FlameGraphUtils,
+ } = require("devtools/client/shared/widgets/FlameGraph");
+
+ Services.prefs.setBoolPref(UI_FLATTEN_RECURSION_PREF, true);
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ let rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ await DetailsView.selectView("js-flamegraph");
+ await rendered;
+
+ const thread1 = PerformanceController.getCurrentRecording().getProfile()
+ .threads[0];
+ const rendering1 = FlameGraphUtils._cache.get(thread1);
+
+ ok(thread1, "The samples were retrieved from the controller.");
+ ok(rendering1, "The rendering data was cached.");
+
+ rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_FLATTEN_RECURSION_PREF, false);
+ await rendered;
+ ok(true, "JsFlameGraphView rerendered when toggling flatten-tree-recursion.");
+
+ const thread2 = PerformanceController.getCurrentRecording().getProfile()
+ .threads[0];
+ const rendering2 = FlameGraphUtils._cache.get(thread2);
+
+ is(
+ thread1,
+ thread2,
+ "The same samples data should be retrieved from the controller (1)."
+ );
+ isnot(
+ rendering1,
+ rendering2,
+ "The rendering data should be different because other options were used (1)."
+ );
+
+ rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_FLATTEN_RECURSION_PREF, true);
+ await rendered;
+ ok(
+ true,
+ "JsFlameGraphView rerendered when toggling back flatten-tree-recursion."
+ );
+
+ const thread3 = PerformanceController.getCurrentRecording().getProfile()
+ .threads[0];
+ const rendering3 = FlameGraphUtils._cache.get(thread3);
+
+ is(
+ thread2,
+ thread3,
+ "The same samples data should be retrieved from the controller (2)."
+ );
+ isnot(
+ rendering2,
+ rendering3,
+ "The rendering data should be different because other options were used (2)."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-flatten-tree-recursion-02.js b/devtools/client/performance/test/browser_perf-options-flatten-tree-recursion-02.js
new file mode 100644
index 0000000000..d7e8a0cc06
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-flatten-tree-recursion-02.js
@@ -0,0 +1,124 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the memory flamegraphs get rerendered when toggling
+ * `flatten-tree-recursion`.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ UI_FLATTEN_RECURSION_PREF,
+ UI_ENABLE_ALLOCATIONS_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const {
+ EVENTS,
+ PerformanceController,
+ DetailsView,
+ MemoryFlameGraphView,
+ } = panel.panelWin;
+
+ const {
+ FlameGraphUtils,
+ } = require("devtools/client/shared/widgets/FlameGraph");
+ const RecordingUtils = require("devtools/shared/performance/recording-utils");
+
+ // Enable memory to test
+ Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
+ Services.prefs.setBoolPref(UI_FLATTEN_RECURSION_PREF, true);
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ let rendered = once(
+ MemoryFlameGraphView,
+ EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED
+ );
+ await DetailsView.selectView("memory-flamegraph");
+ await rendered;
+
+ const allocations1 = PerformanceController.getCurrentRecording().getAllocations();
+ const thread1 = RecordingUtils.getProfileThreadFromAllocations(allocations1);
+ const rendering1 = FlameGraphUtils._cache.get(thread1);
+
+ ok(allocations1, "The allocations were retrieved from the controller.");
+ ok(thread1, "The allocations profile was synthesized by the utility funcs.");
+ ok(rendering1, "The rendering data was cached.");
+
+ rendered = once(MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_FLATTEN_RECURSION_PREF, false);
+ await rendered;
+ ok(
+ true,
+ "MemoryFlameGraphView rerendered when toggling flatten-tree-recursion."
+ );
+
+ const allocations2 = PerformanceController.getCurrentRecording().getAllocations();
+ const thread2 = RecordingUtils.getProfileThreadFromAllocations(allocations2);
+ const rendering2 = FlameGraphUtils._cache.get(thread2);
+
+ is(
+ allocations1,
+ allocations2,
+ "The same allocations data should be retrieved from the controller (1)."
+ );
+ is(
+ thread1,
+ thread2,
+ "The same allocations profile should be retrieved from the utility funcs. (1)."
+ );
+ isnot(
+ rendering1,
+ rendering2,
+ "The rendering data should be different because other options were used (1)."
+ );
+
+ rendered = once(MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_FLATTEN_RECURSION_PREF, true);
+ await rendered;
+ ok(
+ true,
+ "MemoryFlameGraphView rerendered when toggling back flatten-tree-recursion."
+ );
+
+ const allocations3 = PerformanceController.getCurrentRecording().getAllocations();
+ const thread3 = RecordingUtils.getProfileThreadFromAllocations(allocations3);
+ const rendering3 = FlameGraphUtils._cache.get(thread3);
+
+ is(
+ allocations2,
+ allocations3,
+ "The same allocations data should be retrieved from the controller (2)."
+ );
+ is(
+ thread2,
+ thread3,
+ "The same allocations profile should be retrieved from the utility funcs. (2)."
+ );
+ isnot(
+ rendering2,
+ rendering3,
+ "The rendering data should be different because other options were used (2)."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-invert-call-tree-01.js b/devtools/client/performance/test/browser_perf-options-invert-call-tree-01.js
new file mode 100644
index 0000000000..65fcd4f866
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-invert-call-tree-01.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the js call tree views get rerendered when toggling `invert-call-tree`.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ UI_INVERT_CALL_TREE_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { EVENTS, DetailsView, JsCallTreeView } = panel.panelWin;
+
+ Services.prefs.setBoolPref(UI_INVERT_CALL_TREE_PREF, true);
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ let rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ await DetailsView.selectView("js-calltree");
+ await rendered;
+
+ rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ Services.prefs.setBoolPref(UI_INVERT_CALL_TREE_PREF, false);
+ await rendered;
+ ok(true, "JsCallTreeView rerendered when toggling invert-call-tree.");
+
+ rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ Services.prefs.setBoolPref(UI_INVERT_CALL_TREE_PREF, true);
+ await rendered;
+ ok(true, "JsCallTreeView rerendered when toggling back invert-call-tree.");
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-invert-call-tree-02.js b/devtools/client/performance/test/browser_perf-options-invert-call-tree-02.js
new file mode 100644
index 0000000000..c594e9624e
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-invert-call-tree-02.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the memory call tree views get rerendered when toggling `invert-call-tree`.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ UI_ENABLE_ALLOCATIONS_PREF,
+ UI_INVERT_CALL_TREE_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { EVENTS, DetailsView, MemoryCallTreeView } = panel.panelWin;
+
+ // Enable allocations to test.
+ Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
+ Services.prefs.setBoolPref(UI_INVERT_CALL_TREE_PREF, true);
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ let rendered = once(MemoryCallTreeView, EVENTS.UI_MEMORY_CALL_TREE_RENDERED);
+ await DetailsView.selectView("memory-calltree");
+ await rendered;
+
+ rendered = once(MemoryCallTreeView, EVENTS.UI_MEMORY_CALL_TREE_RENDERED);
+ Services.prefs.setBoolPref(UI_INVERT_CALL_TREE_PREF, false);
+ await rendered;
+ ok(true, "MemoryCallTreeView rerendered when toggling invert-call-tree.");
+
+ rendered = once(MemoryCallTreeView, EVENTS.UI_MEMORY_CALL_TREE_RENDERED);
+ Services.prefs.setBoolPref(UI_INVERT_CALL_TREE_PREF, true);
+ await rendered;
+ ok(
+ true,
+ "MemoryCallTreeView rerendered when toggling back invert-call-tree."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-invert-flame-graph-01.js b/devtools/client/performance/test/browser_perf-options-invert-flame-graph-01.js
new file mode 100644
index 0000000000..1416a59df4
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-invert-flame-graph-01.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the js flamegraphs views get rerendered when toggling `invert-flame-graph`.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ UI_INVERT_FLAME_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { EVENTS, DetailsView, JsFlameGraphView } = panel.panelWin;
+
+ Services.prefs.setBoolPref(UI_INVERT_FLAME_PREF, true);
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ let rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ await DetailsView.selectView("js-flamegraph");
+ await rendered;
+
+ rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_INVERT_FLAME_PREF, false);
+ await rendered;
+ ok(true, "JsFlameGraphView rerendered when toggling invert-call-tree.");
+
+ rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_INVERT_FLAME_PREF, true);
+ await rendered;
+ ok(true, "JsFlameGraphView rerendered when toggling back invert-call-tree.");
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-invert-flame-graph-02.js b/devtools/client/performance/test/browser_perf-options-invert-flame-graph-02.js
new file mode 100644
index 0000000000..a968c17c68
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-invert-flame-graph-02.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the memory flamegraphs views get rerendered when toggling
+ * `invert-flame-graph`.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ UI_ENABLE_ALLOCATIONS_PREF,
+ UI_INVERT_FLAME_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { EVENTS, DetailsView, MemoryFlameGraphView } = panel.panelWin;
+
+ // Enable allocations to test.
+ Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
+ Services.prefs.setBoolPref(UI_INVERT_FLAME_PREF, true);
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ let rendered = once(
+ MemoryFlameGraphView,
+ EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED
+ );
+ await DetailsView.selectView("memory-flamegraph");
+ await rendered;
+
+ rendered = once(MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_INVERT_FLAME_PREF, false);
+ await rendered;
+ ok(true, "MemoryFlameGraphView rerendered when toggling invert-call-tree.");
+
+ rendered = once(MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_INVERT_FLAME_PREF, true);
+ await rendered;
+ ok(
+ true,
+ "MemoryFlameGraphView rerendered when toggling back invert-call-tree."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-propagate-allocations.js b/devtools/client/performance/test/browser_perf-options-propagate-allocations.js
new file mode 100644
index 0000000000..330085f9db
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-propagate-allocations.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that setting the `devtools.performance.memory.` prefs propagate to
+ * the memory actor.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ MEMORY_SAMPLE_PROB_PREF,
+ MEMORY_MAX_LOG_LEN_PREF,
+ UI_ENABLE_ALLOCATIONS_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+
+add_task(async function() {
+ const { panel, toolbox } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ // Enable allocations to test.
+ Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
+ Services.prefs.setCharPref(MEMORY_SAMPLE_PROB_PREF, "0.213");
+ Services.prefs.setIntPref(MEMORY_MAX_LOG_LEN_PREF, 777777);
+
+ await startRecording(panel);
+ const performanceFront = await toolbox.target.getFront("performance");
+ const {
+ probability,
+ maxLogLength,
+ } = await performanceFront.getConfiguration();
+ await stopRecording(panel);
+
+ is(
+ probability,
+ 0.213,
+ "The allocations probability option is set on memory actor."
+ );
+ is(
+ maxLogLength,
+ 777777,
+ "The allocations max log length option is set on memory actor."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-propagate-profiler.js b/devtools/client/performance/test/browser_perf-options-propagate-profiler.js
new file mode 100644
index 0000000000..2a969d353a
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-propagate-profiler.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that setting the `devtools.performance.profiler.` prefs propagate
+ * to the profiler actor.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ PROFILER_BUFFER_SIZE_PREF,
+ PROFILER_SAMPLE_RATE_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+
+add_task(async function() {
+ const { panel, toolbox } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ Services.prefs.setIntPref(PROFILER_BUFFER_SIZE_PREF, 1000);
+ Services.prefs.setIntPref(PROFILER_SAMPLE_RATE_PREF, 2000);
+
+ await startRecording(panel);
+ const performanceFront = await toolbox.target.getFront("performance");
+ const { entries, interval } = await performanceFront.getConfiguration();
+ await stopRecording(panel);
+
+ is(entries, 1000, "profiler entries option is set on profiler");
+ is(interval, 0.5, "profiler interval option is set on profiler");
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-show-idle-blocks-01.js b/devtools/client/performance/test/browser_perf-options-show-idle-blocks-01.js
new file mode 100644
index 0000000000..b62e13c794
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-show-idle-blocks-01.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the js flamegraphs get rerendered when toggling `show-idle-blocks`.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ UI_SHOW_IDLE_BLOCKS_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { EVENTS, DetailsView, JsFlameGraphView } = panel.panelWin;
+
+ Services.prefs.setBoolPref(UI_SHOW_IDLE_BLOCKS_PREF, true);
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ let rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ await DetailsView.selectView("js-flamegraph");
+ await rendered;
+
+ rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_SHOW_IDLE_BLOCKS_PREF, false);
+ await rendered;
+ ok(true, "JsFlameGraphView rerendered when toggling show-idle-blocks.");
+
+ rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_SHOW_IDLE_BLOCKS_PREF, true);
+ await rendered;
+ ok(true, "JsFlameGraphView rerendered when toggling back show-idle-blocks.");
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-show-idle-blocks-02.js b/devtools/client/performance/test/browser_perf-options-show-idle-blocks-02.js
new file mode 100644
index 0000000000..fbc500f9fb
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-show-idle-blocks-02.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the memory flamegraphs get rerendered when toggling `show-idle-blocks`.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ UI_ENABLE_ALLOCATIONS_PREF,
+ UI_SHOW_IDLE_BLOCKS_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { EVENTS, DetailsView, MemoryFlameGraphView } = panel.panelWin;
+
+ // Enable allocations to test.
+ Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
+ Services.prefs.setBoolPref(UI_SHOW_IDLE_BLOCKS_PREF, true);
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ let rendered = once(
+ MemoryFlameGraphView,
+ EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED
+ );
+ await DetailsView.selectView("memory-flamegraph");
+ await rendered;
+
+ rendered = once(MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_SHOW_IDLE_BLOCKS_PREF, false);
+ await rendered;
+ ok(true, "MemoryFlameGraphView rerendered when toggling show-idle-blocks.");
+
+ rendered = once(MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_SHOW_IDLE_BLOCKS_PREF, true);
+ await rendered;
+ ok(
+ true,
+ "MemoryFlameGraphView rerendered when toggling back show-idle-blocks."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-show-jit-optimizations.js b/devtools/client/performance/test/browser_perf-options-show-jit-optimizations.js
new file mode 100644
index 0000000000..62cf4b15eb
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-show-jit-optimizations.js
@@ -0,0 +1,303 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/* eslint-disable */
+// Bug 1235788, increase time out of this test
+requestLongerTimeout(2);
+
+/**
+ * Tests that the JIT Optimizations view renders optimization data
+ * if on, and displays selected frames on focus.
+ */
+const {
+ setSelectedRecording,
+} = require("devtools/client/performance/test/helpers/recording-utils");
+Services.prefs.setBoolPref(INVERT_PREF, false);
+
+async function spawnTest() {
+ let { panel } = await initPerformance(SIMPLE_URL);
+ let { EVENTS, $, $$, window, PerformanceController } = panel.panelWin;
+ let {
+ OverviewView,
+ DetailsView,
+ OptimizationsListView,
+ JsCallTreeView,
+ } = panel.panelWin;
+
+ let profilerData = { threads: [gThread] };
+
+ is(
+ Services.prefs.getBoolPref(JIT_PREF),
+ false,
+ "record JIT Optimizations pref off by default"
+ );
+ Services.prefs.setBoolPref(JIT_PREF, true);
+ is(
+ Services.prefs.getBoolPref(JIT_PREF),
+ true,
+ "toggle on record JIT Optimizations"
+ );
+
+ // Make two recordings, so we have one to switch to later, as the
+ // second one will have fake sample data
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ await DetailsView.selectView("js-calltree");
+
+ await injectAndRenderProfilerData();
+
+ is(
+ $("#jit-optimizations-view").classList.contains("hidden"),
+ true,
+ "JIT Optimizations should be hidden when pref is on, but no frame selected"
+ );
+
+ // A is never a leaf, so it's optimizations should not be shown.
+ await checkFrame(1);
+
+ // gRawSite2 and gRawSite3 are both optimizations on B, so they'll have
+ // indices in descending order of # of samples.
+ await checkFrame(2, true);
+
+ // Leaf node (C) with no optimizations should not display any opts.
+ await checkFrame(3);
+
+ // Select the node with optimizations and change to a new recording
+ // to ensure the opts view is cleared
+ let rendered = once(JsCallTreeView, "focus");
+ await mousedown(window, $$(".call-tree-item")[2]);
+ await rendered;
+ let isHidden = $("#jit-optimizations-view").classList.contains("hidden");
+ ok(!isHidden, "opts view should be visible when selecting a frame with opts");
+
+ let select = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+ rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ setSelectedRecording(panel, 0);
+ await Promise.all([select, rendered]);
+
+ isHidden = $("#jit-optimizations-view").classList.contains("hidden");
+ ok(isHidden, "opts view is hidden when switching recordings");
+
+ rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ setSelectedRecording(panel, 1);
+ await rendered;
+
+ rendered = once(JsCallTreeView, "focus");
+ await mousedown(window, $$(".call-tree-item")[2]);
+ await rendered;
+ isHidden = $("#jit-optimizations-view").classList.contains("hidden");
+ ok(!isHidden, "opts view should be visible when selecting a frame with opts");
+
+ rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ Services.prefs.setBoolPref(JIT_PREF, false);
+ await rendered;
+ ok(true, "call tree rerendered when JIT pref changes");
+ isHidden = $("#jit-optimizations-view").classList.contains("hidden");
+ ok(isHidden, "opts view hidden when toggling off jit pref");
+
+ rendered = once(JsCallTreeView, "focus");
+ await mousedown(window, $$(".call-tree-item")[2]);
+ await rendered;
+ isHidden = $("#jit-optimizations-view").classList.contains("hidden");
+ ok(
+ isHidden,
+ "opts view hidden when jit pref off and selecting a frame with opts"
+ );
+
+ await teardown(panel);
+ finish();
+
+ async function injectAndRenderProfilerData() {
+ // Get current recording and inject our mock data
+ info("Injecting mock profile data");
+ let recording = PerformanceController.getCurrentRecording();
+ recording._profile = profilerData;
+
+ // Force a rerender
+ let rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ JsCallTreeView.render(OverviewView.getTimeInterval());
+ await rendered;
+ }
+
+ async function checkFrame(frameIndex, hasOpts) {
+ info(`Checking frame ${frameIndex}`);
+ // Click the frame
+ let rendered = once(JsCallTreeView, "focus");
+ await mousedown(window, $$(".call-tree-item")[frameIndex]);
+ await rendered;
+
+ let isHidden = $("#jit-optimizations-view").classList.contains("hidden");
+ if (hasOpts) {
+ ok(
+ !isHidden,
+ "JIT Optimizations view is not hidden if current frame has opts."
+ );
+ } else {
+ ok(
+ isHidden,
+ "JIT Optimizations view is hidden if current frame does not have opts"
+ );
+ }
+ }
+}
+
+var gUniqueStacks = new RecordingUtils.UniqueStacks();
+
+function uniqStr(s) {
+ return gUniqueStacks.getOrAddStringIndex(s);
+}
+
+// Since deflateThread doesn't handle deflating optimization info, use
+// placeholder names A_O1, B_O2, and B_O3, which will be used to manually
+// splice deduped opts into the profile.
+var gThread = RecordingUtils.deflateThread(
+ {
+ samples: [
+ {
+ time: 0,
+ frames: [{ location: "(root)" }],
+ },
+ {
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "A_O1" },
+ { location: "B_O2" },
+ { location: "C (http://foo/bar/baz:56)" },
+ ],
+ },
+ {
+ time: 5 + 1,
+ frames: [
+ { location: "(root)" },
+ { location: "A (http://foo/bar/baz:12)" },
+ { location: "B_O2" },
+ ],
+ },
+ {
+ time: 5 + 1 + 2,
+ frames: [
+ { location: "(root)" },
+ { location: "A_O1" },
+ { location: "B_O3" },
+ ],
+ },
+ {
+ time: 5 + 1 + 2 + 7,
+ frames: [
+ { location: "(root)" },
+ { location: "A_O1" },
+ { location: "E (http://foo/bar/baz:90)" },
+ { location: "F (http://foo/bar/baz:99)" },
+ ],
+ },
+ ],
+ markers: [],
+ },
+ gUniqueStacks
+);
+
+// 3 RawOptimizationSites
+var gRawSite1 = {
+ _testFrameInfo: { name: "A", line: "12", file: "@baz" },
+ line: 12,
+ column: 2,
+ types: [
+ {
+ mirType: uniqStr("Object"),
+ site: uniqStr("A (http://foo/bar/bar:12)"),
+ typeset: [
+ {
+ keyedBy: uniqStr("constructor"),
+ name: uniqStr("Foo"),
+ location: uniqStr("A (http://foo/bar/baz:12)"),
+ },
+ {
+ keyedBy: uniqStr("primitive"),
+ location: uniqStr("self-hosted"),
+ },
+ ],
+ },
+ ],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1,
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ [uniqStr("Failure3"), uniqStr("SomeGetter3")],
+ ],
+ },
+};
+
+var gRawSite2 = {
+ _testFrameInfo: { name: "B", line: "10", file: "@boo" },
+ line: 40,
+ types: [
+ {
+ mirType: uniqStr("Int32"),
+ site: uniqStr("Receiver"),
+ },
+ ],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1,
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ [uniqStr("Inlined"), uniqStr("SomeGetter3")],
+ ],
+ },
+};
+
+var gRawSite3 = {
+ _testFrameInfo: { name: "B", line: "10", file: "@boo" },
+ line: 34,
+ types: [
+ {
+ mirType: uniqStr("Int32"),
+ site: uniqStr("Receiver"),
+ },
+ ],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1,
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ [uniqStr("Failure3"), uniqStr("SomeGetter3")],
+ ],
+ },
+};
+
+gThread.frameTable.data.forEach(frame => {
+ const LOCATION_SLOT = gThread.frameTable.schema.location;
+ const OPTIMIZATIONS_SLOT = gThread.frameTable.schema.optimizations;
+
+ let l = gThread.stringTable[frame[LOCATION_SLOT]];
+ switch (l) {
+ case "A_O1":
+ frame[LOCATION_SLOT] = uniqStr("A (http://foo/bar/baz:12)");
+ frame[OPTIMIZATIONS_SLOT] = gRawSite1;
+ break;
+ case "B_O2":
+ frame[LOCATION_SLOT] = uniqStr("B (http://foo/bar/boo:10)");
+ frame[OPTIMIZATIONS_SLOT] = gRawSite2;
+ break;
+ case "B_O3":
+ frame[LOCATION_SLOT] = uniqStr("B (http://foo/bar/boo:10)");
+ frame[OPTIMIZATIONS_SLOT] = gRawSite3;
+ break;
+ }
+});
+/* eslint-enable */
diff --git a/devtools/client/performance/test/browser_perf-options-show-platform-data-01.js b/devtools/client/performance/test/browser_perf-options-show-platform-data-01.js
new file mode 100644
index 0000000000..0f0d9f4ced
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-show-platform-data-01.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the js call tree views get rerendered when toggling `show-platform-data`.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ UI_SHOW_PLATFORM_DATA_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { EVENTS, DetailsView, JsCallTreeView } = panel.panelWin;
+
+ Services.prefs.setBoolPref(UI_SHOW_PLATFORM_DATA_PREF, true);
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ let rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ await DetailsView.selectView("js-calltree");
+ await rendered;
+
+ rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ Services.prefs.setBoolPref(UI_SHOW_PLATFORM_DATA_PREF, false);
+ await rendered;
+ ok(true, "JsCallTreeView rerendered when toggling show-idle-blocks.");
+
+ rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ Services.prefs.setBoolPref(UI_SHOW_PLATFORM_DATA_PREF, true);
+ await rendered;
+ ok(true, "JsCallTreeView rerendered when toggling back show-idle-blocks.");
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-options-show-platform-data-02.js b/devtools/client/performance/test/browser_perf-options-show-platform-data-02.js
new file mode 100644
index 0000000000..63317baf70
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-show-platform-data-02.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the js flamegraphs views get rerendered when toggling `show-platform-data`.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ UI_SHOW_PLATFORM_DATA_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { EVENTS, DetailsView, JsFlameGraphView } = panel.panelWin;
+
+ Services.prefs.setBoolPref(UI_SHOW_PLATFORM_DATA_PREF, true);
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ let rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ await DetailsView.selectView("js-flamegraph");
+ await rendered;
+
+ rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_SHOW_PLATFORM_DATA_PREF, false);
+ await rendered;
+ ok(true, "JsFlameGraphView rerendered when toggling show-idle-blocks.");
+
+ rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_SHOW_PLATFORM_DATA_PREF, true);
+ await rendered;
+ ok(true, "JsFlameGraphView rerendered when toggling back show-idle-blocks.");
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-overview-render-01.js b/devtools/client/performance/test/browser_perf-overview-render-01.js
new file mode 100644
index 0000000000..49eaafe1b3
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-overview-render-01.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the overview continuously renders content when recording.
+ */
+
+const { Constants } = require("devtools/client/performance/modules/constants");
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ times,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { EVENTS, OverviewView } = panel.panelWin;
+
+ await startRecording(panel);
+
+ // Ensure overview keeps rendering.
+ await times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
+ expectedArgs: [Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL],
+ });
+
+ ok(true, "Overview was rendered while recording.");
+
+ await stopRecording(panel);
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-overview-render-02.js b/devtools/client/performance/test/browser_perf-overview-render-02.js
new file mode 100644
index 0000000000..bc517ff170
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-overview-render-02.js
@@ -0,0 +1,158 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the overview graphs cannot be selected during recording
+ * and that they're cleared upon rerecording.
+ */
+
+const { Constants } = require("devtools/client/performance/modules/constants");
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ UI_ENABLE_MEMORY_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ times,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { EVENTS, OverviewView } = panel.panelWin;
+
+ // Enable memory to test.
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, true);
+
+ await startRecording(panel);
+
+ const framerate = OverviewView.graphs.get("framerate");
+ const markers = OverviewView.graphs.get("timeline");
+ const memory = OverviewView.graphs.get("memory");
+
+ ok(
+ "selectionEnabled" in framerate,
+ "The selection should not be enabled for the framerate overview (1)."
+ );
+ is(
+ framerate.selectionEnabled,
+ false,
+ "The selection should not be enabled for the framerate overview (2)."
+ );
+ is(
+ framerate.hasSelection(),
+ false,
+ "The framerate overview shouldn't have a selection before recording."
+ );
+
+ ok(
+ "selectionEnabled" in markers,
+ "The selection should not be enabled for the markers overview (1)."
+ );
+ is(
+ markers.selectionEnabled,
+ false,
+ "The selection should not be enabled for the markers overview (2)."
+ );
+ is(
+ markers.hasSelection(),
+ false,
+ "The markers overview shouldn't have a selection before recording."
+ );
+
+ ok(
+ "selectionEnabled" in memory,
+ "The selection should not be enabled for the memory overview (1)."
+ );
+ is(
+ memory.selectionEnabled,
+ false,
+ "The selection should not be enabled for the memory overview (2)."
+ );
+ is(
+ memory.hasSelection(),
+ false,
+ "The memory overview shouldn't have a selection before recording."
+ );
+
+ // Ensure overview keeps rendering.
+ await times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
+ expectedArgs: [Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL],
+ });
+
+ ok(
+ "selectionEnabled" in framerate,
+ "The selection should still not be enabled for the framerate overview (1)."
+ );
+ is(
+ framerate.selectionEnabled,
+ false,
+ "The selection should still not be enabled for the framerate overview (2)."
+ );
+ is(
+ framerate.hasSelection(),
+ false,
+ "The framerate overview still shouldn't have a selection before recording."
+ );
+
+ ok(
+ "selectionEnabled" in markers,
+ "The selection should still not be enabled for the markers overview (1)."
+ );
+ is(
+ markers.selectionEnabled,
+ false,
+ "The selection should still not be enabled for the markers overview (2)."
+ );
+ is(
+ markers.hasSelection(),
+ false,
+ "The markers overview still shouldn't have a selection before recording."
+ );
+
+ ok(
+ "selectionEnabled" in memory,
+ "The selection should still not be enabled for the memory overview (1)."
+ );
+ is(
+ memory.selectionEnabled,
+ false,
+ "The selection should still not be enabled for the memory overview (2)."
+ );
+ is(
+ memory.hasSelection(),
+ false,
+ "The memory overview still shouldn't have a selection before recording."
+ );
+
+ await stopRecording(panel);
+
+ is(
+ framerate.selectionEnabled,
+ true,
+ "The selection should now be enabled for the framerate overview."
+ );
+ is(
+ markers.selectionEnabled,
+ true,
+ "The selection should now be enabled for the markers overview."
+ );
+ is(
+ memory.selectionEnabled,
+ true,
+ "The selection should now be enabled for the memory overview."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-overview-render-03.js b/devtools/client/performance/test/browser_perf-overview-render-03.js
new file mode 100644
index 0000000000..05968827ea
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-overview-render-03.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the overview graphs share the exact same width and scaling.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ UI_ENABLE_MEMORY_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ waitUntil,
+} = require("devtools/client/performance/test/helpers/wait-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { PerformanceController, OverviewView } = panel.panelWin;
+
+ // Enable memory to test.
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, true);
+
+ const doChecks = () => {
+ const markers = OverviewView.graphs.get("timeline");
+ const framerate = OverviewView.graphs.get("framerate");
+ const memory = OverviewView.graphs.get("memory");
+
+ ok(markers.width > 0, "The overview's markers graph has a width.");
+ ok(
+ markers.dataScaleX > 0,
+ "The overview's markers graph has a data scale factor."
+ );
+
+ ok(memory.width > 0, "The overview's memory graph has a width.");
+ ok(
+ memory.dataDuration > 0,
+ "The overview's memory graph has a data duration."
+ );
+ ok(
+ memory.dataScaleX > 0,
+ "The overview's memory graph has a data scale factor."
+ );
+
+ ok(framerate.width > 0, "The overview's framerate graph has a width.");
+ ok(
+ framerate.dataDuration > 0,
+ "The overview's framerate graph has a data duration."
+ );
+ ok(
+ framerate.dataScaleX > 0,
+ "The overview's framerate graph has a data scale factor."
+ );
+
+ is(
+ markers.width,
+ memory.width,
+ "The markers and memory graphs widths are the same."
+ );
+ is(
+ markers.width,
+ framerate.width,
+ "The markers and framerate graphs widths are the same."
+ );
+
+ is(
+ memory.dataDuration,
+ framerate.dataDuration,
+ "The memory and framerate graphs data duration are the same."
+ );
+
+ is(
+ markers.dataScaleX,
+ memory.dataScaleX,
+ "The markers and memory graphs data scale are the same."
+ );
+ is(
+ markers.dataScaleX,
+ framerate.dataScaleX,
+ "The markers and framerate graphs data scale are the same."
+ );
+ };
+
+ await startRecording(panel);
+ doChecks();
+
+ await waitUntil(
+ () => PerformanceController.getCurrentRecording().getMarkers().length
+ );
+ await waitUntil(
+ () => PerformanceController.getCurrentRecording().getMemory().length
+ );
+ await waitUntil(
+ () => PerformanceController.getCurrentRecording().getTicks().length
+ );
+ doChecks();
+
+ await stopRecording(panel);
+ doChecks();
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-overview-render-04.js b/devtools/client/performance/test/browser_perf-overview-render-04.js
new file mode 100644
index 0000000000..e4b156ed83
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-overview-render-04.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the overview graphs do not render when realtime rendering is off
+ * due to lack of e10s.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ UI_ENABLE_MEMORY_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ waitUntil,
+} = require("devtools/client/performance/test/helpers/wait-utils");
+const {
+ isVisible,
+} = require("devtools/client/performance/test/helpers/dom-utils");
+const {
+ setSelectedRecording,
+} = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { $, EVENTS, PerformanceController, OverviewView } = panel.panelWin;
+
+ // Enable memory to test.
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, true);
+
+ // Set realtime rendering off.
+ OverviewView.isRealtimeRenderingEnabled = () => false;
+
+ let updated = 0;
+ OverviewView.on(EVENTS.UI_OVERVIEW_RENDERED, () => updated++);
+
+ await startRecording(panel, { skipWaitingForOverview: true });
+
+ is(isVisible($("#overview-pane")), false, "Overview graphs hidden.");
+ is(updated, 0, "Overview graphs have not been updated");
+
+ await waitUntil(
+ () => PerformanceController.getCurrentRecording().getMarkers().length
+ );
+ await waitUntil(
+ () => PerformanceController.getCurrentRecording().getMemory().length
+ );
+ await waitUntil(
+ () => PerformanceController.getCurrentRecording().getTicks().length
+ );
+ is(isVisible($("#overview-pane")), false, "Overview graphs still hidden.");
+ is(updated, 0, "Overview graphs have still not been updated");
+
+ await stopRecording(panel);
+
+ is(isVisible($("#overview-pane")), true, "Overview graphs no longer hidden.");
+ is(updated, 1, "Overview graphs rendered upon completion.");
+
+ await startRecording(panel, { skipWaitingForOverview: true });
+
+ is(
+ isVisible($("#overview-pane")),
+ false,
+ "Overview graphs hidden again when starting new recording."
+ );
+ is(updated, 1, "Overview graphs have not been updated again.");
+
+ setSelectedRecording(panel, 0);
+ is(
+ isVisible($("#overview-pane")),
+ true,
+ "Overview graphs no longer hidden when switching back to complete recording."
+ );
+ is(updated, 1, "Overview graphs have not been updated again.");
+
+ setSelectedRecording(panel, 1);
+ is(
+ isVisible($("#overview-pane")),
+ false,
+ "Overview graphs hidden again when going back to inprogress recording."
+ );
+ is(updated, 1, "Overview graphs have not been updated again.");
+
+ await stopRecording(panel);
+
+ is(
+ isVisible($("#overview-pane")),
+ true,
+ "overview graphs no longer hidden when recording finishes"
+ );
+ is(updated, 2, "Overview graphs rendered again upon completion.");
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-overview-selection-01.js b/devtools/client/performance/test/browser_perf-overview-selection-01.js
new file mode 100644
index 0000000000..17d9516bfc
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-overview-selection-01.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that events are fired from selection manipulation.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+const {
+ dragStartCanvasGraph,
+ dragStopCanvasGraph,
+ clickCanvasGraph,
+} = require("devtools/client/performance/test/helpers/input-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ const duration = PerformanceController.getCurrentRecording().getDuration();
+ const graph = OverviewView.graphs.get("timeline");
+
+ // Select the first half of the graph.
+
+ let rangeSelected = once(OverviewView, EVENTS.UI_OVERVIEW_RANGE_SELECTED, {
+ spreadArgs: true,
+ });
+ dragStartCanvasGraph(graph, { x: 0 });
+ let [{ startTime, endTime }] = await rangeSelected;
+ is(endTime, duration, "The selected range is the entire graph, for now.");
+
+ rangeSelected = once(OverviewView, EVENTS.UI_OVERVIEW_RANGE_SELECTED, {
+ spreadArgs: true,
+ });
+ dragStopCanvasGraph(graph, { x: graph.width / 2 });
+ [{ startTime, endTime }] = await rangeSelected;
+ is(endTime, duration / 2, "The selected range is half of the graph.");
+
+ is(graph.hasSelection(), true, "A selection exists on the graph.");
+ is(
+ startTime,
+ 0,
+ "The UI_OVERVIEW_RANGE_SELECTED event fired with 0 as a `startTime`."
+ );
+ is(
+ endTime,
+ duration / 2,
+ `The UI_OVERVIEW_RANGE_SELECTED event fired with ${duration /
+ 2} as \`endTime\`.`
+ );
+
+ const mapStart = () => 0;
+ const mapEnd = () => duration;
+ const actual = graph.getMappedSelection({ mapStart, mapEnd });
+ is(actual.min, 0, "Graph selection starts at 0.");
+ is(actual.max, duration / 2, `Graph selection ends at ${duration / 2}.`);
+
+ // Listen to deselection.
+
+ rangeSelected = once(OverviewView, EVENTS.UI_OVERVIEW_RANGE_SELECTED, {
+ spreadArgs: true,
+ });
+ clickCanvasGraph(graph, { x: (3 * graph.width) / 4 });
+ [{ startTime, endTime }] = await rangeSelected;
+
+ is(graph.hasSelection(), false, "A selection no longer on the graph.");
+ is(
+ startTime,
+ 0,
+ "The UI_OVERVIEW_RANGE_SELECTED event fired with 0 as a `startTime`."
+ );
+ is(
+ endTime,
+ duration,
+ "The UI_OVERVIEW_RANGE_SELECTED event fired with duration as `endTime`."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-overview-selection-02.js b/devtools/client/performance/test/browser_perf-overview-selection-02.js
new file mode 100644
index 0000000000..549f1e9287
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-overview-selection-02.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the graphs' selection is correctly disabled or enabled.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ UI_ENABLE_MEMORY_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { OverviewView } = panel.panelWin;
+
+ // Enable memory to test.
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, true);
+
+ await startRecording(panel);
+
+ const markersOverview = OverviewView.graphs.get("timeline");
+ const memoryGraph = OverviewView.graphs.get("memory");
+ const framerateGraph = OverviewView.graphs.get("framerate");
+
+ ok(markersOverview, "The markers graph should have been created now.");
+ ok(memoryGraph, "The memory graph should have been created now.");
+ ok(framerateGraph, "The framerate graph should have been created now.");
+
+ ok(
+ !markersOverview.selectionEnabled,
+ "Selection shouldn't be enabled when the first recording started (2)."
+ );
+ ok(
+ !memoryGraph.selectionEnabled,
+ "Selection shouldn't be enabled when the first recording started (3)."
+ );
+ ok(
+ !framerateGraph.selectionEnabled,
+ "Selection shouldn't be enabled when the first recording started (1)."
+ );
+
+ await stopRecording(panel);
+
+ ok(
+ markersOverview.selectionEnabled,
+ "Selection should be enabled when the first recording finishes (2)."
+ );
+ ok(
+ memoryGraph.selectionEnabled,
+ "Selection should be enabled when the first recording finishes (3)."
+ );
+ ok(
+ framerateGraph.selectionEnabled,
+ "Selection should be enabled when the first recording finishes (1)."
+ );
+
+ await startRecording(panel);
+
+ ok(
+ !markersOverview.selectionEnabled,
+ "Selection shouldn't be enabled when the second recording started (2)."
+ );
+ ok(
+ !memoryGraph.selectionEnabled,
+ "Selection shouldn't be enabled when the second recording started (3)."
+ );
+ ok(
+ !framerateGraph.selectionEnabled,
+ "Selection shouldn't be enabled when the second recording started (1)."
+ );
+
+ await stopRecording(panel);
+
+ ok(
+ markersOverview.selectionEnabled,
+ "Selection should be enabled when the first second finishes (2)."
+ );
+ ok(
+ memoryGraph.selectionEnabled,
+ "Selection should be enabled when the first second finishes (3)."
+ );
+ ok(
+ framerateGraph.selectionEnabled,
+ "Selection should be enabled when the first second finishes (1)."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-overview-selection-03.js b/devtools/client/performance/test/browser_perf-overview-selection-03.js
new file mode 100644
index 0000000000..7dcdbe95de
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-overview-selection-03.js
@@ -0,0 +1,122 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the graphs' selections are linked.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ UI_ENABLE_MEMORY_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ times,
+} = require("devtools/client/performance/test/helpers/event-utils");
+const {
+ dragStartCanvasGraph,
+ dragStopCanvasGraph,
+} = require("devtools/client/performance/test/helpers/input-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { EVENTS, OverviewView } = panel.panelWin;
+
+ // Enable memory to test.
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, true);
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ const markersOverview = OverviewView.graphs.get("timeline");
+ const memoryGraph = OverviewView.graphs.get("memory");
+ const framerateGraph = OverviewView.graphs.get("framerate");
+ const width = framerateGraph.width;
+
+ // Perform a selection inside the framerate graph.
+
+ let rangeSelected = times(OverviewView, EVENTS.UI_OVERVIEW_RANGE_SELECTED, 2);
+ dragStartCanvasGraph(framerateGraph, { x: 0 });
+ dragStopCanvasGraph(framerateGraph, { x: width / 2 });
+ await rangeSelected;
+
+ is(
+ JSON.stringify(markersOverview.getSelection()),
+ JSON.stringify(framerateGraph.getSelection()),
+ "The markers overview has a correct selection."
+ );
+ is(
+ JSON.stringify(memoryGraph.getSelection()),
+ JSON.stringify(framerateGraph.getSelection()),
+ "The memory overview has a correct selection."
+ );
+ is(
+ JSON.stringify(framerateGraph.getSelection()),
+ JSON.stringify({ start: 0, end: width / 2 }),
+ "The framerate graph has a correct selection."
+ );
+
+ // Perform a selection inside the markers overview.
+
+ markersOverview.dropSelection();
+
+ rangeSelected = times(OverviewView, EVENTS.UI_OVERVIEW_RANGE_SELECTED, 2);
+ dragStartCanvasGraph(markersOverview, { x: 0 });
+ dragStopCanvasGraph(markersOverview, { x: width / 4 });
+ await rangeSelected;
+
+ is(
+ JSON.stringify(markersOverview.getSelection()),
+ JSON.stringify(framerateGraph.getSelection()),
+ "The markers overview has a correct selection."
+ );
+ is(
+ JSON.stringify(memoryGraph.getSelection()),
+ JSON.stringify(framerateGraph.getSelection()),
+ "The memory overview has a correct selection."
+ );
+ is(
+ JSON.stringify(framerateGraph.getSelection()),
+ JSON.stringify({ start: 0, end: width / 4 }),
+ "The framerate graph has a correct selection."
+ );
+
+ // Perform a selection inside the memory overview.
+
+ markersOverview.dropSelection();
+
+ rangeSelected = times(OverviewView, EVENTS.UI_OVERVIEW_RANGE_SELECTED, 2);
+ dragStartCanvasGraph(memoryGraph, { x: 0 });
+ dragStopCanvasGraph(memoryGraph, { x: width / 10 });
+ await rangeSelected;
+
+ is(
+ JSON.stringify(markersOverview.getSelection()),
+ JSON.stringify(framerateGraph.getSelection()),
+ "The markers overview has a correct selection."
+ );
+ is(
+ JSON.stringify(memoryGraph.getSelection()),
+ JSON.stringify(framerateGraph.getSelection()),
+ "The memory overview has a correct selection."
+ );
+ is(
+ JSON.stringify(framerateGraph.getSelection()),
+ JSON.stringify({ start: 0, end: width / 10 }),
+ "The framerate graph has a correct selection."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-overview-time-interval.js b/devtools/client/performance/test/browser_perf-overview-time-interval.js
new file mode 100644
index 0000000000..b268d54529
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-overview-time-interval.js
@@ -0,0 +1,96 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the `setTimeInterval` and `getTimeInterval` functions
+ * work properly.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { EVENTS, OverviewView } = panel.panelWin;
+
+ try {
+ OverviewView.setTimeInterval({ starTime: 0, endTime: 1 });
+ ok(false, "Setting a time interval shouldn't have worked.");
+ } catch (e) {
+ ok(true, "Setting a time interval didn't work, as expected.");
+ }
+
+ try {
+ OverviewView.getTimeInterval();
+ ok(false, "Getting the time interval shouldn't have worked.");
+ } catch (e) {
+ ok(true, "Getting the time interval didn't work, as expected.");
+ }
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ // Get/set the time interval and wait for the event propagation.
+
+ const rangeSelected = once(OverviewView, EVENTS.UI_OVERVIEW_RANGE_SELECTED);
+ OverviewView.setTimeInterval({ startTime: 10, endTime: 20 });
+ await rangeSelected;
+
+ const firstInterval = OverviewView.getTimeInterval();
+ info("First interval start time: " + firstInterval.startTime);
+ info("First interval end time: " + firstInterval.endTime);
+ is(
+ Math.round(firstInterval.startTime),
+ 10,
+ "The interval's start time was properly set."
+ );
+ is(
+ Math.round(firstInterval.endTime),
+ 20,
+ "The interval's end time was properly set."
+ );
+
+ // Get/set another time interval and make sure there's no event propagation.
+
+ function fail() {
+ ok(false, "The selection event should not have propagated.");
+ }
+
+ OverviewView.on(EVENTS.UI_OVERVIEW_RANGE_SELECTED, fail);
+ OverviewView.setTimeInterval(
+ { startTime: 30, endTime: 40 },
+ { stopPropagation: true }
+ );
+ OverviewView.off(EVENTS.UI_OVERVIEW_RANGE_SELECTED, fail);
+
+ const secondInterval = OverviewView.getTimeInterval();
+ info("Second interval start time: " + secondInterval.startTime);
+ info("Second interval end time: " + secondInterval.endTime);
+ is(
+ Math.round(secondInterval.startTime),
+ 30,
+ "The interval's start time was properly set again."
+ );
+ is(
+ Math.round(secondInterval.endTime),
+ 40,
+ "The interval's end time was properly set again."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-private-browsing.js b/devtools/client/performance/test/browser_perf-private-browsing.js
new file mode 100644
index 0000000000..8a2c7b93c4
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-private-browsing.js
@@ -0,0 +1,136 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the frontend is disabled when in private browsing mode.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+let gPanelWinTuples = [];
+
+add_task(async function() {
+ await testNormalWindow();
+ await testPrivateWindow();
+ await testRecordingFailingInWindow(0);
+ await testRecordingFailingInWindow(1);
+ await teardownPerfInWindow(1, { shouldCloseWindow: true });
+ await testRecordingSucceedingInWindow(0);
+ await teardownPerfInWindow(0, { shouldCloseWindow: false });
+
+ gPanelWinTuples = null;
+});
+
+async function createPanelInNewWindow(options) {
+ const win = await BrowserTestUtils.openNewBrowserWindow(options);
+ return createPanelInWindow(options, win);
+}
+
+async function createPanelInWindow(options, win = window) {
+ const { panel } = await initPerformanceInNewTab(
+ {
+ url: SIMPLE_URL,
+ win: win,
+ },
+ options
+ );
+
+ gPanelWinTuples.push({ panel, win });
+ return { panel, win };
+}
+
+async function testNormalWindow() {
+ const { panel } = await createPanelInWindow({
+ private: false,
+ });
+
+ const { PerformanceView } = panel.panelWin;
+
+ is(
+ PerformanceView.getState(),
+ "empty",
+ "The initial state of the performance panel view is correct (1)."
+ );
+}
+
+async function testPrivateWindow() {
+ const { panel } = await createPanelInNewWindow({
+ private: true,
+ // The add-on SDK can't seem to be able to listen to "ready" or "close"
+ // events for private tabs. Don't really absolutely need to though.
+ dontWaitForTabReady: true,
+ });
+
+ const { PerformanceView } = panel.panelWin;
+
+ is(
+ PerformanceView.getState(),
+ "unavailable",
+ "The initial state of the performance panel view is correct (2)."
+ );
+}
+
+async function testRecordingFailingInWindow(index) {
+ const { panel } = gPanelWinTuples[index];
+ const { EVENTS, PerformanceController } = panel.panelWin;
+
+ const onRecordingStarted = () => {
+ ok(false, "Recording should not start while a private window is present.");
+ };
+
+ PerformanceController.on(EVENTS.RECORDING_STATE_CHANGE, onRecordingStarted);
+
+ const whenFailed = once(
+ PerformanceController,
+ EVENTS.BACKEND_FAILED_AFTER_RECORDING_START
+ );
+ PerformanceController.startRecording();
+ await whenFailed;
+ ok(true, "Recording has failed.");
+
+ PerformanceController.off(EVENTS.RECORDING_STATE_CHANGE, onRecordingStarted);
+}
+
+async function testRecordingSucceedingInWindow(index) {
+ const { panel } = gPanelWinTuples[index];
+ const { EVENTS, PerformanceController } = panel.panelWin;
+
+ const onRecordingFailed = () => {
+ ok(false, "Recording should start while now private windows are present.");
+ };
+
+ PerformanceController.on(
+ EVENTS.BACKEND_FAILED_AFTER_RECORDING_START,
+ onRecordingFailed
+ );
+
+ await startRecording(panel);
+ await stopRecording(panel);
+ ok(true, "Recording has succeeded.");
+
+ PerformanceController.off(
+ EVENTS.BACKEND_FAILED_AFTER_RECORDING_START,
+ onRecordingFailed
+ );
+}
+
+async function teardownPerfInWindow(index, options) {
+ const { panel, win } = gPanelWinTuples[index];
+ await teardownToolboxAndRemoveTab(panel);
+
+ if (options.shouldCloseWindow) {
+ win.close();
+ }
+}
diff --git a/devtools/client/performance/test/browser_perf-range-changed-render.js b/devtools/client/performance/test/browser_perf-range-changed-render.js
new file mode 100644
index 0000000000..98e6ca7f92
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-range-changed-render.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the detail views are rerendered after the range changes.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const {
+ EVENTS,
+ OverviewView,
+ DetailsView,
+ WaterfallView,
+ JsCallTreeView,
+ JsFlameGraphView,
+ } = panel.panelWin;
+
+ let updatedWaterfall = 0;
+ let updatedCallTree = 0;
+ let updatedFlameGraph = 0;
+ const updateWaterfall = () => updatedWaterfall++;
+ const updateCallTree = () => updatedCallTree++;
+ const updateFlameGraph = () => updatedFlameGraph++;
+ WaterfallView.on(EVENTS.UI_WATERFALL_RENDERED, updateWaterfall);
+ JsCallTreeView.on(EVENTS.UI_JS_CALL_TREE_RENDERED, updateCallTree);
+ JsFlameGraphView.on(EVENTS.UI_JS_FLAMEGRAPH_RENDERED, updateFlameGraph);
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ let rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ OverviewView.emit(EVENTS.UI_OVERVIEW_RANGE_SELECTED, {
+ startTime: 0,
+ endTime: 10,
+ });
+ await rendered;
+ ok(
+ true,
+ "Waterfall rerenders when a range in the overview graph is selected."
+ );
+
+ rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ await DetailsView.selectView("js-calltree");
+ await rendered;
+ ok(true, "Call tree rerenders after its corresponding pane is shown.");
+
+ rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ await DetailsView.selectView("js-flamegraph");
+ await rendered;
+ ok(true, "Flamegraph rerenders after its corresponding pane is shown.");
+
+ rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ OverviewView.emit(EVENTS.UI_OVERVIEW_RANGE_SELECTED);
+ await rendered;
+ ok(
+ true,
+ "Flamegraph rerenders when a range in the overview graph is removed."
+ );
+
+ rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ await DetailsView.selectView("js-calltree");
+ await rendered;
+ ok(true, "Call tree rerenders after its corresponding pane is shown.");
+
+ rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ await DetailsView.selectView("waterfall");
+ await rendered;
+ ok(true, "Waterfall rerenders after its corresponding pane is shown.");
+
+ // The WaterfallView is rerendered on window resize. Loading the other graphs can
+ // trigger a window resize and increase the total number of rerenders.
+ // See Bug 1532993#c12.
+ ok(
+ updatedWaterfall === 3 || updatedWaterfall === 4,
+ "WaterfallView rerendered 3 or 4 times."
+ );
+ is(updatedCallTree, 2, "JsCallTreeView rerendered 2 times.");
+ is(updatedFlameGraph, 2, "JsFlameGraphView rerendered 2 times.");
+
+ WaterfallView.off(EVENTS.UI_WATERFALL_RENDERED, updateWaterfall);
+ JsCallTreeView.off(EVENTS.UI_JS_CALL_TREE_RENDERED, updateCallTree);
+ JsFlameGraphView.off(EVENTS.UI_JS_FLAMEGRAPH_RENDERED, updateFlameGraph);
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-recording-notices-01.js b/devtools/client/performance/test/browser_perf-recording-notices-01.js
new file mode 100644
index 0000000000..e608b8b19b
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recording-notices-01.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the recording notice panes are toggled in correct scenarios
+ * for initialization and a single recording.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { $, PerformanceView } = panel.panelWin;
+
+ const MAIN_CONTAINER = $("#performance-view");
+ const EMPTY = $("#empty-notice");
+ const CONTENT = $("#performance-view-content");
+ const DETAILS_CONTAINER = $("#details-pane-container");
+ const RECORDING = $("#recording-notice");
+ const DETAILS = $("#details-pane");
+
+ is(PerformanceView.getState(), "empty", "Correct default state.");
+ is(MAIN_CONTAINER.selectedPanel, EMPTY, "Showing empty panel on load.");
+
+ await startRecording(panel);
+
+ is(
+ PerformanceView.getState(),
+ "recording",
+ "Correct state during recording."
+ );
+ is(MAIN_CONTAINER.selectedPanel, CONTENT, "Showing main view with timeline.");
+ is(DETAILS_CONTAINER.selectedPanel, RECORDING, "Showing recording panel.");
+
+ await stopRecording(panel);
+
+ is(PerformanceView.getState(), "recorded", "Correct state after recording.");
+ is(MAIN_CONTAINER.selectedPanel, CONTENT, "Showing main view with timeline.");
+ is(DETAILS_CONTAINER.selectedPanel, DETAILS, "Showing rendered graphs.");
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-recording-notices-02.js b/devtools/client/performance/test/browser_perf-recording-notices-02.js
new file mode 100644
index 0000000000..a482aafcdc
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recording-notices-02.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the recording notice panes are toggled when going between
+ * a completed recording and an in-progress recording.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+const {
+ setSelectedRecording,
+} = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { EVENTS, $, PerformanceController, PerformanceView } = panel.panelWin;
+
+ const MAIN_CONTAINER = $("#performance-view");
+ const CONTENT = $("#performance-view-content");
+ const DETAILS_CONTAINER = $("#details-pane-container");
+ const RECORDING = $("#recording-notice");
+ const DETAILS = $("#details-pane");
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ await startRecording(panel);
+
+ is(
+ PerformanceView.getState(),
+ "recording",
+ "Correct state during recording."
+ );
+ is(MAIN_CONTAINER.selectedPanel, CONTENT, "Showing main view with timeline.");
+ is(DETAILS_CONTAINER.selectedPanel, RECORDING, "Showing recording panel.");
+
+ let selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+ setSelectedRecording(panel, 0);
+ await selected;
+
+ is(
+ PerformanceView.getState(),
+ "recorded",
+ "Correct state during recording but selecting a completed recording."
+ );
+ is(MAIN_CONTAINER.selectedPanel, CONTENT, "Showing main view with timeline.");
+ is(DETAILS_CONTAINER.selectedPanel, DETAILS, "Showing recorded panel.");
+
+ selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+ setSelectedRecording(panel, 1);
+ await selected;
+
+ is(
+ PerformanceView.getState(),
+ "recording",
+ "Correct state when switching back to recording in progress."
+ );
+ is(MAIN_CONTAINER.selectedPanel, CONTENT, "Showing main view with timeline.");
+ is(DETAILS_CONTAINER.selectedPanel, RECORDING, "Showing recording panel.");
+
+ await stopRecording(panel);
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-recording-notices-03.js b/devtools/client/performance/test/browser_perf-recording-notices-03.js
new file mode 100644
index 0000000000..de9cfdb144
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recording-notices-03.js
@@ -0,0 +1,208 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that recording notices display buffer status when available,
+ * and can switch between different recordings with the correct buffer
+ * information displayed.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ PROFILER_BUFFER_SIZE_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ pmmInitWithBrowser,
+ pmmStopProfiler,
+} = require("devtools/client/performance/test/helpers/profiler-mm-utils");
+const {
+ initPerformanceInTab,
+ initConsoleInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ waitUntil,
+} = require("devtools/client/performance/test/helpers/wait-utils");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+const {
+ setSelectedRecording,
+} = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(async function() {
+ // Make sure the profiler module is stopped so we can set a new buffer limit.
+ pmmInitWithBrowser(gBrowser);
+ await pmmStopProfiler();
+
+ // Keep the profiler's buffer large, but still get to 1% relatively quick.
+ Services.prefs.setIntPref(PROFILER_BUFFER_SIZE_PREF, 1000000);
+
+ const { target, console } = await initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { panel } = await initPerformanceInTab({ tab: target.localTab });
+ const { EVENTS, $, PerformanceController, PerformanceView } = panel.panelWin;
+
+ // Set a fast profiler-status update interval.
+ const performanceFront = await panel.target.getFront("performance");
+ await performanceFront.setProfilerStatusInterval(10);
+
+ const DETAILS_CONTAINER = $("#details-pane-container");
+ const NORMAL_BUFFER_STATUS_MESSAGE = $(
+ "#recording-notice .buffer-status-message"
+ );
+ const CONSOLE_BUFFER_STATUS_MESSAGE = $(
+ "#console-recording-notice .buffer-status-message"
+ );
+ let gPercent;
+
+ // Start a manual recording.
+ await startRecording(panel);
+
+ await waitUntil(async function() {
+ [gPercent] = await once(
+ PerformanceView,
+ EVENTS.UI_RECORDING_PROFILER_STATUS_RENDERED,
+ { spreadArgs: true }
+ );
+
+ return gPercent > 0;
+ });
+
+ ok(true, "Buffer percentage increased in display (1).");
+
+ let bufferUsage = PerformanceController.getBufferUsageForRecording(
+ PerformanceController.getCurrentRecording()
+ );
+
+ let bufferStatus = DETAILS_CONTAINER.getAttribute("buffer-status");
+ either(
+ bufferStatus,
+ "in-progress",
+ "full",
+ "Container has [buffer-status=in-progress] or [buffer-status=full]."
+ );
+ ok(
+ NORMAL_BUFFER_STATUS_MESSAGE.value.includes(gPercent + "%"),
+ "Buffer status text has correct percentage."
+ );
+
+ // Start a console profile.
+ await console.profile("rust");
+
+ await waitUntil(async function() {
+ [gPercent] = await once(
+ PerformanceView,
+ EVENTS.UI_RECORDING_PROFILER_STATUS_RENDERED,
+ { spreadArgs: true }
+ );
+
+ // In some slow environments (eg ccov) it can happen that the buffer is full
+ // during the test run. In that case, the next condition can never be true
+ // because bufferUsage is 1, so we introduced a special condition to account
+ // for this special case.
+ if (bufferStatus === "full") {
+ return gPercent === 100;
+ }
+
+ return gPercent > Math.floor(bufferUsage * 100);
+ });
+
+ ok(true, "Buffer percentage increased in display (2).");
+
+ bufferUsage = PerformanceController.getBufferUsageForRecording(
+ PerformanceController.getCurrentRecording()
+ );
+ bufferStatus = DETAILS_CONTAINER.getAttribute("buffer-status");
+ either(
+ bufferStatus,
+ "in-progress",
+ "full",
+ "Container has [buffer-status=in-progress] or [buffer-status=full]."
+ );
+ ok(
+ NORMAL_BUFFER_STATUS_MESSAGE.value.includes(gPercent + "%"),
+ "Buffer status text has correct percentage."
+ );
+
+ // Select the console recording.
+ let selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+ setSelectedRecording(panel, 1);
+ await selected;
+
+ await waitUntil(async function() {
+ [gPercent] = await once(
+ PerformanceView,
+ EVENTS.UI_RECORDING_PROFILER_STATUS_RENDERED,
+ { spreadArgs: true }
+ );
+ return gPercent > 0;
+ });
+
+ ok(true, "Percentage updated for newly selected recording.");
+
+ bufferStatus = DETAILS_CONTAINER.getAttribute("buffer-status");
+ either(
+ bufferStatus,
+ "in-progress",
+ "full",
+ "Container has [buffer-status=in-progress] or [buffer-status=full]."
+ );
+ ok(
+ CONSOLE_BUFFER_STATUS_MESSAGE.value.includes(gPercent + "%"),
+ "Buffer status text has correct percentage for console recording."
+ );
+
+ // Stop the console profile, then select the original manual recording.
+ await console.profileEnd("rust");
+
+ selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+ setSelectedRecording(panel, 0);
+ await selected;
+
+ bufferStatus = DETAILS_CONTAINER.getAttribute("buffer-status");
+ await waitUntil(async function() {
+ [gPercent] = await once(
+ PerformanceView,
+ EVENTS.UI_RECORDING_PROFILER_STATUS_RENDERED,
+ { spreadArgs: true }
+ );
+
+ // In some slow environments (eg ccov) it can happen that the buffer is full
+ // during the test run. In that case, the next condition can never be true
+ // because bufferUsage is 1, so we introduced a special condition to account
+ // for this special case.
+ if (bufferStatus === "full") {
+ return gPercent === 100;
+ }
+
+ return gPercent > Math.floor(bufferUsage * 100);
+ });
+
+ ok(true, "Buffer percentage increased in display (3).");
+
+ bufferStatus = DETAILS_CONTAINER.getAttribute("buffer-status");
+ either(
+ bufferStatus,
+ "in-progress",
+ "full",
+ "Container has [buffer-status=in-progress] or [buffer-status=full]."
+ );
+ ok(
+ NORMAL_BUFFER_STATUS_MESSAGE.value.includes(gPercent + "%"),
+ "Buffer status text has correct percentage."
+ );
+
+ // Stop the manual recording.
+ await stopRecording(panel);
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-recording-notices-04.js b/devtools/client/performance/test/browser_perf-recording-notices-04.js
new file mode 100644
index 0000000000..d3be6fbab9
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recording-notices-04.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that when a recording overlaps the circular buffer, that
+ * a class is assigned to the recording notices.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ PROFILER_BUFFER_SIZE_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ pmmInitWithBrowser,
+ pmmStopProfiler,
+} = require("devtools/client/performance/test/helpers/profiler-mm-utils");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ waitUntil,
+} = require("devtools/client/performance/test/helpers/wait-utils");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(async function() {
+ // Make sure the profiler module is stopped so we can set a new buffer limit.
+ pmmInitWithBrowser(gBrowser);
+ await pmmStopProfiler();
+
+ // Keep the profiler's buffer small, to get to 100% really quickly.
+ Services.prefs.setIntPref(PROFILER_BUFFER_SIZE_PREF, 10000);
+
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { EVENTS, $, PerformanceController, PerformanceView } = panel.panelWin;
+
+ // Set a fast profiler-status update interval
+ const performanceFront = await panel.target.getFront("performance");
+ await performanceFront.setProfilerStatusInterval(10);
+
+ const DETAILS_CONTAINER = $("#details-pane-container");
+ const NORMAL_BUFFER_STATUS_MESSAGE = $(
+ "#recording-notice .buffer-status-message"
+ );
+ let gPercent;
+
+ // Start a manual recording.
+ await startRecording(panel);
+
+ await waitUntil(async function() {
+ [gPercent] = await once(
+ PerformanceView,
+ EVENTS.UI_RECORDING_PROFILER_STATUS_RENDERED,
+ { spreadArgs: true }
+ );
+ return gPercent == 100;
+ });
+
+ ok(true, "Buffer percentage increased in display.");
+
+ const bufferUsage = PerformanceController.getBufferUsageForRecording(
+ PerformanceController.getCurrentRecording()
+ );
+ is(bufferUsage, 1, "Buffer is full for this recording.");
+ is(
+ DETAILS_CONTAINER.getAttribute("buffer-status"),
+ "full",
+ "Container has [buffer-status=full]."
+ );
+ ok(
+ NORMAL_BUFFER_STATUS_MESSAGE.value.includes(gPercent + "%"),
+ "Buffer status text has correct percentage."
+ );
+
+ // Stop the manual recording.
+ await stopRecording(panel);
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-recording-notices-05.js b/devtools/client/performance/test/browser_perf-recording-notices-05.js
new file mode 100644
index 0000000000..0541797cb3
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recording-notices-05.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the circular buffer notices work when e10s is on/off.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { $, PerformanceController } = panel.panelWin;
+
+ // Set a fast profiler-status update interval
+ const performanceFront = await panel.target.getFront("performance");
+ await performanceFront.setProfilerStatusInterval(10);
+
+ let enabled = false;
+
+ PerformanceController.getMultiprocessStatus = () => {
+ return { enabled };
+ };
+
+ PerformanceController._setMultiprocessAttributes();
+ is(
+ $("#performance-view").getAttribute("e10s"),
+ "disabled",
+ "When e10s is disabled, container has [e10s=disabled]."
+ );
+
+ enabled = true;
+
+ PerformanceController._setMultiprocessAttributes();
+
+ // XXX: Switched to from ok() to todo_is() in Bug 1467712. Follow up in 1500913
+ // This cannot work with the current implementation, _setMultiprocessAttributes is not
+ // removing existing attributes.
+ todo_is(
+ $("#performance-view").getAttribute("e10s"),
+ "",
+ "When e10s is enabled, there should be no e10s attribute."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-recording-selected-01.js b/devtools/client/performance/test/browser_perf-recording-selected-01.js
new file mode 100644
index 0000000000..9da220083b
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recording-selected-01.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the profiler correctly handles multiple recordings and can
+ * successfully switch between them.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+const {
+ setSelectedRecording,
+ getRecordingsCount,
+ getSelectedRecordingIndex,
+} = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { EVENTS, PerformanceController } = panel.panelWin;
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ is(getRecordingsCount(panel), 2, "There should be two recordings visible.");
+ is(
+ getSelectedRecordingIndex(panel),
+ 1,
+ "The second recording item should be selected."
+ );
+
+ const selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+ setSelectedRecording(panel, 0);
+ await selected;
+
+ is(
+ getRecordingsCount(panel),
+ 2,
+ "There should still be two recordings visible."
+ );
+ is(
+ getSelectedRecordingIndex(panel),
+ 0,
+ "The first recording item should be selected."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-recording-selected-02.js b/devtools/client/performance/test/browser_perf-recording-selected-02.js
new file mode 100644
index 0000000000..d2e54fd975
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recording-selected-02.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the profiler correctly handles multiple recordings and can
+ * successfully switch between them, even when one of them is in progress.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+const {
+ getSelectedRecordingIndex,
+ setSelectedRecording,
+ getRecordingsCount,
+} = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(async function() {
+ // This test seems to take a very long time to finish on Linux VMs.
+ requestLongerTimeout(4);
+
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { EVENTS, PerformanceController } = panel.panelWin;
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ await startRecording(panel);
+
+ is(getRecordingsCount(panel), 2, "There should be two recordings visible.");
+ is(
+ getSelectedRecordingIndex(panel),
+ 1,
+ "The new recording item should be selected."
+ );
+
+ let selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+ setSelectedRecording(panel, 0);
+ await selected;
+
+ is(
+ getRecordingsCount(panel),
+ 2,
+ "There should still be two recordings visible."
+ );
+ is(
+ getSelectedRecordingIndex(panel),
+ 0,
+ "The first recording item should be selected now."
+ );
+
+ selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+ setSelectedRecording(panel, 1);
+ await selected;
+
+ is(
+ getRecordingsCount(panel),
+ 2,
+ "There should still be two recordings visible."
+ );
+ is(
+ getSelectedRecordingIndex(panel),
+ 1,
+ "The second recording item should be selected again."
+ );
+
+ await stopRecording(panel);
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-recording-selected-03.js b/devtools/client/performance/test/browser_perf-recording-selected-03.js
new file mode 100644
index 0000000000..316731a363
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recording-selected-03.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the profiler UI does not forget that recording is active when
+ * selected recording changes.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+const {
+ setSelectedRecording,
+} = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { $, EVENTS, PerformanceController } = panel.panelWin;
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ await startRecording(panel);
+
+ info("Selecting recording #0 and waiting for it to be displayed.");
+
+ const selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+ setSelectedRecording(panel, 0);
+ await selected;
+
+ ok(
+ $("#main-record-button").classList.contains("checked"),
+ "Button is still checked after selecting another item."
+ );
+ ok(
+ !$("#main-record-button").hasAttribute("disabled"),
+ "Button is not locked after selecting another item."
+ );
+
+ await stopRecording(panel);
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-recording-selected-04.js b/devtools/client/performance/test/browser_perf-recording-selected-04.js
new file mode 100644
index 0000000000..89c0f02023
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recording-selected-04.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that all components can get rerendered for a profile when switching.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ UI_ENABLE_MEMORY_PREF,
+ UI_ENABLE_ALLOCATIONS_PREF,
+ PROFILER_SAMPLE_RATE_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+ waitForAllWidgetsRendered,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ setSelectedRecording,
+} = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { DetailsView, DetailsSubview } = panel.panelWin;
+
+ // Enable memory to test the memory overview.
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, true);
+
+ // Enable allocations to test the memory-calltree and memory-flamegraph.
+ Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
+
+ // Because enabling the memory panel has a significant overhead, especially in
+ // slow builds like ccov builds, let's reduce the overhead from the sampling.
+ Services.prefs.setIntPref(PROFILER_SAMPLE_RATE_PREF, 100);
+
+ ok(true, "Starting recording...");
+ await startRecording(panel);
+ ok(true, "Recording started!");
+ ok(true, "Stopping recording...");
+ await stopRecording(panel);
+ ok(true, "Recording stopped!");
+
+ // Allow widgets to be updated while hidden, to make testing easier.
+ DetailsSubview.canUpdateWhileHidden = true;
+
+ // Cycle through all the views to initialize them. The waterfall is shown
+ // by default, but all the other views are created lazily, so won't emit
+ // any events.
+ await DetailsView.selectView("js-calltree");
+ await DetailsView.selectView("js-flamegraph");
+ await DetailsView.selectView("memory-calltree");
+ await DetailsView.selectView("memory-flamegraph");
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ let rerender = waitForAllWidgetsRendered(panel);
+ setSelectedRecording(panel, 0);
+ await rerender;
+
+ ok(true, "All widgets were rendered when selecting the first recording.");
+
+ rerender = waitForAllWidgetsRendered(panel);
+ setSelectedRecording(panel, 1);
+ await rerender;
+
+ ok(true, "All widgets were rendered when selecting the second recording.");
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-recordings-clear-01.js b/devtools/client/performance/test/browser_perf-recordings-clear-01.js
new file mode 100644
index 0000000000..3cfc27328f
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recordings-clear-01.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that clearing recordings empties out the recordings list and toggles
+ * the empty notice state.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPanelInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ getRecordingsCount,
+} = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(async function() {
+ const { panel } = await initPanelInNewTab({
+ tool: "performance",
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { PerformanceController, PerformanceView } = panel.panelWin;
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ is(
+ getRecordingsCount(panel),
+ 1,
+ "The recordings list should have one recording."
+ );
+ isnot(
+ PerformanceView.getState(),
+ "empty",
+ "PerformanceView should not be in an empty state."
+ );
+ isnot(
+ PerformanceController.getCurrentRecording(),
+ null,
+ "There should be a current recording."
+ );
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ is(
+ getRecordingsCount(panel),
+ 2,
+ "The recordings list should have two recordings."
+ );
+ isnot(
+ PerformanceView.getState(),
+ "empty",
+ "PerformanceView should not be in an empty state."
+ );
+ isnot(
+ PerformanceController.getCurrentRecording(),
+ null,
+ "There should be a current recording."
+ );
+
+ await PerformanceController.clearRecordings();
+
+ is(getRecordingsCount(panel), 0, "The recordings list should be empty.");
+ is(
+ PerformanceView.getState(),
+ "empty",
+ "PerformanceView should be in an empty state."
+ );
+ is(
+ PerformanceController.getCurrentRecording(),
+ null,
+ "There should be no current recording."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-recordings-clear-02.js b/devtools/client/performance/test/browser_perf-recordings-clear-02.js
new file mode 100644
index 0000000000..2f09304436
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recordings-clear-02.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that clearing recordings empties out the recordings list and stops
+ * a current recording if recording and can continue recording after.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPanelInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ times,
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+const {
+ getRecordingsCount,
+} = require("devtools/client/performance/test/helpers/recording-utils");
+
+add_task(async function() {
+ const { panel } = await initPanelInNewTab({
+ tool: "performance",
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { EVENTS, PerformanceController, PerformanceView } = panel.panelWin;
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ is(
+ getRecordingsCount(panel),
+ 1,
+ "The recordings list should have one recording."
+ );
+ isnot(
+ PerformanceView.getState(),
+ "empty",
+ "PerformanceView should not be in an empty state."
+ );
+ isnot(
+ PerformanceController.getCurrentRecording(),
+ null,
+ "There should be a current recording."
+ );
+
+ await startRecording(panel);
+
+ is(
+ getRecordingsCount(panel),
+ 2,
+ "The recordings list should have two recordings."
+ );
+ isnot(
+ PerformanceView.getState(),
+ "empty",
+ "PerformanceView should not be in an empty state."
+ );
+ isnot(
+ PerformanceController.getCurrentRecording(),
+ null,
+ "There should be a current recording."
+ );
+
+ const recordingDeleted = times(
+ PerformanceController,
+ EVENTS.RECORDING_DELETED,
+ 2
+ );
+ const recordingStopped = once(
+ PerformanceController,
+ EVENTS.RECORDING_STATE_CHANGE,
+ {
+ expectedArgs: ["recording-stopped"],
+ }
+ );
+
+ PerformanceController.clearRecordings();
+
+ await recordingDeleted;
+ await recordingStopped;
+
+ is(getRecordingsCount(panel), 0, "The recordings list should be empty.");
+ is(
+ PerformanceView.getState(),
+ "empty",
+ "PerformanceView should be in an empty state."
+ );
+ is(
+ PerformanceController.getCurrentRecording(),
+ null,
+ "There should be no current recording."
+ );
+
+ // Bug 1169146: Try another recording after clearing mid-recording.
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ is(
+ getRecordingsCount(panel),
+ 1,
+ "The recordings list should have one recording."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-recordings-io-01.js b/devtools/client/performance/test/browser_perf-recordings-io-01.js
new file mode 100644
index 0000000000..63040566c1
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recordings-io-01.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/* eslint-disable */
+/**
+ * Tests if the performance tool is able to save and load recordings.
+ */
+
+var test = async function () {
+ var { target, panel, toolbox } = await initPerformance(SIMPLE_URL);
+ var { $, EVENTS, PerformanceController, PerformanceView, DetailsView, DetailsSubview } = panel.panelWin;
+
+ // Enable allocations to test the memory-calltree and memory-flamegraph.
+ Services.prefs.setBoolPref(ALLOCATIONS_PREF, true);
+ Services.prefs.setBoolPref(MEMORY_PREF, true);
+ Services.prefs.setBoolPref(FRAMERATE_PREF, true);
+
+ // Need to allow widgets to be updated while hidden, otherwise we can't use
+ // `waitForWidgetsRendered`.
+ DetailsSubview.canUpdateWhileHidden = true;
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ // Cycle through all the views to initialize them, otherwise we can't use
+ // `waitForWidgetsRendered`. The waterfall is shown by default, but all the
+ // other views are created lazily, so won't emit any events.
+ await DetailsView.selectView("js-calltree");
+ await DetailsView.selectView("js-flamegraph");
+ await DetailsView.selectView("memory-calltree");
+ await DetailsView.selectView("memory-flamegraph");
+
+ // Verify original recording.
+
+ let originalData = PerformanceController.getCurrentRecording().getAllData();
+ ok(originalData, "The original recording is not empty.");
+
+ // Save recording.
+
+ let file = FileUtils.getFile("TmpD", ["tmpprofile.json"]);
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
+
+ let exported = once(PerformanceController, EVENTS.RECORDING_EXPORTED);
+ await PerformanceController.exportRecording("", PerformanceController.getCurrentRecording(), file);
+
+ await exported;
+ ok(true, "The recording data appears to have been successfully saved.");
+
+ // Check if the imported file name has tmpprofile in it as the file
+ // names also has different suffix to avoid conflict
+
+ let displayedName = $(".recording-item-title").getAttribute("value");
+ ok(/^tmpprofile/.test(displayedName), "File has expected display name after import");
+ ok(!/\.json$/.test(displayedName), "Display name does not have .json in it");
+
+ // Import recording.
+
+ let rerendered = waitForWidgetsRendered(panel);
+ let imported = once(PerformanceController, EVENTS.RECORDING_IMPORTED);
+ PerformanceView.emit(EVENTS.UI_IMPORT_RECORDING, file);
+
+ await imported;
+ ok(true, "The recording data appears to have been successfully imported.");
+
+ await rerendered;
+ ok(true, "The imported data was re-rendered.");
+
+ // Verify imported recording.
+
+ let importedData = PerformanceController.getCurrentRecording().getAllData();
+
+ ok(/^tmpprofile/.test(importedData.label),
+ "The imported data label is identical to the filename without its extension.");
+ is(importedData.duration, originalData.duration,
+ "The imported data is identical to the original data (1).");
+ is(importedData.markers.toSource(), originalData.markers.toSource(),
+ "The imported data is identical to the original data (2).");
+ is(importedData.memory.toSource(), originalData.memory.toSource(),
+ "The imported data is identical to the original data (3).");
+ is(importedData.ticks.toSource(), originalData.ticks.toSource(),
+ "The imported data is identical to the original data (4).");
+ is(importedData.allocations.toSource(), originalData.allocations.toSource(),
+ "The imported data is identical to the original data (5).");
+ is(importedData.profile.toSource(), originalData.profile.toSource(),
+ "The imported data is identical to the original data (6).");
+ is(importedData.configuration.withTicks, originalData.configuration.withTicks,
+ "The imported data is identical to the original data (7).");
+ is(importedData.configuration.withMemory, originalData.configuration.withMemory,
+ "The imported data is identical to the original data (8).");
+
+ await teardown(panel);
+ finish();
+};
+/* eslint-enable */
diff --git a/devtools/client/performance/test/browser_perf-recordings-io-02.js b/devtools/client/performance/test/browser_perf-recordings-io-02.js
new file mode 100644
index 0000000000..3104a40e96
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recordings-io-02.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/* eslint-disable */
+/**
+ * Tests if the performance tool gracefully handles loading bogus files.
+ */
+
+var test = async function () {
+ let { target, panel, toolbox } = await initPerformance(SIMPLE_URL);
+ let { EVENTS, PerformanceController } = panel.panelWin;
+
+ let file = FileUtils.getFile("TmpD", ["tmpprofile.json"]);
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
+
+ try {
+ await PerformanceController.importRecording("", file);
+ ok(false, "The recording succeeded unexpectedly.");
+ } catch (e) {
+ is(e.message, "Could not read recording data file.", "Message is correct.");
+ ok(true, "The recording was cancelled.");
+ }
+
+ await teardown(panel);
+ finish();
+};
diff --git a/devtools/client/performance/test/browser_perf-recordings-io-03.js b/devtools/client/performance/test/browser_perf-recordings-io-03.js
new file mode 100644
index 0000000000..6b9dc47f64
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recordings-io-03.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/* eslint-disable */
+/**
+ * Tests if the performance tool gracefully handles loading files that are JSON,
+ * but don't contain the appropriate recording data.
+ */
+
+var { FileUtils } = ChromeUtils.import("resource://gre/modules/FileUtils.jsm");
+var { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+
+var test = async function () {
+ let { target, panel, toolbox } = await initPerformance(SIMPLE_URL);
+ let { EVENTS, PerformanceController } = panel.panelWin;
+
+ let file = FileUtils.getFile("TmpD", ["tmpprofile.json"]);
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
+ await asyncCopy({ bogus: "data" }, file);
+
+ try {
+ await PerformanceController.importRecording("", file);
+ ok(false, "The recording succeeded unexpectedly.");
+ } catch (e) {
+ is(e.message, "Unrecognized recording data file.", "Message is correct.");
+ ok(true, "The recording was cancelled.");
+ }
+
+ await teardown(panel);
+ finish();
+};
+
+function getUnicodeConverter() {
+ let className = "@mozilla.org/intl/scriptableunicodeconverter";
+ let converter = Cc[className].createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ return converter;
+}
+
+function asyncCopy(data, file) {
+ let string = JSON.stringify(data);
+ let inputStream = getUnicodeConverter().convertToInputStream(string);
+ let outputStream = FileUtils.openSafeFileOutputStream(file);
+
+ return new Promise((resolve, reject) => {
+ NetUtil.asyncCopy(inputStream, outputStream, status => {
+ if (!Components.isSuccessCode(status)) {
+ reject(new Error("Could not save data to file."));
+ }
+ resolve();
+ });
+ });
+}
+/* eslint-enable */
diff --git a/devtools/client/performance/test/browser_perf-recordings-io-04.js b/devtools/client/performance/test/browser_perf-recordings-io-04.js
new file mode 100644
index 0000000000..30f723c3d8
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recordings-io-04.js
@@ -0,0 +1,176 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/* eslint-disable */
+/**
+ * Tests if the performance tool can import profiler data from the
+ * original profiler tool (Performance Recording v1, and Profiler data v2) and the correct views and graphs are loaded.
+ */
+var TICKS_DATA = (function () {
+ let ticks = [];
+ for (let i = 0; i < 100; i++) {
+ ticks.push(i * 10);
+ }
+ return ticks;
+})();
+
+var PROFILER_DATA = (function () {
+ let data = {};
+ let threads = data.threads = [];
+ let thread = {};
+ threads.push(thread);
+ thread.name = "Content";
+ thread.samples = [{
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" }
+ ]
+ }, {
+ time: 5 + 6,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" }
+ ]
+ }, {
+ time: 5 + 6 + 7,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "E" },
+ { location: "F" }
+ ]
+ }, {
+ time: 20,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" },
+ { location: "D" },
+ { location: "E" },
+ { location: "F" },
+ { location: "G" }
+ ]
+ }];
+
+ // Handled in other deflating tests
+ thread.markers = [];
+
+ let meta = data.meta = {};
+ meta.version = 2;
+ meta.interval = 1;
+ meta.stackwalk = 0;
+ meta.product = "Firefox";
+ return data;
+})();
+
+var test = async function () {
+ let { target, panel, toolbox } = await initPerformance(SIMPLE_URL);
+ let { $, EVENTS, PerformanceController, DetailsView, OverviewView, JsCallTreeView } = panel.panelWin;
+
+ // Enable memory to test the memory-calltree and memory-flamegraph.
+ Services.prefs.setBoolPref(ALLOCATIONS_PREF, true);
+
+ // Create a structure from the data that mimics the old profiler's data.
+ // Different name for `ticks`, different way of storing time,
+ // and no memory, markers data.
+ let oldProfilerData = {
+ profilerData: { profile: PROFILER_DATA },
+ ticksData: TICKS_DATA,
+ recordingDuration: 10000,
+ fileType: "Recorded Performance Data",
+ version: 1
+ };
+
+ // Save recording as an old profiler data.
+ let file = FileUtils.getFile("TmpD", ["tmpprofile.json"]);
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
+ await asyncCopy(oldProfilerData, file);
+
+ // Import recording.
+
+ let calltreeRendered = once(OverviewView, EVENTS.UI_FRAMERATE_GRAPH_RENDERED);
+ let fpsRendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ let imported = once(PerformanceController, EVENTS.RECORDING_IMPORTED);
+ await PerformanceController.importRecording("", file);
+
+ await imported;
+ ok(true, "The original profiler data appears to have been successfully imported.");
+
+ await calltreeRendered;
+ await fpsRendered;
+ ok(true, "The imported data was re-rendered.");
+
+ // Ensure that only framerate and js calltree/flamegraph view are available
+ is(isVisible($("#overview-pane")), true, "overview graph container still shown");
+ is(isVisible($("#memory-overview")), false, "memory graph hidden");
+ is(isVisible($("#markers-overview")), false, "markers overview graph hidden");
+ is(isVisible($("#time-framerate")), true, "fps graph shown");
+ is($("#select-waterfall-view").hidden, true, "waterfall button hidden");
+ is($("#select-js-calltree-view").hidden, false, "jscalltree button shown");
+ is($("#select-js-flamegraph-view").hidden, false, "jsflamegraph button shown");
+ is($("#select-memory-calltree-view").hidden, true, "memorycalltree button hidden");
+ is($("#select-memory-flamegraph-view").hidden, true, "memoryflamegraph button hidden");
+ ok(DetailsView.isViewSelected(JsCallTreeView), "jscalltree view selected as its the only option");
+
+ // Verify imported recording.
+
+ let importedData = PerformanceController.getCurrentRecording().getAllData();
+ let expected = Object.create({
+ duration: 10000,
+ markers: [].toSource(),
+ frames: [].toSource(),
+ memory: [].toSource(),
+ ticks: TICKS_DATA.toSource(),
+ profile: RecordingUtils.deflateProfile(JSON.parse(JSON.stringify(PROFILER_DATA))).toSource(),
+ allocations: ({sites:[], timestamps:[], frames:[], sizes: []}).toSource(),
+ withTicks: true,
+ withMemory: false,
+ sampleFrequency: void 0
+ });
+
+ for (let field in expected) {
+ if (!!~["withTicks", "withMemory", "sampleFrequency"].indexOf(field)) {
+ is(importedData.configuration[field], expected[field], `${field} successfully converted in legacy import.`);
+ } else if (field === "profile") {
+ is(importedData.profile.toSource(), expected.profile,
+ "profiler data's samples successfully converted in legacy import.");
+ is(importedData.profile.meta.version, 3, "Updated meta version to 3.");
+ } else {
+ let data = importedData[field];
+ is(typeof data === "object" ? data.toSource() : data, expected[field],
+ `${field} successfully converted in legacy import.`);
+ }
+ }
+
+ await teardown(panel);
+ finish();
+};
+
+function getUnicodeConverter() {
+ let className = "@mozilla.org/intl/scriptableunicodeconverter";
+ let converter = Cc[className].createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ return converter;
+}
+
+function asyncCopy(data, file) {
+ let string = JSON.stringify(data);
+ let inputStream = getUnicodeConverter().convertToInputStream(string);
+ let outputStream = FileUtils.openSafeFileOutputStream(file);
+
+ return new Promise((resolve, reject) => {
+ NetUtil.asyncCopy(inputStream, outputStream, status => {
+ if (!Components.isSuccessCode(status)) {
+ reject(new Error("Could not save data to file."));
+ }
+ resolve();
+ });
+ });
+}
+/* eslint-enable */
diff --git a/devtools/client/performance/test/browser_perf-recordings-io-05.js b/devtools/client/performance/test/browser_perf-recordings-io-05.js
new file mode 100644
index 0000000000..4d671543d2
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recordings-io-05.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/* eslint-disable */
+/**
+ * Test that when importing and no graphs rendered yet, we do not get a
+ * `getMappedSelection` error.
+ */
+
+var test = async function () {
+ var { target, panel, toolbox } = await initPerformance(SIMPLE_URL);
+ var { EVENTS, PerformanceController, WaterfallView } = panel.panelWin;
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ // Save recording.
+
+ let file = FileUtils.getFile("TmpD", ["tmpprofile.json"]);
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
+
+ let exported = once(PerformanceController, EVENTS.RECORDING_EXPORTED);
+ await PerformanceController.exportRecording("", PerformanceController.getCurrentRecording(), file);
+
+ await exported;
+ ok(true, "The recording data appears to have been successfully saved.");
+
+ // Clear and re-import.
+
+ await PerformanceController.clearRecordings();
+
+ let rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ let imported = once(PerformanceController, EVENTS.RECORDING_IMPORTED);
+ await PerformanceController.importRecording("", file);
+ await imported;
+ await rendered;
+
+ ok(true, "No error was thrown.");
+
+ await teardown(panel);
+ finish();
+};
+/* eslint-enable */
diff --git a/devtools/client/performance/test/browser_perf-recordings-io-06.js b/devtools/client/performance/test/browser_perf-recordings-io-06.js
new file mode 100644
index 0000000000..06c55efe99
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recordings-io-06.js
@@ -0,0 +1,140 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/* eslint-disable */
+/**
+ * Tests if the performance tool can import profiler data when Profiler is v2
+ * and requires deflating, and has an extra thread that's a string. Not sure
+ * what causes this.
+ */
+var STRINGED_THREAD = (function () {
+ let thread = {};
+
+ thread.libs = [{
+ start: 123,
+ end: 456,
+ offset: 0,
+ name: "",
+ breakpadId: ""
+ }];
+ thread.meta = { version: 2, interval: 1, stackwalk: 0, processType: 1, startTime: 0 };
+ thread.threads = [{
+ name: "Plugin",
+ tid: 4197,
+ samples: [],
+ markers: [],
+ }];
+
+ return JSON.stringify(thread);
+})();
+
+var PROFILER_DATA = (function () {
+ let data = {};
+ let threads = data.threads = [];
+ let thread = {};
+ threads.push(thread);
+ threads.push(STRINGED_THREAD);
+ thread.name = "Content";
+ thread.samples = [{
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" }
+ ]
+ }, {
+ time: 5 + 6,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" }
+ ]
+ }, {
+ time: 5 + 6 + 7,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "E" },
+ { location: "F" }
+ ]
+ }, {
+ time: 20,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" },
+ { location: "D" },
+ { location: "E" },
+ { location: "F" },
+ { location: "G" }
+ ]
+ }];
+
+ // Handled in other deflating tests
+ thread.markers = [];
+
+ let meta = data.meta = {};
+ meta.version = 2;
+ meta.interval = 1;
+ meta.stackwalk = 0;
+ meta.product = "Firefox";
+ return data;
+})();
+
+var test = async function () {
+ let { target, panel, toolbox } = await initPerformance(SIMPLE_URL);
+ let { $, EVENTS, PerformanceController, DetailsView, JsCallTreeView } = panel.panelWin;
+
+ let profilerData = {
+ profile: PROFILER_DATA,
+ duration: 10000,
+ configuration: {},
+ fileType: "Recorded Performance Data",
+ version: 2
+ };
+
+ let file = FileUtils.getFile("TmpD", ["tmpprofile.json"]);
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
+ await asyncCopy(profilerData, file);
+
+ // Import recording.
+
+ let calltreeRendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ let imported = once(PerformanceController, EVENTS.RECORDING_IMPORTED);
+ await PerformanceController.importRecording("", file);
+
+ await imported;
+ ok(true, "The profiler data appears to have been successfully imported.");
+
+ await calltreeRendered;
+ ok(true, "The imported data was re-rendered.");
+
+ await teardown(panel);
+ finish();
+};
+
+function getUnicodeConverter() {
+ let className = "@mozilla.org/intl/scriptableunicodeconverter";
+ let converter = Cc[className].createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ return converter;
+}
+
+function asyncCopy(data, file) {
+ let string = JSON.stringify(data);
+ let inputStream = getUnicodeConverter().convertToInputStream(string);
+ let outputStream = FileUtils.openSafeFileOutputStream(file);
+
+ return new Promise((resolve, reject) => {
+ NetUtil.asyncCopy(inputStream, outputStream, status => {
+ if (!Components.isSuccessCode(status)) {
+ reject(new Error("Could not save data to file."));
+ }
+ resolve();
+ });
+ });
+}
+/* eslint-enable */
diff --git a/devtools/client/performance/test/browser_perf-refresh.js b/devtools/client/performance/test/browser_perf-refresh.js
new file mode 100644
index 0000000000..bea41c5fc6
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-refresh.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Rough test that the recording still continues after a refresh.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+ reload,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ waitUntil,
+} = require("devtools/client/performance/test/helpers/wait-utils");
+
+add_task(async function() {
+ const { panel, target } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { PerformanceController } = panel.panelWin;
+
+ await startRecording(panel);
+ await reload(target);
+
+ const recording = PerformanceController.getCurrentRecording();
+ const markersLength = recording.getAllData().markers.length;
+
+ ok(
+ recording.isRecording(),
+ "RecordingModel should still be recording after reload"
+ );
+
+ await waitUntil(() => recording.getMarkers().length > markersLength);
+ ok(true, "Markers continue after reload.");
+
+ await stopRecording(panel);
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-states.js b/devtools/client/performance/test/browser_perf-states.js
new file mode 100644
index 0000000000..d55c24872c
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-states.js
@@ -0,0 +1,176 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that view states and lazy component intialization works.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ UI_ENABLE_MEMORY_PREF,
+ UI_ENABLE_ALLOCATIONS_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { PerformanceView, OverviewView, DetailsView } = panel.panelWin;
+
+ is(
+ PerformanceView.getState(),
+ "empty",
+ "The intial state of the performance panel view is correct."
+ );
+
+ ok(
+ !OverviewView.graphs.get("timeline"),
+ "The markers graph should not have been created yet."
+ );
+ ok(
+ !OverviewView.graphs.get("memory"),
+ "The memory graph should not have been created yet."
+ );
+ ok(
+ !OverviewView.graphs.get("framerate"),
+ "The framerate graph should not have been created yet."
+ );
+
+ ok(
+ !DetailsView.components.waterfall.initialized,
+ "The waterfall detail view should not have been created yet."
+ );
+ ok(
+ !DetailsView.components["js-calltree"].initialized,
+ "The js-calltree detail view should not have been created yet."
+ );
+ ok(
+ !DetailsView.components["js-flamegraph"].initialized,
+ "The js-flamegraph detail view should not have been created yet."
+ );
+ ok(
+ !DetailsView.components["memory-calltree"].initialized,
+ "The memory-calltree detail view should not have been created yet."
+ );
+ ok(
+ !DetailsView.components["memory-flamegraph"].initialized,
+ "The memory-flamegraph detail view should not have been created yet."
+ );
+
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, true);
+ Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
+
+ ok(
+ !OverviewView.graphs.get("timeline"),
+ "The markers graph should still not have been created yet."
+ );
+ ok(
+ !OverviewView.graphs.get("memory"),
+ "The memory graph should still not have been created yet."
+ );
+ ok(
+ !OverviewView.graphs.get("framerate"),
+ "The framerate graph should still not have been created yet."
+ );
+
+ await startRecording(panel);
+
+ is(
+ PerformanceView.getState(),
+ "recording",
+ "The current state of the performance panel view is 'recording'."
+ );
+ ok(
+ OverviewView.graphs.get("memory"),
+ "The memory graph should have been created now."
+ );
+ ok(
+ OverviewView.graphs.get("framerate"),
+ "The framerate graph should have been created now."
+ );
+
+ await stopRecording(panel);
+
+ is(
+ PerformanceView.getState(),
+ "recorded",
+ "The current state of the performance panel view is 'recorded'."
+ );
+ ok(
+ !DetailsView.components["js-calltree"].initialized,
+ "The js-calltree detail view should still not have been created yet."
+ );
+ ok(
+ !DetailsView.components["js-flamegraph"].initialized,
+ "The js-flamegraph detail view should still not have been created yet."
+ );
+ ok(
+ !DetailsView.components["memory-calltree"].initialized,
+ "The memory-calltree detail view should still not have been created yet."
+ );
+ ok(
+ !DetailsView.components["memory-flamegraph"].initialized,
+ "The memory-flamegraph detail view should still not have been created yet."
+ );
+
+ await DetailsView.selectView("js-calltree");
+
+ is(
+ PerformanceView.getState(),
+ "recorded",
+ "The current state of the performance panel view is still 'recorded'."
+ );
+ ok(
+ DetailsView.components["js-calltree"].initialized,
+ "The js-calltree detail view should still have been created now."
+ );
+ ok(
+ !DetailsView.components["js-flamegraph"].initialized,
+ "The js-flamegraph detail view should still not have been created yet."
+ );
+ ok(
+ !DetailsView.components["memory-calltree"].initialized,
+ "The memory-calltree detail view should still not have been created yet."
+ );
+ ok(
+ !DetailsView.components["memory-flamegraph"].initialized,
+ "The memory-flamegraph detail view should still not have been created yet."
+ );
+
+ await DetailsView.selectView("memory-calltree");
+
+ is(
+ PerformanceView.getState(),
+ "recorded",
+ "The current state of the performance panel view is still 'recorded'."
+ );
+ ok(
+ DetailsView.components["js-calltree"].initialized,
+ "The js-calltree detail view should still register as being created."
+ );
+ ok(
+ !DetailsView.components["js-flamegraph"].initialized,
+ "The js-flamegraph detail view should still not have been created yet."
+ );
+ ok(
+ DetailsView.components["memory-calltree"].initialized,
+ "The memory-calltree detail view should still have been created now."
+ );
+ ok(
+ !DetailsView.components["memory-flamegraph"].initialized,
+ "The memory-flamegraph detail view should still not have been created yet."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_perf-telemetry-01.js b/devtools/client/performance/test/browser_perf-telemetry-01.js
new file mode 100644
index 0000000000..6c236c8639
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-telemetry-01.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that the performance telemetry module records events at appropriate times.
+ * Specificaly the state about a recording process.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ UI_ENABLE_MEMORY_PREF,
+} = require("devtools/client/performance/test/helpers/prefs");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+
+add_task(async function() {
+ startTelemetry();
+
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, false);
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, true);
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ checkResults();
+
+ await teardownToolboxAndRemoveTab(panel);
+});
+
+function checkResults() {
+ // For help generating these tests use generateTelemetryTests("DEVTOOLS_PERFTOOLS_")
+ // here.
+ checkTelemetry(
+ "DEVTOOLS_PERFTOOLS_RECORDING_COUNT",
+ "",
+ { 0: 2, 1: 0 },
+ "array"
+ );
+ checkTelemetry(
+ "DEVTOOLS_PERFTOOLS_RECORDING_DURATION_MS",
+ "",
+ null,
+ "hasentries"
+ );
+ checkTelemetry(
+ "DEVTOOLS_PERFTOOLS_RECORDING_FEATURES_USED",
+ "withMarkers",
+ { 0: 0, 1: 2, 2: 0 },
+ "array"
+ );
+ checkTelemetry(
+ "DEVTOOLS_PERFTOOLS_RECORDING_FEATURES_USED",
+ "withMemory",
+ { 0: 1, 1: 1, 2: 0 },
+ "array"
+ );
+ checkTelemetry(
+ "DEVTOOLS_PERFTOOLS_RECORDING_FEATURES_USED",
+ "withAllocations",
+ { 0: 2, 1: 0 },
+ "array"
+ );
+ checkTelemetry(
+ "DEVTOOLS_PERFTOOLS_RECORDING_FEATURES_USED",
+ "withTicks",
+ { 0: 0, 1: 2, 2: 0 },
+ "array"
+ );
+}
diff --git a/devtools/client/performance/test/browser_perf-telemetry-02.js b/devtools/client/performance/test/browser_perf-telemetry-02.js
new file mode 100644
index 0000000000..c76a349b6d
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-telemetry-02.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the performance telemetry module records events at appropriate times.
+ * Specifically export/import.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(async function() {
+ startTelemetry();
+
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { EVENTS, PerformanceController } = panel.panelWin;
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ const file = FileUtils.getFile("TmpD", ["tmpprofile.json"]);
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
+
+ const exported = once(PerformanceController, EVENTS.RECORDING_EXPORTED);
+ await PerformanceController.exportRecording(
+ PerformanceController.getCurrentRecording(),
+ file
+ );
+ await exported;
+
+ const imported = once(PerformanceController, EVENTS.RECORDING_IMPORTED);
+ await PerformanceController.importRecording(file);
+ await imported;
+
+ checkResults();
+ await teardownToolboxAndRemoveTab(panel);
+});
+
+function checkResults() {
+ // For help generating these tests use generateTelemetryTests("DEVTOOLS_PERFTOOLS_")
+ // here.
+ checkTelemetry(
+ "DEVTOOLS_PERFTOOLS_RECORDING_IMPORT_FLAG",
+ "",
+ { 0: 0, 1: 1, 2: 0 },
+ "array"
+ );
+ checkTelemetry(
+ "DEVTOOLS_PERFTOOLS_RECORDING_EXPORT_FLAG",
+ "",
+ { 0: 0, 1: 1, 2: 0 },
+ "array"
+ );
+}
diff --git a/devtools/client/performance/test/browser_perf-telemetry-03.js b/devtools/client/performance/test/browser_perf-telemetry-03.js
new file mode 100644
index 0000000000..2983b1a125
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-telemetry-03.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the performance telemetry module records events at appropriate times.
+ * Specifically the destruction of certain views.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(async function() {
+ startTelemetry();
+
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const {
+ EVENTS,
+ DetailsView,
+ JsCallTreeView,
+ JsFlameGraphView,
+ } = panel.panelWin;
+
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ const calltreeRendered = once(
+ JsCallTreeView,
+ EVENTS.UI_JS_CALL_TREE_RENDERED
+ );
+ const flamegraphRendered = once(
+ JsFlameGraphView,
+ EVENTS.UI_JS_FLAMEGRAPH_RENDERED
+ );
+
+ // Go through some views to check later.
+ await DetailsView.selectView("js-calltree");
+ await calltreeRendered;
+
+ await DetailsView.selectView("js-flamegraph");
+ await flamegraphRendered;
+
+ await teardownToolboxAndRemoveTab(panel);
+
+ checkResults();
+});
+
+function checkResults() {
+ // For help generating these tests use generateTelemetryTests("DEVTOOLS_PERFTOOLS_")
+ // here.
+ checkTelemetry(
+ "DEVTOOLS_PERFTOOLS_SELECTED_VIEW_MS",
+ "js-calltree",
+ null,
+ "hasentries"
+ );
+ checkTelemetry(
+ "DEVTOOLS_PERFTOOLS_SELECTED_VIEW_MS",
+ "js-flamegraph",
+ null,
+ "hasentries"
+ );
+ checkTelemetry(
+ "DEVTOOLS_PERFTOOLS_SELECTED_VIEW_MS",
+ "waterfall",
+ null,
+ "hasentries"
+ );
+}
diff --git a/devtools/client/performance/test/browser_perf-telemetry-04.js b/devtools/client/performance/test/browser_perf-telemetry-04.js
new file mode 100644
index 0000000000..68df776cf5
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-telemetry-04.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the performance telemetry module records events at appropriate times.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInTab,
+ initConsoleInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ waitForRecordingStartedEvents,
+ waitForRecordingStoppedEvents,
+} = require("devtools/client/performance/test/helpers/actions");
+
+add_task(async function() {
+ startTelemetry();
+
+ const { target, console } = await initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { panel } = await initPerformanceInTab({ tab: target.localTab });
+
+ const started = waitForRecordingStartedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ });
+ await console.profile("rust");
+ await started;
+
+ const stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ });
+ await console.profileEnd("rust");
+ await stopped;
+
+ checkResults();
+ await teardownToolboxAndRemoveTab(panel);
+});
+
+function checkResults() {
+ // For help generating these tests use generateTelemetryTests("DEVTOOLS_PERFTOOLS_")
+ // here.
+ checkTelemetry(
+ "DEVTOOLS_PERFTOOLS_CONSOLE_RECORDING_COUNT",
+ "",
+ { 0: 1, 1: 0 },
+ "array"
+ );
+ checkTelemetry(
+ "DEVTOOLS_PERFTOOLS_RECORDING_DURATION_MS",
+ "",
+ null,
+ "hasentries"
+ );
+ checkTelemetry(
+ "DEVTOOLS_PERFTOOLS_RECORDING_FEATURES_USED",
+ "withMarkers",
+ { 0: 0, 1: 1, 2: 0 },
+ "array"
+ );
+ checkTelemetry(
+ "DEVTOOLS_PERFTOOLS_RECORDING_FEATURES_USED",
+ "withMemory",
+ { 0: 1, 1: 0 },
+ "array"
+ );
+ checkTelemetry(
+ "DEVTOOLS_PERFTOOLS_RECORDING_FEATURES_USED",
+ "withAllocations",
+ { 0: 1, 1: 0 },
+ "array"
+ );
+ checkTelemetry(
+ "DEVTOOLS_PERFTOOLS_RECORDING_FEATURES_USED",
+ "withTicks",
+ { 0: 0, 1: 1, 2: 0 },
+ "array"
+ );
+}
diff --git a/devtools/client/performance/test/browser_perf-theme-toggle.js b/devtools/client/performance/test/browser_perf-theme-toggle.js
new file mode 100644
index 0000000000..b866e59b6a
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-theme-toggle.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/* eslint-disable */
+/**
+ * Tests if the markers and memory overviews render with the correct
+ * theme on load, and rerenders when changed.
+ */
+
+const { setTheme } = require("devtools/client/shared/theme");
+
+const LIGHT_BG = "white";
+const DARK_BG = "#14171a";
+
+setTheme("dark");
+Services.prefs.setBoolPref(MEMORY_PREF, false);
+
+requestLongerTimeout(2);
+
+async function spawnTest() {
+ let { panel } = await initPerformance(SIMPLE_URL);
+ let { EVENTS, $, OverviewView, document: doc } = panel.panelWin;
+
+ await startRecording(panel);
+ let markers = OverviewView.graphs.get("timeline");
+ is(markers.backgroundColor, DARK_BG,
+ "correct theme on load for markers.");
+ await stopRecording(panel);
+
+ let refreshed = once(markers, "refresh");
+ setTheme("light");
+ await refreshed;
+
+ ok(true, "markers were rerendered after theme change.");
+ is(markers.backgroundColor, LIGHT_BG,
+ "correct theme on after toggle for markers.");
+
+ // reset back to dark
+ refreshed = once(markers, "refresh");
+ setTheme("dark");
+ await refreshed;
+
+ info("Testing with memory overview");
+
+ Services.prefs.setBoolPref(MEMORY_PREF, true);
+
+ await startRecording(panel);
+ let memory = OverviewView.graphs.get("memory");
+ is(memory.backgroundColor, DARK_BG,
+ "correct theme on load for memory.");
+ await stopRecording(panel);
+
+ refreshed = Promise.all([
+ once(markers, "refresh"),
+ once(memory, "refresh"),
+ ]);
+ setTheme("light");
+ await refreshed;
+
+ ok(true, "Both memory and markers were rerendered after theme change.");
+ is(markers.backgroundColor, LIGHT_BG,
+ "correct theme on after toggle for markers.");
+ is(memory.backgroundColor, LIGHT_BG,
+ "correct theme on after toggle for memory.");
+
+ refreshed = Promise.all([
+ once(markers, "refresh"),
+ once(memory, "refresh"),
+ ]);
+
+ // Set theme back to light
+ setTheme("light");
+ await refreshed;
+
+ await teardown(panel);
+ finish();
+}
+/* eslint-enable */
diff --git a/devtools/client/performance/test/browser_perf-tree-abstract-01.js b/devtools/client/performance/test/browser_perf-tree-abstract-01.js
new file mode 100644
index 0000000000..75c8c3803b
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-abstract-01.js
@@ -0,0 +1,205 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the abstract tree base class for the profiler's tree view
+ * works as advertised.
+ */
+
+const {
+ appendAndWaitForPaint,
+} = require("devtools/client/performance/test/helpers/dom-utils");
+const {
+ synthesizeCustomTreeClass,
+} = require("devtools/client/performance/test/helpers/synth-utils");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(async function() {
+ const { MyCustomTreeItem, myDataSrc } = synthesizeCustomTreeClass();
+
+ const container = document.createXULElement("vbox");
+ await appendAndWaitForPaint(gBrowser.selectedBrowser.parentNode, container);
+
+ // Populate the tree and test the root item...
+
+ const treeRoot = new MyCustomTreeItem(myDataSrc, { parent: null });
+ treeRoot.attachTo(container);
+
+ ok(!treeRoot.expanded, "The root node should not be expanded yet.");
+ ok(!treeRoot.populated, "The root node should not be populated yet.");
+
+ is(
+ container.childNodes.length,
+ 1,
+ "The container node should have one child available."
+ );
+ is(
+ container.childNodes[0],
+ treeRoot.target,
+ "The root node's target is a child of the container node."
+ );
+
+ is(treeRoot.root, treeRoot, "The root node has the correct root.");
+ is(treeRoot.parent, null, "The root node has the correct parent.");
+ is(treeRoot.level, 0, "The root node has the correct level.");
+ is(
+ treeRoot.target.style.marginInlineStart,
+ "0px",
+ "The root node's indentation is correct."
+ );
+ is(
+ treeRoot.target.textContent,
+ "root",
+ "The root node's text contents are correct."
+ );
+ is(treeRoot.container, container, "The root node's container is correct.");
+
+ // Expand the root and test the child items...
+
+ let receivedExpandEvent = once(treeRoot, "expand", { spreadArgs: true });
+ let receivedFocusEvent = once(treeRoot, "focus");
+ await mousedown(treeRoot.target.querySelector(".arrow"));
+
+ let [eventItem] = await receivedExpandEvent;
+ is(eventItem, treeRoot, "The 'expand' event target is correct (1).");
+
+ await receivedFocusEvent;
+ is(
+ document.commandDispatcher.focusedElement,
+ treeRoot.target,
+ "The root node is now focused."
+ );
+
+ const fooItem = treeRoot.getChild(0);
+ const barItem = treeRoot.getChild(1);
+
+ is(
+ container.childNodes.length,
+ 3,
+ "The container node should now have three children available."
+ );
+ is(
+ container.childNodes[0],
+ treeRoot.target,
+ "The root node's target is a child of the container node."
+ );
+ is(
+ container.childNodes[1],
+ fooItem.target,
+ "The 'foo' node's target is a child of the container node."
+ );
+ is(
+ container.childNodes[2],
+ barItem.target,
+ "The 'bar' node's target is a child of the container node."
+ );
+
+ is(fooItem.root, treeRoot, "The 'foo' node has the correct root.");
+ is(fooItem.parent, treeRoot, "The 'foo' node has the correct parent.");
+ is(fooItem.level, 1, "The 'foo' node has the correct level.");
+ is(
+ fooItem.target.style.marginInlineStart,
+ "10px",
+ "The 'foo' node's indentation is correct."
+ );
+ is(
+ fooItem.target.textContent,
+ "foo",
+ "The 'foo' node's text contents are correct."
+ );
+ is(fooItem.container, container, "The 'foo' node's container is correct.");
+
+ is(barItem.root, treeRoot, "The 'bar' node has the correct root.");
+ is(barItem.parent, treeRoot, "The 'bar' node has the correct parent.");
+ is(barItem.level, 1, "The 'bar' node has the correct level.");
+ is(
+ barItem.target.style.marginInlineStart,
+ "10px",
+ "The 'bar' node's indentation is correct."
+ );
+ is(
+ barItem.target.textContent,
+ "bar",
+ "The 'bar' node's text contents are correct."
+ );
+ is(barItem.container, container, "The 'bar' node's container is correct.");
+
+ // Test clicking on the `foo` node...
+
+ receivedFocusEvent = once(treeRoot, "focus", { spreadArgs: true });
+ await mousedown(fooItem.target);
+
+ [eventItem] = await receivedFocusEvent;
+ is(eventItem, fooItem, "The 'focus' event target is correct (2).");
+ is(
+ document.commandDispatcher.focusedElement,
+ fooItem.target,
+ "The 'foo' node is now focused."
+ );
+
+ // Test double clicking on the `bar` node...
+
+ receivedExpandEvent = once(treeRoot, "expand", { spreadArgs: true });
+ receivedFocusEvent = once(treeRoot, "focus");
+ await dblclick(barItem.target);
+
+ [eventItem] = await receivedExpandEvent;
+ is(eventItem, barItem, "The 'expand' event target is correct (3).");
+
+ await receivedFocusEvent;
+ is(
+ document.commandDispatcher.focusedElement,
+ barItem.target,
+ "The 'foo' node is now focused."
+ );
+
+ // A child item got expanded, test the descendants...
+
+ const bazItem = barItem.getChild(0);
+
+ is(
+ container.childNodes.length,
+ 4,
+ "The container node should now have four children available."
+ );
+ is(
+ container.childNodes[0],
+ treeRoot.target,
+ "The root node's target is a child of the container node."
+ );
+ is(
+ container.childNodes[1],
+ fooItem.target,
+ "The 'foo' node's target is a child of the container node."
+ );
+ is(
+ container.childNodes[2],
+ barItem.target,
+ "The 'bar' node's target is a child of the container node."
+ );
+ is(
+ container.childNodes[3],
+ bazItem.target,
+ "The 'baz' node's target is a child of the container node."
+ );
+
+ is(bazItem.root, treeRoot, "The 'baz' node has the correct root.");
+ is(bazItem.parent, barItem, "The 'baz' node has the correct parent.");
+ is(bazItem.level, 2, "The 'baz' node has the correct level.");
+ is(
+ bazItem.target.style.marginInlineStart,
+ "20px",
+ "The 'baz' node's indentation is correct."
+ );
+ is(
+ bazItem.target.textContent,
+ "baz",
+ "The 'baz' node's text contents are correct."
+ );
+ is(bazItem.container, container, "The 'baz' node's container is correct.");
+
+ container.remove();
+});
diff --git a/devtools/client/performance/test/browser_perf-tree-abstract-02.js b/devtools/client/performance/test/browser_perf-tree-abstract-02.js
new file mode 100644
index 0000000000..0eeb539c32
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-abstract-02.js
@@ -0,0 +1,213 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the abstract tree base class for the profiler's tree view
+ * has a functional public API.
+ */
+
+const {
+ appendAndWaitForPaint,
+} = require("devtools/client/performance/test/helpers/dom-utils");
+const {
+ synthesizeCustomTreeClass,
+} = require("devtools/client/performance/test/helpers/synth-utils");
+
+add_task(async function() {
+ const { MyCustomTreeItem, myDataSrc } = synthesizeCustomTreeClass();
+
+ const container = document.createXULElement("vbox");
+ await appendAndWaitForPaint(gBrowser.selectedBrowser.parentNode, container);
+
+ // Populate the tree and test the root item...
+
+ const treeRoot = new MyCustomTreeItem(myDataSrc, { parent: null });
+ treeRoot.autoExpandDepth = 1;
+ treeRoot.attachTo(container);
+
+ ok(treeRoot.expanded, "The root node should now be expanded.");
+ ok(treeRoot.populated, "The root node should now be populated.");
+
+ const fooItem = treeRoot.getChild(0);
+ const barItem = treeRoot.getChild(1);
+ ok(
+ !fooItem.expanded && !barItem.expanded,
+ "The 'foo' and 'bar' nodes should not be expanded yet."
+ );
+ ok(
+ !fooItem.populated && !barItem.populated,
+ "The 'foo' and 'bar' nodes should not be populated yet."
+ );
+
+ fooItem.expand();
+ barItem.expand();
+ ok(
+ fooItem.expanded && barItem.expanded,
+ "The 'foo' and 'bar' nodes should now be expanded."
+ );
+ ok(
+ !fooItem.populated,
+ "The 'foo' node should not be populated because it's empty."
+ );
+ ok(barItem.populated, "The 'bar' node should now be populated.");
+
+ const bazItem = barItem.getChild(0);
+ ok(!bazItem.expanded, "The 'bar' node should not be expanded yet.");
+ ok(!bazItem.populated, "The 'bar' node should not be populated yet.");
+
+ bazItem.expand();
+ ok(bazItem.expanded, "The 'baz' node should now be expanded.");
+ ok(
+ !bazItem.populated,
+ "The 'baz' node should not be populated because it's empty."
+ );
+
+ ok(
+ !treeRoot.getChild(-1) && !treeRoot.getChild(2),
+ "Calling `getChild` with out of bounds indices will return null (1)."
+ );
+ ok(
+ !fooItem.getChild(-1) && !fooItem.getChild(0),
+ "Calling `getChild` with out of bounds indices will return null (2)."
+ );
+ ok(
+ !barItem.getChild(-1) && !barItem.getChild(1),
+ "Calling `getChild` with out of bounds indices will return null (3)."
+ );
+ ok(
+ !bazItem.getChild(-1) && !bazItem.getChild(0),
+ "Calling `getChild` with out of bounds indices will return null (4)."
+ );
+
+ // Finished expanding all nodes in the tree...
+ // Continue checking.
+
+ is(
+ container.childNodes.length,
+ 4,
+ "The container node should now have four children available."
+ );
+ is(
+ container.childNodes[0],
+ treeRoot.target,
+ "The root node's target is a child of the container node."
+ );
+ is(
+ container.childNodes[1],
+ fooItem.target,
+ "The 'foo' node's target is a child of the container node."
+ );
+ is(
+ container.childNodes[2],
+ barItem.target,
+ "The 'bar' node's target is a child of the container node."
+ );
+ is(
+ container.childNodes[3],
+ bazItem.target,
+ "The 'baz' node's target is a child of the container node."
+ );
+
+ treeRoot.collapse();
+ is(
+ container.childNodes.length,
+ 1,
+ "The container node should now have one children available."
+ );
+
+ ok(!treeRoot.expanded, "The root node should not be expanded anymore.");
+ ok(
+ fooItem.expanded && barItem.expanded && bazItem.expanded,
+ "The 'foo', 'bar' and 'baz' nodes should still be expanded."
+ );
+ ok(
+ treeRoot.populated && barItem.populated,
+ "The root and 'bar' nodes should still be populated."
+ );
+ ok(
+ !fooItem.populated && !bazItem.populated,
+ "The 'foo' and 'baz' nodes should still not be populated because they're empty."
+ );
+
+ treeRoot.expand();
+ is(
+ container.childNodes.length,
+ 4,
+ "The container node should now have four children available again."
+ );
+
+ ok(
+ treeRoot.expanded &&
+ fooItem.expanded &&
+ barItem.expanded &&
+ bazItem.expanded,
+ "The root, 'foo', 'bar' and 'baz' nodes should now be reexpanded."
+ );
+ ok(
+ treeRoot.populated && barItem.populated,
+ "The root and 'bar' nodes should still be populated."
+ );
+ ok(
+ !fooItem.populated && !bazItem.populated,
+ "The 'foo' and 'baz' nodes should still not be populated because they're empty."
+ );
+
+ // Test `focus` on the root node...
+
+ treeRoot.focus();
+ is(
+ document.commandDispatcher.focusedElement,
+ treeRoot.target,
+ "The root node is now focused."
+ );
+
+ // Test `focus` on a leaf node...
+
+ bazItem.focus();
+ is(
+ document.commandDispatcher.focusedElement,
+ bazItem.target,
+ "The 'baz' node is now focused."
+ );
+
+ // Test `remove`...
+
+ barItem.remove();
+ is(
+ container.childNodes.length,
+ 2,
+ "The container node should now have two children available."
+ );
+ is(
+ container.childNodes[0],
+ treeRoot.target,
+ "The root node should be the first in the container node."
+ );
+ is(
+ container.childNodes[1],
+ fooItem.target,
+ "The 'foo' node should be the second in the container node."
+ );
+
+ fooItem.remove();
+ is(
+ container.childNodes.length,
+ 1,
+ "The container node should now have one children available."
+ );
+ is(
+ container.childNodes[0],
+ treeRoot.target,
+ "The root node should be the only in the container node."
+ );
+
+ treeRoot.remove();
+ is(
+ container.childNodes.length,
+ 0,
+ "The container node should now have no children available."
+ );
+
+ container.remove();
+});
diff --git a/devtools/client/performance/test/browser_perf-tree-abstract-03.js b/devtools/client/performance/test/browser_perf-tree-abstract-03.js
new file mode 100644
index 0000000000..9ec289944b
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-abstract-03.js
@@ -0,0 +1,207 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the abstract tree base class for the profiler's tree view
+ * is keyboard accessible.
+ */
+
+const {
+ appendAndWaitForPaint,
+} = require("devtools/client/performance/test/helpers/dom-utils");
+const {
+ synthesizeCustomTreeClass,
+} = require("devtools/client/performance/test/helpers/synth-utils");
+
+add_task(async function() {
+ const { MyCustomTreeItem, myDataSrc } = synthesizeCustomTreeClass();
+
+ const container = document.createXULElement("vbox");
+ await appendAndWaitForPaint(gBrowser.selectedBrowser.parentNode, container);
+
+ // Populate the tree by pressing RIGHT...
+
+ const treeRoot = new MyCustomTreeItem(myDataSrc, { parent: null });
+ treeRoot.attachTo(container);
+ treeRoot.focus();
+
+ key("VK_RIGHT");
+ ok(treeRoot.expanded, "The root node is now expanded.");
+ is(
+ document.commandDispatcher.focusedElement,
+ treeRoot.target,
+ "The root node is still focused."
+ );
+
+ const fooItem = treeRoot.getChild(0);
+ const barItem = treeRoot.getChild(1);
+
+ key("VK_RIGHT");
+ ok(!fooItem.expanded, "The 'foo' node is not expanded yet.");
+ is(
+ document.commandDispatcher.focusedElement,
+ fooItem.target,
+ "The 'foo' node is now focused."
+ );
+
+ key("VK_RIGHT");
+ ok(fooItem.expanded, "The 'foo' node is now expanded.");
+ is(
+ document.commandDispatcher.focusedElement,
+ fooItem.target,
+ "The 'foo' node is still focused."
+ );
+
+ key("VK_RIGHT");
+ ok(!barItem.expanded, "The 'bar' node is not expanded yet.");
+ is(
+ document.commandDispatcher.focusedElement,
+ barItem.target,
+ "The 'bar' node is now focused."
+ );
+
+ key("VK_RIGHT");
+ ok(barItem.expanded, "The 'bar' node is now expanded.");
+ is(
+ document.commandDispatcher.focusedElement,
+ barItem.target,
+ "The 'bar' node is still focused."
+ );
+
+ const bazItem = barItem.getChild(0);
+
+ key("VK_RIGHT");
+ ok(!bazItem.expanded, "The 'baz' node is not expanded yet.");
+ is(
+ document.commandDispatcher.focusedElement,
+ bazItem.target,
+ "The 'baz' node is now focused."
+ );
+
+ key("VK_RIGHT");
+ ok(bazItem.expanded, "The 'baz' node is now expanded.");
+ is(
+ document.commandDispatcher.focusedElement,
+ bazItem.target,
+ "The 'baz' node is still focused."
+ );
+
+ // Test RIGHT on a leaf node.
+
+ key("VK_RIGHT");
+ is(
+ document.commandDispatcher.focusedElement,
+ bazItem.target,
+ "The 'baz' node is still focused."
+ );
+
+ // Test DOWN on a leaf node.
+
+ key("VK_DOWN");
+ is(
+ document.commandDispatcher.focusedElement,
+ bazItem.target,
+ "The 'baz' node is now refocused."
+ );
+
+ // Test UP.
+
+ key("VK_UP");
+ is(
+ document.commandDispatcher.focusedElement,
+ barItem.target,
+ "The 'bar' node is now refocused."
+ );
+
+ key("VK_UP");
+ is(
+ document.commandDispatcher.focusedElement,
+ fooItem.target,
+ "The 'foo' node is now refocused."
+ );
+
+ key("VK_UP");
+ is(
+ document.commandDispatcher.focusedElement,
+ treeRoot.target,
+ "The root node is now refocused."
+ );
+
+ // Test DOWN.
+
+ key("VK_DOWN");
+ is(
+ document.commandDispatcher.focusedElement,
+ fooItem.target,
+ "The 'foo' node is now refocused."
+ );
+
+ key("VK_DOWN");
+ is(
+ document.commandDispatcher.focusedElement,
+ barItem.target,
+ "The 'bar' node is now refocused."
+ );
+
+ key("VK_DOWN");
+ is(
+ document.commandDispatcher.focusedElement,
+ bazItem.target,
+ "The 'baz' node is now refocused."
+ );
+
+ // Test LEFT.
+
+ key("VK_LEFT");
+ ok(barItem.expanded, "The 'bar' node is still expanded.");
+ is(
+ document.commandDispatcher.focusedElement,
+ barItem.target,
+ "The 'bar' node is now refocused."
+ );
+
+ key("VK_LEFT");
+ ok(!barItem.expanded, "The 'bar' node is not expanded anymore.");
+ is(
+ document.commandDispatcher.focusedElement,
+ barItem.target,
+ "The 'bar' node is still focused."
+ );
+
+ key("VK_LEFT");
+ ok(treeRoot.expanded, "The root node is still expanded.");
+ is(
+ document.commandDispatcher.focusedElement,
+ treeRoot.target,
+ "The root node is now refocused."
+ );
+
+ key("VK_LEFT");
+ ok(!treeRoot.expanded, "The root node is not expanded anymore.");
+ is(
+ document.commandDispatcher.focusedElement,
+ treeRoot.target,
+ "The root node is still focused."
+ );
+
+ // Test LEFT on the root node.
+
+ key("VK_LEFT");
+ is(
+ document.commandDispatcher.focusedElement,
+ treeRoot.target,
+ "The root node is still focused."
+ );
+
+ // Test UP on the root node.
+
+ key("VK_UP");
+ is(
+ document.commandDispatcher.focusedElement,
+ treeRoot.target,
+ "The root node is still focused."
+ );
+
+ container.remove();
+});
diff --git a/devtools/client/performance/test/browser_perf-tree-abstract-04.js b/devtools/client/performance/test/browser_perf-tree-abstract-04.js
new file mode 100644
index 0000000000..fe5acb7be8
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-abstract-04.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the treeview expander arrow doesn't react to dblclick events.
+ */
+
+const {
+ appendAndWaitForPaint,
+} = require("devtools/client/performance/test/helpers/dom-utils");
+const {
+ synthesizeCustomTreeClass,
+} = require("devtools/client/performance/test/helpers/synth-utils");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(async function() {
+ const { MyCustomTreeItem, myDataSrc } = synthesizeCustomTreeClass();
+
+ const container = document.createXULElement("vbox");
+ await appendAndWaitForPaint(gBrowser.selectedBrowser.parentNode, container);
+
+ // Populate the tree and test the root item...
+
+ const treeRoot = new MyCustomTreeItem(myDataSrc, { parent: null });
+ treeRoot.attachTo(container);
+
+ const originalTreeRootExpandedState = treeRoot.expanded;
+ info("Double clicking on the root item arrow and waiting for focus event.");
+
+ const receivedFocusEvent = once(treeRoot, "focus");
+ await dblclick(treeRoot.target.querySelector(".arrow"));
+ await receivedFocusEvent;
+
+ is(
+ treeRoot.expanded,
+ originalTreeRootExpandedState,
+ "A double click on the arrow was ignored."
+ );
+
+ container.remove();
+});
diff --git a/devtools/client/performance/test/browser_perf-tree-abstract-05.js b/devtools/client/performance/test/browser_perf-tree-abstract-05.js
new file mode 100644
index 0000000000..55cdb39224
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-abstract-05.js
@@ -0,0 +1,139 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the abstract tree base class for the profiler's tree view
+ * supports PageUp/PageDown/Home/End keys.
+ */
+
+const {
+ appendAndWaitForPaint,
+} = require("devtools/client/performance/test/helpers/dom-utils");
+const {
+ synthesizeCustomTreeClass,
+} = require("devtools/client/performance/test/helpers/synth-utils");
+
+add_task(async function() {
+ const { MyCustomTreeItem } = synthesizeCustomTreeClass();
+
+ const container = document.createXULElement("vbox");
+ container.style.height = "100%";
+ container.style.overflow = "scroll";
+ const browserStack = gBrowser.selectedBrowser.parentNode;
+ // Allow the browser stack to shrink since it will have really long content
+ browserStack.style.minHeight = "0";
+ registerCleanupFunction(() => {
+ browserStack.style.removeProperty("min-height");
+ });
+ await appendAndWaitForPaint(browserStack, container);
+
+ const myDataSrc = {
+ label: "root",
+ children: [],
+ };
+
+ for (let i = 0; i < 1000; i++) {
+ myDataSrc.children.push({
+ label: "child-" + i,
+ children: [],
+ });
+ }
+
+ const treeRoot = new MyCustomTreeItem(myDataSrc, { parent: null });
+ treeRoot.attachTo(container);
+ treeRoot.focus();
+ treeRoot.expand();
+
+ is(
+ document.commandDispatcher.focusedElement,
+ treeRoot.target,
+ "The root node is focused."
+ );
+
+ // Test HOME and END
+
+ key("VK_END");
+ is(
+ document.commandDispatcher.focusedElement,
+ treeRoot.getChild(myDataSrc.children.length - 1).target,
+ "The last node is focused."
+ );
+
+ key("VK_HOME");
+ is(
+ document.commandDispatcher.focusedElement,
+ treeRoot.target,
+ "The first (root) node is focused."
+ );
+
+ // Test PageUp and PageDown
+
+ const nodesPerPageSize = treeRoot._getNodesPerPageSize();
+
+ key("VK_PAGE_DOWN");
+ is(
+ document.commandDispatcher.focusedElement,
+ treeRoot.getChild(nodesPerPageSize - 1).target,
+ "The first node in the second page is focused."
+ );
+
+ key("VK_PAGE_DOWN");
+ is(
+ document.commandDispatcher.focusedElement,
+ treeRoot.getChild(nodesPerPageSize * 2 - 1).target,
+ "The first node in the third page is focused."
+ );
+
+ key("VK_PAGE_UP");
+ is(
+ document.commandDispatcher.focusedElement,
+ treeRoot.getChild(nodesPerPageSize - 1).target,
+ "The first node in the second page is focused."
+ );
+
+ key("VK_PAGE_UP");
+ is(
+ document.commandDispatcher.focusedElement,
+ treeRoot.target,
+ "The first (root) node is focused."
+ );
+
+ // Test PageUp in the middle of the first page
+
+ let middleIndex = Math.floor(nodesPerPageSize / 2);
+
+ treeRoot.getChild(middleIndex).target.focus();
+ is(
+ document.commandDispatcher.focusedElement,
+ treeRoot.getChild(middleIndex).target,
+ "The middle node in the first page is focused."
+ );
+
+ key("VK_PAGE_UP");
+ is(
+ document.commandDispatcher.focusedElement,
+ treeRoot.target,
+ "The first (root) node is focused."
+ );
+
+ // Test PageDown in the middle of the last page
+
+ middleIndex = Math.ceil(myDataSrc.children.length - middleIndex);
+
+ treeRoot.getChild(middleIndex).target.focus();
+ is(
+ document.commandDispatcher.focusedElement,
+ treeRoot.getChild(middleIndex).target,
+ "The middle node in the last page is focused."
+ );
+
+ key("VK_PAGE_DOWN");
+ is(
+ document.commandDispatcher.focusedElement,
+ treeRoot.getChild(myDataSrc.children.length - 1).target,
+ "The last node is focused."
+ );
+
+ container.remove();
+});
diff --git a/devtools/client/performance/test/browser_perf-tree-view-01.js b/devtools/client/performance/test/browser_perf-tree-view-01.js
new file mode 100644
index 0000000000..35b2164e61
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-view-01.js
@@ -0,0 +1,122 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://foo/bar/creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the profiler's tree view implementation works properly and
+ * creates the correct column structure.
+ */
+
+const {
+ ThreadNode,
+} = require("devtools/client/performance/modules/logic/tree-model");
+const {
+ CallView,
+} = require("devtools/client/performance/modules/widgets/tree-view");
+const {
+ synthesizeProfile,
+} = require("devtools/client/performance/test/helpers/synth-utils");
+
+add_task(function() {
+ const profile = synthesizeProfile();
+ const threadNode = new ThreadNode(profile.threads[0], {
+ startTime: 0,
+ endTime: 20,
+ });
+
+ // Don't display the synthesized (root) and the real (root) node twice.
+ threadNode.calls = threadNode.calls[0].calls;
+
+ const treeRoot = new CallView({ frame: threadNode });
+ const container = document.createXULElement("vbox");
+ treeRoot.autoExpandDepth = 0;
+ treeRoot.attachTo(container);
+
+ is(
+ container.childNodes.length,
+ 1,
+ "The container node should have one child available."
+ );
+ is(
+ container.childNodes[0].className,
+ "call-tree-item",
+ "The root node in the tree has the correct class name."
+ );
+
+ is(
+ container.childNodes[0].childNodes.length,
+ 6,
+ "The root node in the tree has the correct number of children."
+ );
+ is(
+ container.childNodes[0].querySelectorAll(".call-tree-cell").length,
+ 6,
+ "The root node in the tree has only 6 'call-tree-cell' children."
+ );
+
+ is(
+ container.childNodes[0].childNodes[0].getAttribute("type"),
+ "duration",
+ "The root node in the tree has a duration cell."
+ );
+ is(
+ container.childNodes[0].childNodes[0].textContent.trim(),
+ "20 ms",
+ "The root node in the tree has the correct duration cell value."
+ );
+
+ is(
+ container.childNodes[0].childNodes[1].getAttribute("type"),
+ "percentage",
+ "The root node in the tree has a percentage cell."
+ );
+ is(
+ container.childNodes[0].childNodes[1].textContent.trim(),
+ "100%",
+ "The root node in the tree has the correct percentage cell value."
+ );
+
+ is(
+ container.childNodes[0].childNodes[2].getAttribute("type"),
+ "self-duration",
+ "The root node in the tree has a self-duration cell."
+ );
+ is(
+ container.childNodes[0].childNodes[2].textContent.trim(),
+ "0 ms",
+ "The root node in the tree has the correct self-duration cell value."
+ );
+
+ is(
+ container.childNodes[0].childNodes[3].getAttribute("type"),
+ "self-percentage",
+ "The root node in the tree has a self-percentage cell."
+ );
+ is(
+ container.childNodes[0].childNodes[3].textContent.trim(),
+ "0%",
+ "The root node in the tree has the correct self-percentage cell value."
+ );
+
+ is(
+ container.childNodes[0].childNodes[4].getAttribute("type"),
+ "samples",
+ "The root node in the tree has an samples cell."
+ );
+ is(
+ container.childNodes[0].childNodes[4].textContent.trim(),
+ "0",
+ "The root node in the tree has the correct samples cell value."
+ );
+
+ is(
+ container.childNodes[0].childNodes[5].getAttribute("type"),
+ "function",
+ "The root node in the tree has a function cell."
+ );
+ is(
+ container.childNodes[0].childNodes[5].style.marginInlineStart,
+ "0px",
+ "The root node in the tree has the correct indentation."
+ );
+});
diff --git a/devtools/client/performance/test/browser_perf-tree-view-02.js b/devtools/client/performance/test/browser_perf-tree-view-02.js
new file mode 100644
index 0000000000..222626e2eb
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-view-02.js
@@ -0,0 +1,287 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the profiler's tree view implementation works properly and
+ * creates the correct column structure after expanding some of the nodes.
+ * Also tests that demangling works.
+ */
+
+const {
+ ThreadNode,
+} = require("devtools/client/performance/modules/logic/tree-model");
+const {
+ CallView,
+} = require("devtools/client/performance/modules/widgets/tree-view");
+const {
+ synthesizeProfile,
+} = require("devtools/client/performance/test/helpers/synth-utils");
+
+const MANGLED_FN = "__Z3FooIiEvv";
+const UNMANGLED_FN = "void Foo<int>()";
+
+add_task(function() {
+ // Create a profile and mangle a function inside the string table.
+ const profile = synthesizeProfile();
+
+ profile.threads[0].stringTable[1] = profile.threads[0].stringTable[1].replace(
+ "A (",
+ `${MANGLED_FN} (`
+ );
+
+ const threadNode = new ThreadNode(profile.threads[0], {
+ startTime: 0,
+ endTime: 20,
+ });
+
+ // Don't display the synthesized (root) and the real (root) node twice.
+ threadNode.calls = threadNode.calls[0].calls;
+
+ const treeRoot = new CallView({ frame: threadNode });
+ const container = document.createXULElement("vbox");
+ treeRoot.autoExpandDepth = 0;
+ treeRoot.attachTo(container);
+
+ const $$ = node => container.querySelectorAll(node);
+ const $fun = (node, ancestor) =>
+ (ancestor || container).querySelector(
+ ".call-tree-cell[type=function] > " + node
+ );
+ const $$fun = (node, ancestor) =>
+ (ancestor || container).querySelectorAll(
+ ".call-tree-cell[type=function] > " + node
+ );
+ const $$dur = i =>
+ container.querySelectorAll(".call-tree-cell[type=duration]")[i];
+ const $$per = i =>
+ container.querySelectorAll(".call-tree-cell[type=percentage]")[i];
+ const $$sam = i =>
+ container.querySelectorAll(".call-tree-cell[type=samples]")[i];
+
+ is(
+ container.childNodes.length,
+ 1,
+ "The container node should have one child available."
+ );
+ is(
+ container.childNodes[0].className,
+ "call-tree-item",
+ "The root node in the tree has the correct class name."
+ );
+
+ is(
+ $$dur(0).textContent.trim(),
+ "20 ms",
+ "The root's duration cell displays the correct value."
+ );
+ is(
+ $$per(0).textContent.trim(),
+ "100%",
+ "The root's percentage cell displays the correct value."
+ );
+ is(
+ $$sam(0).textContent.trim(),
+ "0",
+ "The root's samples cell displays the correct value."
+ );
+ is(
+ $$fun(".call-tree-name")[0].textContent.trim(),
+ "(root)",
+ "The root's function cell displays the correct name."
+ );
+ is(
+ $$fun(".call-tree-url")[0],
+ undefined,
+ "The root's function cell displays no url."
+ );
+ is(
+ $$fun(".call-tree-line")[0],
+ undefined,
+ "The root's function cell displays no line."
+ );
+ is(
+ $$fun(".call-tree-host")[0],
+ undefined,
+ "The root's function cell displays no host."
+ );
+ is(
+ $$fun(".call-tree-category")[0],
+ undefined,
+ "The root's function cell displays no category."
+ );
+
+ treeRoot.expand();
+
+ is(
+ container.childNodes.length,
+ 2,
+ "The container node should have two children available."
+ );
+ is(
+ container.childNodes[0].className,
+ "call-tree-item",
+ "The root node in the tree has the correct class name."
+ );
+ is(
+ container.childNodes[1].className,
+ "call-tree-item",
+ "The .A node in the tree has the correct class name."
+ );
+
+ // Test demangling in the profiler tree.
+ is(
+ $$dur(1).textContent.trim(),
+ "20 ms",
+ "The .A node's duration cell displays the correct value."
+ );
+ is(
+ $$per(1).textContent.trim(),
+ "100%",
+ "The .A node's percentage cell displays the correct value."
+ );
+ is(
+ $$sam(1).textContent.trim(),
+ "0",
+ "The .A node's samples cell displays the correct value."
+ );
+
+ is(
+ $fun(".call-tree-name", $$(".call-tree-item")[1]).textContent.trim(),
+ UNMANGLED_FN,
+ "The .A node's function cell displays the correct name."
+ );
+ is(
+ $fun(".call-tree-url", $$(".call-tree-item")[1]).textContent.trim(),
+ "baz",
+ "The .A node's function cell displays the correct url."
+ );
+ is(
+ $fun(".call-tree-line", $$(".call-tree-item")[1]).textContent.trim(),
+ ":12",
+ "The .A node's function cell displays the correct line."
+ );
+ is(
+ $fun(".call-tree-host", $$(".call-tree-item")[1]).textContent.trim(),
+ "foo",
+ "The .A node's function cell displays the correct host."
+ );
+ is(
+ $fun(".call-tree-category", $$(".call-tree-item")[1]).textContent.trim(),
+ "JIT",
+ "The .A node's function cell displays the correct category."
+ );
+
+ ok(
+ $$(".call-tree-item")[1]
+ .getAttribute("tooltiptext")
+ .includes(MANGLED_FN),
+ "The .A node's row's tooltip contains the original mangled name."
+ );
+
+ const A = treeRoot.getChild();
+ A.expand();
+
+ is(
+ container.childNodes.length,
+ 4,
+ "The container node should have four children available."
+ );
+ is(
+ container.childNodes[0].className,
+ "call-tree-item",
+ "The root node in the tree has the correct class name."
+ );
+ is(
+ container.childNodes[1].className,
+ "call-tree-item",
+ "The .A node in the tree has the correct class name."
+ );
+ is(
+ container.childNodes[2].className,
+ "call-tree-item",
+ "The .B node in the tree has the correct class name."
+ );
+ is(
+ container.childNodes[3].className,
+ "call-tree-item",
+ "The .E node in the tree has the correct class name."
+ );
+
+ ok(
+ $fun(".call-tree-url", $$(".call-tree-item")[1])
+ .getAttribute("tooltiptext")
+ .includes("http://foo/bar/baz"),
+ "The .A node's function cell displays the correct url tooltiptext."
+ );
+ is(
+ $fun(".call-tree-line", $$(".call-tree-item")[1]).textContent.trim(),
+ ":12",
+ "The .A node's function cell displays the correct line."
+ );
+ is(
+ $fun(".call-tree-column", $$(".call-tree-item")[1]).textContent.trim(),
+ ":9",
+ "The .A node's function cell displays the correct column."
+ );
+ is(
+ $fun(".call-tree-host", $$(".call-tree-item")[1]).textContent.trim(),
+ "foo",
+ "The .A node's function cell displays the correct host."
+ );
+ is(
+ $$dur(2).textContent.trim(),
+ "15 ms",
+ "The .A.B node's duration cell displays the correct value."
+ );
+ is(
+ $$per(2).textContent.trim(),
+ "75%",
+ "The .A.B node's percentage cell displays the correct value."
+ );
+ is(
+ $$sam(2).textContent.trim(),
+ "0",
+ "The .A.B node's samples cell displays the correct value."
+ );
+ is(
+ $fun(".call-tree-name", $$(".call-tree-item")[2]).textContent.trim(),
+ "B InterruptibleLayout",
+ "The .A.B node's function cell displays the correct name."
+ );
+ is(
+ $fun(".call-tree-url", $$(".call-tree-item")[2]),
+ null,
+ "The .A.B node's function cell has no url."
+ );
+ is(
+ $fun(".call-tree-category", $$(".call-tree-item")[2]).textContent.trim(),
+ "Layout",
+ "The .A.B node's function cell displays the correct category."
+ );
+ is(
+ $$dur(3).textContent.trim(),
+ "5 ms",
+ "The .A.E node's duration cell displays the correct value."
+ );
+ is(
+ $$per(3).textContent.trim(),
+ "25%",
+ "The .A.E node's percentage cell displays the correct value."
+ );
+ is(
+ $$sam(3).textContent.trim(),
+ "0",
+ "The .A.E node's samples cell displays the correct value."
+ );
+ is(
+ $fun(".call-tree-name", $$(".call-tree-item")[3]).textContent.trim(),
+ "E",
+ "The .A.E node's function cell displays the correct name."
+ );
+ is(
+ $fun(".call-tree-category", $$(".call-tree-item")[3]).textContent.trim(),
+ "GC",
+ "The .A.E node's function cell displays the correct category."
+ );
+});
diff --git a/devtools/client/performance/test/browser_perf-tree-view-03.js b/devtools/client/performance/test/browser_perf-tree-view-03.js
new file mode 100644
index 0000000000..b69a71d51b
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-view-03.js
@@ -0,0 +1,163 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the profiler's tree view implementation works properly and
+ * creates the correct column structure and can auto-expand all nodes.
+ */
+
+const {
+ ThreadNode,
+} = require("devtools/client/performance/modules/logic/tree-model");
+const {
+ CallView,
+} = require("devtools/client/performance/modules/widgets/tree-view");
+const {
+ synthesizeProfile,
+} = require("devtools/client/performance/test/helpers/synth-utils");
+
+add_task(function() {
+ const profile = synthesizeProfile();
+ const threadNode = new ThreadNode(profile.threads[0], {
+ startTime: 0,
+ endTime: 20,
+ });
+
+ // Don't display the synthesized (root) and the real (root) node twice.
+ threadNode.calls = threadNode.calls[0].calls;
+
+ const treeRoot = new CallView({ frame: threadNode });
+ const container = document.createXULElement("vbox");
+ treeRoot.attachTo(container);
+
+ const $$fun = i =>
+ container.querySelectorAll(".call-tree-cell[type=function]")[i];
+ const $$nam = i =>
+ container.querySelectorAll(
+ ".call-tree-cell[type=function] > .call-tree-name"
+ )[i];
+ const $$dur = i =>
+ container.querySelectorAll(".call-tree-cell[type=duration]")[i];
+
+ is(
+ container.childNodes.length,
+ 7,
+ "The container node should have all children available."
+ );
+ is(
+ Array.from(container.childNodes).filter(
+ e => e.className != "call-tree-item"
+ ).length,
+ 0,
+ "All item nodes in the tree have the correct class name."
+ );
+
+ is(
+ $$fun(0).style.marginInlineStart,
+ "0px",
+ "The root node's function cell has the correct indentation."
+ );
+ is(
+ $$fun(1).style.marginInlineStart,
+ "16px",
+ "The .A node's function cell has the correct indentation."
+ );
+ is(
+ $$fun(2).style.marginInlineStart,
+ "32px",
+ "The .A.B node's function cell has the correct indentation."
+ );
+ is(
+ $$fun(3).style.marginInlineStart,
+ "48px",
+ "The .A.B.D node's function cell has the correct indentation."
+ );
+ is(
+ $$fun(4).style.marginInlineStart,
+ "48px",
+ "The .A.B.C node's function cell has the correct indentation."
+ );
+ is(
+ $$fun(5).style.marginInlineStart,
+ "32px",
+ "The .A.E node's function cell has the correct indentation."
+ );
+ is(
+ $$fun(6).style.marginInlineStart,
+ "48px",
+ "The .A.E.F node's function cell has the correct indentation."
+ );
+
+ is(
+ $$nam(0).textContent.trim(),
+ "(root)",
+ "The root node's function cell displays the correct name."
+ );
+ is(
+ $$nam(1).textContent.trim(),
+ "A",
+ "The .A node's function cell displays the correct name."
+ );
+ is(
+ $$nam(2).textContent.trim(),
+ "B InterruptibleLayout",
+ "The .A.B node's function cell displays the correct name."
+ );
+ is(
+ $$nam(3).textContent.trim(),
+ "D INTER_SLICE_GC",
+ "The .A.B.D node's function cell displays the correct name."
+ );
+ is(
+ $$nam(4).textContent.trim(),
+ "C",
+ "The .A.B.C node's function cell displays the correct name."
+ );
+ is(
+ $$nam(5).textContent.trim(),
+ "E",
+ "The .A.E node's function cell displays the correct name."
+ );
+ is(
+ $$nam(6).textContent.trim(),
+ "F",
+ "The .A.E.F node's function cell displays the correct name."
+ );
+
+ is(
+ $$dur(0).textContent.trim(),
+ "20 ms",
+ "The root node's function cell displays the correct duration."
+ );
+ is(
+ $$dur(1).textContent.trim(),
+ "20 ms",
+ "The .A node's function cell displays the correct duration."
+ );
+ is(
+ $$dur(2).textContent.trim(),
+ "15 ms",
+ "The .A.B node's function cell displays the correct duration."
+ );
+ is(
+ $$dur(3).textContent.trim(),
+ "10 ms",
+ "The .A.B.D node's function cell displays the correct duration."
+ );
+ is(
+ $$dur(4).textContent.trim(),
+ "5 ms",
+ "The .A.B.C node's function cell displays the correct duration."
+ );
+ is(
+ $$dur(5).textContent.trim(),
+ "5 ms",
+ "The .A.E node's function cell displays the correct duration."
+ );
+ is(
+ $$dur(6).textContent.trim(),
+ "5 ms",
+ "The .A.E.F node's function cell displays the correct duration."
+ );
+});
diff --git a/devtools/client/performance/test/browser_perf-tree-view-04.js b/devtools/client/performance/test/browser_perf-tree-view-04.js
new file mode 100644
index 0000000000..f3281634de
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-view-04.js
@@ -0,0 +1,158 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the profiler's tree view implementation works properly and
+ * creates the correct DOM nodes in the correct order.
+ */
+
+const {
+ ThreadNode,
+} = require("devtools/client/performance/modules/logic/tree-model");
+const {
+ CallView,
+} = require("devtools/client/performance/modules/widgets/tree-view");
+const {
+ synthesizeProfile,
+} = require("devtools/client/performance/test/helpers/synth-utils");
+
+add_task(function() {
+ const profile = synthesizeProfile();
+ const threadNode = new ThreadNode(profile.threads[0], {
+ startTime: 0,
+ endTime: 20,
+ });
+
+ // Don't display the synthesized (root) and the real (root) node twice.
+ threadNode.calls = threadNode.calls[0].calls;
+
+ const treeRoot = new CallView({ frame: threadNode });
+ const container = document.createXULElement("vbox");
+ treeRoot.attachTo(container);
+
+ is(
+ treeRoot.target.getAttribute("origin"),
+ "chrome",
+ "The root node's 'origin' attribute is correct."
+ );
+ is(
+ treeRoot.target.getAttribute("category"),
+ "",
+ "The root node's 'category' attribute is correct."
+ );
+ is(
+ treeRoot.target.getAttribute("tooltiptext"),
+ "",
+ "The root node's 'tooltiptext' attribute is correct."
+ );
+ is(
+ treeRoot.target.querySelector(".call-tree-category"),
+ null,
+ "The root node's category label cell should be hidden."
+ );
+
+ const A = treeRoot.getChild();
+ const B = A.getChild();
+ const D = B.getChild();
+
+ is(
+ A.target.getAttribute("tooltiptext"),
+ "A (http://foo/bar/baz:12:9)",
+ "The .A node's 'tooltiptext' attribute is correct"
+ );
+ is(
+ D.target.getAttribute("origin"),
+ "chrome",
+ "The .A.B.D node's 'origin' attribute is correct."
+ );
+ is(
+ D.target.getAttribute("category"),
+ "gc",
+ "The .A.B.D node's 'category' attribute is correct."
+ );
+ is(
+ D.target.getAttribute("tooltiptext"),
+ "D INTER_SLICE_GC",
+ "The .A.B.D node's 'tooltiptext' attribute is correct."
+ );
+
+ is(
+ D.target.childNodes.length,
+ 6,
+ "The number of columns displayed for tree items is correct."
+ );
+ is(
+ D.target.childNodes[0].getAttribute("type"),
+ "duration",
+ "The first column displayed for tree items is correct."
+ );
+ is(
+ D.target.childNodes[1].getAttribute("type"),
+ "percentage",
+ "The third column displayed for tree items is correct."
+ );
+ is(
+ D.target.childNodes[2].getAttribute("type"),
+ "self-duration",
+ "The second column displayed for tree items is correct."
+ );
+ is(
+ D.target.childNodes[3].getAttribute("type"),
+ "self-percentage",
+ "The fourth column displayed for tree items is correct."
+ );
+ is(
+ D.target.childNodes[4].getAttribute("type"),
+ "samples",
+ "The fifth column displayed for tree items is correct."
+ );
+ is(
+ D.target.childNodes[5].getAttribute("type"),
+ "function",
+ "The sixth column displayed for tree items is correct."
+ );
+
+ const functionCell = A.target.childNodes[5];
+
+ is(
+ functionCell.childNodes.length,
+ 7,
+ "The number of columns displayed for function cells is correct."
+ );
+ is(
+ functionCell.childNodes[0].className,
+ "arrow theme-twisty",
+ "The first node displayed for function cells is correct."
+ );
+ is(
+ functionCell.childNodes[1].className,
+ "plain call-tree-name",
+ "The second node displayed for function cells is correct."
+ );
+ is(
+ functionCell.childNodes[2].className,
+ "plain call-tree-url",
+ "The third node displayed for function cells is correct."
+ );
+ is(
+ functionCell.childNodes[3].className,
+ "plain call-tree-line",
+ "The fourth node displayed for function cells is correct."
+ );
+ is(
+ functionCell.childNodes[4].className,
+ "plain call-tree-column",
+ "The fifth node displayed for function cells is correct."
+ );
+ is(
+ functionCell.childNodes[5].className,
+ "plain call-tree-host",
+ "The sixth node displayed for function cells is correct."
+ );
+ is(
+ functionCell.childNodes[6].className,
+ "plain call-tree-category",
+ "The seventh node displayed for function cells is correct."
+ );
+});
diff --git a/devtools/client/performance/test/browser_perf-tree-view-05.js b/devtools/client/performance/test/browser_perf-tree-view-05.js
new file mode 100644
index 0000000000..a70129717b
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-view-05.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the profiler's tree view implementation works properly and
+ * can toggle categories hidden or visible.
+ */
+
+const {
+ ThreadNode,
+} = require("devtools/client/performance/modules/logic/tree-model");
+const {
+ CallView,
+} = require("devtools/client/performance/modules/widgets/tree-view");
+const {
+ synthesizeProfile,
+} = require("devtools/client/performance/test/helpers/synth-utils");
+
+add_task(function() {
+ const profile = synthesizeProfile();
+ const threadNode = new ThreadNode(profile.threads[0], {
+ startTime: 0,
+ endTime: 20,
+ });
+
+ // Don't display the synthesized (root) and the real (root) node twice.
+ threadNode.calls = threadNode.calls[0].calls;
+
+ const treeRoot = new CallView({ frame: threadNode });
+ const container = document.createXULElement("vbox");
+ treeRoot.attachTo(container);
+
+ const categories = container.querySelectorAll(".call-tree-category");
+ is(
+ categories.length,
+ 6,
+ "The call tree displays a correct number of categories."
+ );
+ ok(
+ !container.hasAttribute("categories-hidden"),
+ "All categories should be visible in the tree."
+ );
+
+ treeRoot.toggleCategories(false);
+ is(
+ categories.length,
+ 6,
+ "The call tree displays the same number of categories."
+ );
+ ok(
+ container.hasAttribute("categories-hidden"),
+ "All categories should now be hidden in the tree."
+ );
+});
diff --git a/devtools/client/performance/test/browser_perf-tree-view-06.js b/devtools/client/performance/test/browser_perf-tree-view-06.js
new file mode 100644
index 0000000000..1a51be9ea1
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-view-06.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the profiler's tree view implementation works properly and
+ * correctly emits events when certain DOM nodes are clicked.
+ */
+
+const {
+ ThreadNode,
+} = require("devtools/client/performance/modules/logic/tree-model");
+const {
+ CallView,
+} = require("devtools/client/performance/modules/widgets/tree-view");
+const {
+ synthesizeProfile,
+} = require("devtools/client/performance/test/helpers/synth-utils");
+const {
+ idleWait,
+ waitUntil,
+} = require("devtools/client/performance/test/helpers/wait-utils");
+
+add_task(async function() {
+ const profile = synthesizeProfile();
+ const threadNode = new ThreadNode(profile.threads[0], {
+ startTime: 0,
+ endTime: 20,
+ });
+
+ // Don't display the synthesized (root) and the real (root) node twice.
+ threadNode.calls = threadNode.calls[0].calls;
+
+ const treeRoot = new CallView({ frame: threadNode });
+ const container = document.createXULElement("vbox");
+ treeRoot.attachTo(container);
+
+ const A = treeRoot.getChild();
+
+ let linkEvent = null;
+ const handler = e => {
+ linkEvent = e;
+ };
+
+ treeRoot.on("link", handler);
+
+ // Fire right click.
+ await rightMousedown(A.target.querySelector(".call-tree-url"));
+
+ // Ensure link was not called for right click.
+ await idleWait(100);
+ ok(!linkEvent, "The `link` event not fired for right click.");
+
+ // Fire left click.
+ await mousedown(A.target.querySelector(".call-tree-url"));
+
+ // Ensure link was called for left click.
+ await waitUntil(() => linkEvent);
+ is(linkEvent, A, "The `link` event target is correct.");
+
+ treeRoot.off("link", handler);
+});
diff --git a/devtools/client/performance/test/browser_perf-tree-view-07.js b/devtools/client/performance/test/browser_perf-tree-view-07.js
new file mode 100644
index 0000000000..a15ff1cbc0
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-view-07.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the profiler's tree view implementation works properly and
+ * has the correct 'root', 'parent', 'level' etc. accessors on child nodes.
+ */
+
+const {
+ ThreadNode,
+} = require("devtools/client/performance/modules/logic/tree-model");
+const {
+ CallView,
+} = require("devtools/client/performance/modules/widgets/tree-view");
+const {
+ synthesizeProfile,
+} = require("devtools/client/performance/test/helpers/synth-utils");
+
+add_task(function() {
+ const profile = synthesizeProfile();
+ const threadNode = new ThreadNode(profile.threads[0], {
+ startTime: 0,
+ endTime: 20,
+ });
+
+ // Don't display the synthesized (root) and the real (root) node twice.
+ threadNode.calls = threadNode.calls[0].calls;
+
+ const treeRoot = new CallView({ frame: threadNode });
+ const container = document.createXULElement("vbox");
+ container.id = "call-tree-container";
+ treeRoot.attachTo(container);
+
+ const A = treeRoot.getChild();
+ const B = A.getChild();
+ const D = B.getChild();
+
+ is(D.root, treeRoot, "The .A.B.D node has the correct root.");
+ is(D.parent, B, "The .A.B.D node has the correct parent.");
+ is(D.level, 3, "The .A.B.D node has the correct level.");
+ is(
+ D.target.className,
+ "call-tree-item",
+ "The .A.B.D node has the correct target node."
+ );
+ is(
+ D.container.id,
+ "call-tree-container",
+ "The .A.B.D node has the correct container node."
+ );
+});
diff --git a/devtools/client/performance/test/browser_perf-tree-view-08.js b/devtools/client/performance/test/browser_perf-tree-view-08.js
new file mode 100644
index 0000000000..f04029ab18
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-view-08.js
@@ -0,0 +1,155 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the profiler's tree view renders generalized platform data
+ * when `contentOnly` is on correctly.
+ */
+
+const {
+ ThreadNode,
+} = require("devtools/client/performance/modules/logic/tree-model");
+const {
+ CallView,
+} = require("devtools/client/performance/modules/widgets/tree-view");
+const {
+ CATEGORY_INDEX,
+} = require("devtools/client/performance/modules/categories");
+const RecordingUtils = require("devtools/shared/performance/recording-utils");
+
+add_task(function() {
+ const threadNode = new ThreadNode(gProfile.threads[0], {
+ startTime: 0,
+ endTime: 20,
+ contentOnly: true,
+ });
+
+ // Don't display the synthesized (root) and the real (root) node twice.
+ threadNode.calls = threadNode.calls[0].calls;
+
+ const treeRoot = new CallView({ frame: threadNode, autoExpandDepth: 10 });
+ const container = document.createXULElement("vbox");
+ treeRoot.attachTo(container);
+
+ /*
+ * (root)
+ * - A
+ * - B
+ * - C
+ * - D
+ * - (GC)
+ * - E
+ * - F
+ * - (JS)
+ * - (JS)
+ */
+
+ const A = treeRoot.getChild(0);
+ const JS = treeRoot.getChild(1);
+ const GC = A.getChild(1);
+ const JS2 = A.getChild(2)
+ .getChild()
+ .getChild();
+
+ is(
+ JS.target.getAttribute("category"),
+ "js",
+ "Generalized JS node has correct category"
+ );
+ is(
+ JS.target.getAttribute("tooltiptext"),
+ "JIT",
+ "Generalized JS node has correct category"
+ );
+ is(
+ JS.target.querySelector(".call-tree-name").textContent.trim(),
+ "JIT",
+ "Generalized JS node has correct display value as just the category name."
+ );
+
+ is(
+ JS2.target.getAttribute("category"),
+ "js",
+ "Generalized second JS node has correct category"
+ );
+ is(
+ JS2.target.getAttribute("tooltiptext"),
+ "JIT",
+ "Generalized second JS node has correct category"
+ );
+ is(
+ JS2.target.querySelector(".call-tree-name").textContent.trim(),
+ "JIT",
+ "Generalized second JS node has correct display value as just the category name."
+ );
+
+ is(
+ GC.target.getAttribute("category"),
+ "gc",
+ "Generalized GC node has correct category"
+ );
+ is(
+ GC.target.getAttribute("tooltiptext"),
+ "GC",
+ "Generalized GC node has correct category"
+ );
+ is(
+ GC.target.querySelector(".call-tree-name").textContent.trim(),
+ "GC",
+ "Generalized GC node has correct display value as just the category name."
+ );
+});
+
+const gProfile = RecordingUtils.deflateProfile({
+ meta: { version: 2 },
+ threads: [
+ {
+ samples: [
+ {
+ time: 1,
+ frames: [
+ { location: "(root)" },
+ { location: "http://content/A" },
+ { location: "http://content/B" },
+ { location: "http://content/C" },
+ ],
+ },
+ {
+ time: 1 + 1,
+ frames: [
+ { location: "(root)" },
+ { location: "http://content/A" },
+ { location: "http://content/B" },
+ { location: "http://content/D" },
+ ],
+ },
+ {
+ time: 1 + 1 + 2,
+ frames: [
+ { location: "(root)" },
+ { location: "http://content/A" },
+ { location: "http://content/E" },
+ { location: "http://content/F" },
+ { location: "platform_JS", category: CATEGORY_INDEX("js") },
+ ],
+ },
+ {
+ time: 1 + 1 + 2 + 3,
+ frames: [
+ { location: "(root)" },
+ { location: "platform_JS2", category: CATEGORY_INDEX("js") },
+ ],
+ },
+ {
+ time: 1 + 1 + 2 + 3 + 5,
+ frames: [
+ { location: "(root)" },
+ { location: "http://content/A" },
+ { location: "platform_GC", category: CATEGORY_INDEX("gc") },
+ ],
+ },
+ ],
+ },
+ ],
+});
diff --git a/devtools/client/performance/test/browser_perf-tree-view-09.js b/devtools/client/performance/test/browser_perf-tree-view-09.js
new file mode 100644
index 0000000000..651a09685f
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-view-09.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the profiler's tree view sorts inverted call trees by
+ * "self cost" and not "total cost".
+ */
+
+const {
+ ThreadNode,
+} = require("devtools/client/performance/modules/logic/tree-model");
+const {
+ CallView,
+} = require("devtools/client/performance/modules/widgets/tree-view");
+const RecordingUtils = require("devtools/shared/performance/recording-utils");
+
+add_task(function() {
+ const threadNode = new ThreadNode(gProfile.threads[0], {
+ startTime: 0,
+ endTime: 20,
+ invertTree: true,
+ });
+ const treeRoot = new CallView({ frame: threadNode, inverted: true });
+ const container = document.createXULElement("vbox");
+ treeRoot.attachTo(container);
+
+ is(
+ treeRoot.getChild(0).frame.location,
+ "B",
+ "The tree root's first child is the `B` function."
+ );
+ is(
+ treeRoot.getChild(1).frame.location,
+ "A",
+ "The tree root's second child is the `A` function."
+ );
+});
+
+const gProfile = RecordingUtils.deflateProfile({
+ meta: { version: 2 },
+ threads: [
+ {
+ samples: [
+ {
+ time: 1,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ ],
+ },
+ {
+ time: 2,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ ],
+ },
+ {
+ time: 3,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ ],
+ },
+ {
+ time: 4,
+ frames: [{ location: "(root)" }, { location: "A" }],
+ },
+ ],
+ },
+ ],
+});
diff --git a/devtools/client/performance/test/browser_perf-tree-view-10.js b/devtools/client/performance/test/browser_perf-tree-view-10.js
new file mode 100644
index 0000000000..8b50ea9092
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-view-10.js
@@ -0,0 +1,195 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the profiler's tree view, when inverted, displays the self and
+ * total costs correctly.
+ */
+
+const {
+ ThreadNode,
+} = require("devtools/client/performance/modules/logic/tree-model");
+const {
+ CallView,
+} = require("devtools/client/performance/modules/widgets/tree-view");
+const RecordingUtils = require("devtools/shared/performance/recording-utils");
+
+add_task(function() {
+ const threadNode = new ThreadNode(gProfile.threads[0], {
+ startTime: 0,
+ endTime: 50,
+ invertTree: true,
+ });
+ const treeRoot = new CallView({ frame: threadNode, inverted: true });
+ const container = document.createXULElement("vbox");
+ treeRoot.attachTo(container);
+
+ // Add 1 to each index to skip the hidden root node
+ const $$nam = i =>
+ container.querySelectorAll(
+ ".call-tree-cell[type=function] > .call-tree-name"
+ )[i + 1];
+ const $$per = i =>
+ container.querySelectorAll(".call-tree-cell[type=percentage]")[i + 1];
+ const $$selfper = i =>
+ container.querySelectorAll(".call-tree-cell[type='self-percentage']")[
+ i + 1
+ ];
+
+ /**
+ * Samples:
+ *
+ * A->C
+ * A->B
+ * A->B->C x4
+ * A->B->D x4
+ *
+ * Expected:
+ *
+ * +--total--+--self--+--tree----+
+ * | 50% | 50% | C |
+ * | 40% | 0 | -> B |
+ * | 30% | 0 | -> A |
+ * | 10% | 0 | -> A |
+ * | 40% | 40% | D |
+ * | 40% | 0 | -> B |
+ * | 40% | 0 | -> A |
+ * | 10% | 10% | B |
+ * | 10% | 0 | -> A |
+ * +---------+--------+----------+
+ */
+
+ is(
+ container.childNodes.length,
+ 10,
+ "The container node should have all children available."
+ );
+
+ // total, self, indent + name
+ [
+ [50, 50, "C"],
+ [40, 0, " B"],
+ [30, 0, " A"],
+ [10, 0, " A"],
+ [40, 40, "D"],
+ [40, 0, " B"],
+ [40, 0, " A"],
+ [10, 10, "B"],
+ [10, 0, " A"],
+ ].forEach(function(def, i) {
+ info(`Checking ${i}th tree item.`);
+
+ let [total, self, name] = def;
+ name = name.trim();
+
+ is($$nam(i).textContent.trim(), name, `${name} has correct name.`);
+ is(
+ $$per(i).textContent.trim(),
+ `${total}%`,
+ `${name} has correct total percent.`
+ );
+ is(
+ $$selfper(i).textContent.trim(),
+ `${self}%`,
+ `${name} has correct self percent.`
+ );
+ });
+});
+
+const gProfile = RecordingUtils.deflateProfile({
+ meta: { version: 2 },
+ threads: [
+ {
+ samples: [
+ {
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" },
+ ],
+ },
+ {
+ time: 10,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" },
+ ],
+ },
+ {
+ time: 15,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "C" },
+ ],
+ },
+ {
+ time: 20,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ ],
+ },
+ {
+ time: 25,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" },
+ ],
+ },
+ {
+ time: 30,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" },
+ ],
+ },
+ {
+ time: 35,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" },
+ ],
+ },
+ {
+ time: 40,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" },
+ ],
+ },
+ {
+ time: 45,
+ frames: [
+ { location: "(root)" },
+ { location: "B" },
+ { location: "C" },
+ ],
+ },
+ {
+ time: 50,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" },
+ ],
+ },
+ ],
+ },
+ ],
+});
diff --git a/devtools/client/performance/test/browser_perf-tree-view-11.js b/devtools/client/performance/test/browser_perf-tree-view-11.js
new file mode 100644
index 0000000000..3a25f6c1dc
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-view-11.js
@@ -0,0 +1,154 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/* eslint-disable */
+/**
+ * Tests that if `show-jit-optimizations` is true, then an
+ * icon is next to the frame with optimizations
+ */
+
+var { CATEGORY_MASK } = require("devtools/client/performance/modules/categories");
+
+async function spawnTest() {
+ let { panel } = await initPerformance(SIMPLE_URL);
+ let { EVENTS, $, $$, window, PerformanceController } = panel.panelWin;
+ let { OverviewView, DetailsView, JsCallTreeView } = panel.panelWin;
+
+ let profilerData = { threads: [gThread] };
+
+ Services.prefs.setBoolPref(JIT_PREF, true);
+ Services.prefs.setBoolPref(PLATFORM_DATA_PREF, false);
+ Services.prefs.setBoolPref(INVERT_PREF, false);
+
+ // Make two recordings, so we have one to switch to later, as the
+ // second one will have fake sample data
+ await startRecording(panel);
+ await stopRecording(panel);
+
+ await DetailsView.selectView("js-calltree");
+
+ await injectAndRenderProfilerData();
+
+ let rows = $$("#js-calltree-view .call-tree-item");
+ is(rows.length, 4, "4 call tree rows exist");
+ for (let row of rows) {
+ let name = $(".call-tree-name", row).textContent.trim();
+ switch (name) {
+ case "A":
+ ok($(".opt-icon", row), "found an opt icon on a leaf node with opt data");
+ break;
+ case "C":
+ ok(!$(".opt-icon", row), "frames without opt data do not have an icon");
+ break;
+ case "Gecko":
+ ok(!$(".opt-icon", row), "meta category frames with opt data do not have an icon");
+ break;
+ case "(root)":
+ ok(!$(".opt-icon", row), "root frame certainly does not have opt data");
+ break;
+ default:
+ ok(false, `Unidentified frame: ${name}`);
+ break;
+ }
+ }
+
+ await teardown(panel);
+ finish();
+
+ async function injectAndRenderProfilerData() {
+ // Get current recording and inject our mock data
+ info("Injecting mock profile data");
+ let recording = PerformanceController.getCurrentRecording();
+ recording._profile = profilerData;
+
+ // Force a rerender
+ let rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ JsCallTreeView.render(OverviewView.getTimeInterval());
+ await rendered;
+ }
+}
+
+var gUniqueStacks = new RecordingUtils.UniqueStacks();
+
+function uniqStr(s) {
+ return gUniqueStacks.getOrAddStringIndex(s);
+}
+
+// Since deflateThread doesn't handle deflating optimization info, use
+// placeholder names A_O1, B_O2, and B_O3, which will be used to manually
+// splice deduped opts into the profile.
+var gThread = RecordingUtils.deflateThread({
+ samples: [{
+ time: 0,
+ frames: [
+ { location: "(root)" }
+ ]
+ }, {
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "A (http://foo:1)" },
+ ]
+ }, {
+ time: 5 + 1,
+ frames: [
+ { location: "(root)" },
+ { location: "C (http://foo/bar/baz:56)" }
+ ]
+ }, {
+ time: 5 + 1 + 2,
+ frames: [
+ { location: "(root)" },
+ { category: CATEGORY_MASK("other"), location: "PlatformCode" }
+ ]
+ }],
+ markers: []
+}, gUniqueStacks);
+
+// 3 RawOptimizationSites
+var gRawSite1 = {
+ _testFrameInfo: { name: "A", line: "12", file: "@baz" },
+ line: 12,
+ column: 2,
+ types: [{
+ mirType: uniqStr("Object"),
+ site: uniqStr("A (http://foo/bar/bar:12)"),
+ typeset: [{
+ keyedBy: uniqStr("constructor"),
+ name: uniqStr("Foo"),
+ location: uniqStr("A (http://foo/bar/baz:12)")
+ }, {
+ keyedBy: uniqStr("primitive"),
+ location: uniqStr("self-hosted")
+ }]
+ }],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ [uniqStr("Failure3"), uniqStr("SomeGetter3")]
+ ]
+ }
+};
+
+gThread.frameTable.data.forEach((frame) => {
+ const LOCATION_SLOT = gThread.frameTable.schema.location;
+ const OPTIMIZATIONS_SLOT = gThread.frameTable.schema.optimizations;
+
+ let l = gThread.stringTable[frame[LOCATION_SLOT]];
+ switch (l) {
+ case "A (http://foo:1)":
+ frame[LOCATION_SLOT] = uniqStr("A (http://foo:1)");
+ frame[OPTIMIZATIONS_SLOT] = gRawSite1;
+ break;
+ case "PlatformCode":
+ frame[LOCATION_SLOT] = uniqStr("PlatformCode");
+ frame[OPTIMIZATIONS_SLOT] = gRawSite1;
+ break;
+ }
+});
+/* eslint-enable */
diff --git a/devtools/client/performance/test/browser_perf-ui-recording.js b/devtools/client/performance/test/browser_perf-ui-recording.js
new file mode 100644
index 0000000000..fbf1f4611d
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-ui-recording.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the controller handles recording via the `stopwatch` button
+ * in the UI.
+ */
+
+const {
+ pmmInitWithBrowser,
+ pmmIsProfilerActive,
+} = require("devtools/client/performance/test/helpers/profiler-mm-utils");
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+} = require("devtools/client/performance/test/helpers/actions");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ pmmInitWithBrowser(gBrowser);
+
+ ok(
+ !(await pmmIsProfilerActive()),
+ "The built-in profiler module should not have been automatically started."
+ );
+
+ await startRecording(panel);
+
+ ok(
+ await pmmIsProfilerActive(),
+ "The built-in profiler module should now be active."
+ );
+
+ await stopRecording(panel);
+
+ ok(
+ await pmmIsProfilerActive(),
+ "The built-in profiler module should still be active."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_timeline-filters-01.js b/devtools/client/performance/test/browser_timeline-filters-01.js
new file mode 100644
index 0000000000..ff9698cbf8
--- /dev/null
+++ b/devtools/client/performance/test/browser_timeline-filters-01.js
@@ -0,0 +1,119 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable */
+
+/**
+ * Tests markers filtering mechanism.
+ */
+
+const EPSILON = 0.00000001;
+
+async function spawnTest() {
+ let { panel } = await initPerformance(SIMPLE_URL);
+ let { $, $$, EVENTS, PerformanceController, OverviewView, WaterfallView } = panel.panelWin;
+ let { TimelineGraph } = require("devtools/client/performance/modules/widgets/graphs");
+ let { rowHeight: MARKERS_GRAPH_ROW_HEIGHT } = TimelineGraph.prototype;
+
+ await startRecording(panel);
+ ok(true, "Recording has started.");
+
+ await waitUntil(() => {
+ // Wait until we get 3 different markers.
+ let markers = PerformanceController.getCurrentRecording().getMarkers();
+ return markers.some(m => m.name == "Styles") &&
+ markers.some(m => m.name == "Reflow") &&
+ markers.some(m => m.name == "Paint");
+ });
+
+ await stopRecording(panel);
+ ok(true, "Recording has ended.");
+
+ // Push some fake markers of a type we do not have a blueprint for
+ let markers = PerformanceController.getCurrentRecording().getMarkers();
+ let endTime = markers[markers.length - 1].end;
+ markers.push({ name: "CustomMarker", start: endTime + EPSILON, end: endTime + (EPSILON * 2) });
+ markers.push({ name: "CustomMarker", start: endTime + (EPSILON * 3), end: endTime + (EPSILON * 4) });
+
+ // Invalidate marker cache
+ WaterfallView._cache.delete(markers);
+
+ // Select everything
+ let waterfallRendered = WaterfallView.once(EVENTS.UI_WATERFALL_RENDERED);
+ OverviewView.setTimeInterval({ startTime: 0, endTime: Number.MAX_VALUE });
+
+ $("#filter-button").click();
+ let menuItem1 = $("menuitem[marker-type=Styles]");
+ let menuItem2 = $("menuitem[marker-type=Reflow]");
+ let menuItem3 = $("menuitem[marker-type=Paint]");
+ let menuItem4 = $("menuitem[marker-type=UNKNOWN]");
+
+ let overview = OverviewView.graphs.get("timeline");
+ let originalHeight = overview.fixedHeight;
+
+ await waterfallRendered;
+
+ ok($(".waterfall-marker-bar[type=Styles]"), "Found at least one 'Styles' marker (1)");
+ ok($(".waterfall-marker-bar[type=Reflow]"), "Found at least one 'Reflow' marker (1)");
+ ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker (1)");
+ ok($(".waterfall-marker-bar[type=CustomMarker]"), "Found at least one 'Unknown' marker (1)");
+
+ let heightBefore = overview.fixedHeight;
+ EventUtils.synthesizeMouseAtCenter(menuItem1, {type: "mouseup"}, panel.panelWin);
+ await waitForOverviewAndCommand(overview, menuItem1);
+
+ is(overview.fixedHeight, heightBefore, "Overview height hasn't changed");
+ ok(!$(".waterfall-marker-bar[type=Styles]"), "No 'Styles' marker (2)");
+ ok($(".waterfall-marker-bar[type=Reflow]"), "Found at least one 'Reflow' marker (2)");
+ ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker (2)");
+ ok($(".waterfall-marker-bar[type=CustomMarker]"), "Found at least one 'Unknown' marker (2)");
+
+ heightBefore = overview.fixedHeight;
+ EventUtils.synthesizeMouseAtCenter(menuItem2, {type: "mouseup"}, panel.panelWin);
+ await waitForOverviewAndCommand(overview, menuItem2);
+
+ is(overview.fixedHeight, heightBefore, "Overview height hasn't changed");
+ ok(!$(".waterfall-marker-bar[type=Styles]"), "No 'Styles' marker (3)");
+ ok(!$(".waterfall-marker-bar[type=Reflow]"), "No 'Reflow' marker (3)");
+ ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker (3)");
+ ok($(".waterfall-marker-bar[type=CustomMarker]"), "Found at least one 'Unknown' marker (3)");
+
+ heightBefore = overview.fixedHeight;
+ EventUtils.synthesizeMouseAtCenter(menuItem3, {type: "mouseup"}, panel.panelWin);
+ await waitForOverviewAndCommand(overview, menuItem3);
+
+ is(overview.fixedHeight, heightBefore - MARKERS_GRAPH_ROW_HEIGHT, "Overview is smaller");
+ ok(!$(".waterfall-marker-bar[type=Styles]"), "No 'Styles' marker (4)");
+ ok(!$(".waterfall-marker-bar[type=Reflow]"), "No 'Reflow' marker (4)");
+ ok(!$(".waterfall-marker-bar[type=Paint]"), "No 'Paint' marker (4)");
+ ok($(".waterfall-marker-bar[type=CustomMarker]"), "Found at least one 'Unknown' marker (4)");
+
+ EventUtils.synthesizeMouseAtCenter(menuItem4, {type: "mouseup"}, panel.panelWin);
+ await waitForOverviewAndCommand(overview, menuItem4);
+
+ ok(!$(".waterfall-marker-bar[type=Styles]"), "No 'Styles' marker (5)");
+ ok(!$(".waterfall-marker-bar[type=Reflow]"), "No 'Reflow' marker (5)");
+ ok(!$(".waterfall-marker-bar[type=Paint]"), "No 'Paint' marker (5)");
+ ok(!$(".waterfall-marker-bar[type=CustomMarker]"), "No 'Unknown' marker (5)");
+
+ for (let item of [menuItem1, menuItem2, menuItem3]) {
+ EventUtils.synthesizeMouseAtCenter(item, {type: "mouseup"}, panel.panelWin);
+ await waitForOverviewAndCommand(overview, item);
+ }
+
+ ok($(".waterfall-marker-bar[type=Styles]"), "Found at least one 'Styles' marker (6)");
+ ok($(".waterfall-marker-bar[type=Reflow]"), "Found at least one 'Reflow' marker (6)");
+ ok($(".waterfall-marker-bar[type=Paint]"), "Found at least one 'Paint' marker (6)");
+ ok(!$(".waterfall-marker-bar[type=CustomMarker]"), "No 'Unknown' marker (6)");
+
+ is(overview.fixedHeight, originalHeight, "Overview restored");
+
+ await teardown(panel);
+ finish();
+}
+
+function waitForOverviewAndCommand(overview, item) {
+ let overviewRendered = overview.once("refresh");
+ let menuitemCommandDispatched = once(item, "command");
+ return Promise.all([overviewRendered, menuitemCommandDispatched]);
+}
+/* eslint-enable */
diff --git a/devtools/client/performance/test/browser_timeline-filters-02.js b/devtools/client/performance/test/browser_timeline-filters-02.js
new file mode 100644
index 0000000000..6d94853dc6
--- /dev/null
+++ b/devtools/client/performance/test/browser_timeline-filters-02.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/* eslint-disable */
+/**
+ * Tests markers filtering mechanism.
+ */
+
+const URL = EXAMPLE_URL + "doc_innerHTML.html";
+
+async function spawnTest() {
+ let { panel } = await initPerformance(URL);
+ let { $, $$, EVENTS, PerformanceController, OverviewView, WaterfallView } = panel.panelWin;
+
+ await startRecording(panel);
+ ok(true, "Recording has started.");
+
+ await waitUntil(() => {
+ let markers = PerformanceController.getCurrentRecording().getMarkers();
+ return markers.some(m => m.name == "Parse HTML") &&
+ markers.some(m => m.name == "Javascript");
+ });
+
+ let waterfallRendered = WaterfallView.once(EVENTS.UI_WATERFALL_RENDERED);
+ await stopRecording(panel);
+
+ $("#filter-button").click();
+ let filterJS = $("menuitem[marker-type=Javascript]");
+
+ await waterfallRendered;
+
+ ok($(".waterfall-marker-bar[type=Javascript]"), "Found at least one 'Javascript' marker");
+ ok(!$(".waterfall-marker-bar[type='Parse HTML']"), "Found no Parse HTML markers as they are nested still");
+
+ EventUtils.synthesizeMouseAtCenter(filterJS, {type: "mouseup"}, panel.panelWin);
+ await Promise.all([
+ WaterfallView.once(EVENTS.UI_WATERFALL_RENDERED),
+ once(filterJS, "command")
+ ]);
+
+ ok(!$(".waterfall-marker-bar[type=Javascript]"), "Javascript markers are all hidden.");
+ ok($(".waterfall-marker-bar[type='Parse HTML']"),
+ "Found at least one 'Parse HTML' marker still visible after hiding JS markers");
+
+ await teardown(panel);
+ finish();
+}
+/* eslint-enable */
diff --git a/devtools/client/performance/test/browser_timeline-waterfall-background.js b/devtools/client/performance/test/browser_timeline-waterfall-background.js
new file mode 100644
index 0000000000..2b165e7991
--- /dev/null
+++ b/devtools/client/performance/test/browser_timeline-waterfall-background.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the waterfall background is a 1px high canvas stretching across
+ * the container bounds.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+ waitForOverviewRenderedWithMarkers,
+} = require("devtools/client/performance/test/helpers/actions");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { WaterfallView } = panel.panelWin;
+
+ await startRecording(panel);
+ ok(true, "Recording has started.");
+
+ // Ensure overview is rendering and some markers were received.
+ await waitForOverviewRenderedWithMarkers(panel);
+
+ await stopRecording(panel);
+ ok(true, "Recording has ended.");
+
+ // Test the waterfall background.
+
+ ok(
+ WaterfallView.canvas,
+ "A canvas should be created after the recording ended."
+ );
+
+ is(
+ WaterfallView.canvas.width,
+ WaterfallView.waterfallWidth,
+ "The canvas width is correct."
+ );
+ is(WaterfallView.canvas.height, 1, "The canvas height is correct.");
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_timeline-waterfall-generic.js b/devtools/client/performance/test/browser_timeline-waterfall-generic.js
new file mode 100644
index 0000000000..f356845229
--- /dev/null
+++ b/devtools/client/performance/test/browser_timeline-waterfall-generic.js
@@ -0,0 +1,154 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the waterfall is properly built after finishing a recording.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const {
+ initPerformanceInNewTab,
+ teardownToolboxAndRemoveTab,
+} = require("devtools/client/performance/test/helpers/panel-utils");
+const {
+ startRecording,
+ stopRecording,
+ waitForOverviewRenderedWithMarkers,
+} = require("devtools/client/performance/test/helpers/actions");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(async function() {
+ const { panel } = await initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window,
+ });
+
+ const { $, $$, EVENTS, WaterfallView } = panel.panelWin;
+
+ await startRecording(panel);
+ ok(true, "Recording has started.");
+
+ // Ensure overview is rendering and some markers were received.
+ await waitForOverviewRenderedWithMarkers(panel);
+
+ await stopRecording(panel);
+ ok(true, "Recording has ended.");
+
+ // Test the header container.
+
+ ok($(".waterfall-header"), "A header container should have been created.");
+
+ // Test the header sidebar (left).
+
+ ok(
+ $(".waterfall-header > .waterfall-sidebar"),
+ "A header sidebar node should have been created."
+ );
+
+ // Test the header ticks (right).
+
+ ok(
+ $(".waterfall-header-ticks"),
+ "A header ticks node should have been created."
+ );
+ ok(
+ $$(".waterfall-header-ticks > .waterfall-header-tick").length > 0,
+ "Some header tick labels should have been created inside the tick node."
+ );
+
+ // Test the markers sidebar (left).
+
+ ok(
+ $$(".waterfall-tree-item > .waterfall-sidebar").length,
+ "Some marker sidebar nodes should have been created."
+ );
+ ok(
+ $$(".waterfall-tree-item > .waterfall-sidebar > .waterfall-marker-bullet")
+ .length,
+ "Some marker color bullets should have been created inside the sidebar."
+ );
+ ok(
+ $$(".waterfall-tree-item > .waterfall-sidebar > .waterfall-marker-name")
+ .length,
+ "Some marker name labels should have been created inside the sidebar."
+ );
+
+ // Test the markers waterfall (right).
+
+ ok(
+ $$(".waterfall-tree-item > .waterfall-marker").length,
+ "Some marker waterfall nodes should have been created."
+ );
+ ok(
+ $$(".waterfall-tree-item > .waterfall-marker .waterfall-marker-bar").length,
+ "Some marker color bars should have been created inside the waterfall."
+ );
+
+ // Test the sidebar.
+
+ const detailsView = WaterfallView.details;
+ // Make sure the bounds are up to date.
+ WaterfallView._recalculateBounds();
+
+ const parentWidthBefore = $("#waterfall-view").getBoundingClientRect().width;
+ const sidebarWidthBefore = $(".waterfall-sidebar").getBoundingClientRect()
+ .width;
+ const detailsWidthBefore = $("#waterfall-details").getBoundingClientRect()
+ .width;
+
+ ok(
+ detailsView.hidden,
+ "The details view in the waterfall view is hidden by default."
+ );
+ is(detailsWidthBefore, 0, "The details view width should be 0 when hidden.");
+ is(
+ WaterfallView.waterfallWidth,
+ parentWidthBefore -
+ sidebarWidthBefore -
+ WaterfallView.WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS,
+ "The waterfall width is correct (1)."
+ );
+
+ const waterfallRerendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ $$(".waterfall-tree-item")[0].click();
+ await waterfallRerendered;
+
+ const parentWidthAfter = $("#waterfall-view").getBoundingClientRect().width;
+ const sidebarWidthAfter = $(".waterfall-sidebar").getBoundingClientRect()
+ .width;
+ const detailsWidthAfter = $("#waterfall-details").getBoundingClientRect()
+ .width;
+
+ ok(
+ !detailsView.hidden,
+ "The details view in the waterfall view is now visible."
+ );
+ is(
+ parentWidthBefore,
+ parentWidthAfter,
+ "The parent view's width should not have changed."
+ );
+ is(
+ sidebarWidthBefore,
+ sidebarWidthAfter,
+ "The sidebar view's width should not have changed."
+ );
+ isnot(
+ detailsWidthAfter,
+ 0,
+ "The details view width should not be 0 when visible."
+ );
+ is(
+ WaterfallView.waterfallWidth,
+ parentWidthAfter -
+ sidebarWidthAfter -
+ detailsWidthAfter -
+ WaterfallView.WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS,
+ "The waterfall width is correct (2)."
+ );
+
+ await teardownToolboxAndRemoveTab(panel);
+});
diff --git a/devtools/client/performance/test/browser_timeline-waterfall-rerender.js b/devtools/client/performance/test/browser_timeline-waterfall-rerender.js
new file mode 100644
index 0000000000..bab37542cf
--- /dev/null
+++ b/devtools/client/performance/test/browser_timeline-waterfall-rerender.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable */
+/**
+ * Tests if the waterfall remembers the selection when rerendering.
+ */
+
+async function spawnTest() {
+ let { target, panel } = await initPerformance(SIMPLE_URL);
+ let { $, $$, EVENTS, PerformanceController, OverviewView, WaterfallView } = panel.panelWin;
+
+ const MIN_MARKERS_COUNT = 50;
+ const MAX_MARKERS_SELECT = 20;
+
+ await startRecording(panel);
+ ok(true, "Recording has started.");
+
+ let updated = 0;
+ OverviewView.on(EVENTS.UI_OVERVIEW_RENDERED, () => updated++);
+
+ ok((await waitUntil(() => updated > 0)),
+ "The overview graphs were updated a bunch of times.");
+ ok((await waitUntil(() => PerformanceController.getCurrentRecording().getMarkers().length > MIN_MARKERS_COUNT)),
+ "There are some markers available.");
+
+ await stopRecording(panel);
+ ok(true, "Recording has ended.");
+
+ let currentMarkers = PerformanceController.getCurrentRecording().getMarkers();
+ info("Gathered markers: " + JSON.stringify(currentMarkers, null, 2));
+
+ let initialBarsCount = $$(".waterfall-marker-bar").length;
+ info("Initial bars count: " + initialBarsCount);
+
+ // Select a portion of the overview.
+ let rerendered = WaterfallView.once(EVENTS.UI_WATERFALL_RENDERED);
+ OverviewView.setTimeInterval({ startTime: 0, endTime: currentMarkers[MAX_MARKERS_SELECT].end });
+ await rerendered;
+
+ ok(!$(".waterfall-tree-item:focus"),
+ "There is no item focused in the waterfall yet.");
+ ok($("#waterfall-details").hidden,
+ "The waterfall sidebar is initially hidden.");
+
+ // Focus the second item in the tree.
+ WaterfallView._markersRoot.getChild(1).focus();
+
+ let beforeResizeBarsCount = $$(".waterfall-marker-bar").length;
+ info("Before resize bars count: " + beforeResizeBarsCount);
+ ok(beforeResizeBarsCount < initialBarsCount,
+ "A subset of the total markers was selected.");
+
+ is(Array.prototype.indexOf.call($$(".waterfall-tree-item"), $(".waterfall-tree-item:focus")), 2,
+ "The correct item was focused in the tree.");
+ ok(!$("#waterfall-details").hidden,
+ "The waterfall sidebar is now visible.");
+
+ // Simulate a resize on the marker details.
+ rerendered = WaterfallView.once(EVENTS.UI_WATERFALL_RENDERED);
+ await EventUtils.sendMouseEvent({ type: "mouseup" }, WaterfallView.detailsSplitter);
+ await rerendered;
+
+ let afterResizeBarsCount = $$(".waterfall-marker-bar").length;
+ info("After resize bars count: " + afterResizeBarsCount);
+ is(afterResizeBarsCount, beforeResizeBarsCount,
+ "The same subset of the total markers remained visible.");
+
+ is(Array.prototype.indexOf.call($$(".waterfall-tree-item"), $(".waterfall-tree-item:focus")), 2,
+ "The correct item is still focused in the tree.");
+ ok(!$("#waterfall-details").hidden,
+ "The waterfall sidebar is still visible.");
+
+ await teardown(panel);
+ finish();
+}
+/* eslint-enable */
diff --git a/devtools/client/performance/test/browser_timeline-waterfall-sidebar.js b/devtools/client/performance/test/browser_timeline-waterfall-sidebar.js
new file mode 100644
index 0000000000..e82c463742
--- /dev/null
+++ b/devtools/client/performance/test/browser_timeline-waterfall-sidebar.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable */
+/**
+ * Tests if the sidebar is properly updated when a marker is selected.
+ */
+
+async function spawnTest() {
+ let { target, panel } = await initPerformance(SIMPLE_URL);
+ let { $, $$, PerformanceController, WaterfallView } = panel.panelWin;
+ let { L10N } = require("devtools/client/performance/modules/global");
+ let { MarkerBlueprintUtils } = require("devtools/client/performance/modules/marker-blueprint-utils");
+
+ // Hijack the markers massaging part of creating the waterfall view,
+ // to prevent collapsing markers and allowing this test to verify
+ // everything individually. A better solution would be to just expand
+ // all markers first and then skip the meta nodes, but I'm lazy.
+ WaterfallView._prepareWaterfallTree = markers => {
+ return { submarkers: markers };
+ };
+
+ await startRecording(panel);
+ ok(true, "Recording has started.");
+
+ await waitUntil(() => {
+ // Wait until we get 3 different markers.
+ let markers = PerformanceController.getCurrentRecording().getMarkers();
+ return markers.some(m => m.name == "Styles") &&
+ markers.some(m => m.name == "Reflow") &&
+ markers.some(m => m.name == "Paint");
+ });
+
+ await stopRecording(panel);
+ ok(true, "Recording has ended.");
+
+ info("No need to select everything in the timeline.");
+ info("All the markers should be displayed by default.");
+
+ let bars = $$(".waterfall-marker-bar");
+ let markers = PerformanceController.getCurrentRecording().getMarkers();
+
+ info(`Got ${bars.length} bars and ${markers.length} markers.`);
+ info("Markers types from datasrc: " + Array.from(markers, e => e.name));
+ info("Markers names from sidebar: " + Array.from(bars, e => e.parentNode.parentNode.querySelector(".waterfall-marker-name").getAttribute("value")));
+
+ ok(bars.length > 2, "Got at least 3 markers (1)");
+ ok(markers.length > 2, "Got at least 3 markers (2)");
+
+ let toMs = ms => L10N.getFormatStrWithNumbers("timeline.tick", ms);
+
+ for (let i = 0; i < bars.length; i++) {
+ let bar = bars[i];
+ let mkr = markers[i];
+ await EventUtils.sendMouseEvent({ type: "mousedown" }, bar);
+
+ let type = $(".marker-details-type").getAttribute("value");
+ let tooltip = $(".marker-details-duration").getAttribute("tooltiptext");
+ let duration = $(".marker-details-duration .marker-details-labelvalue").getAttribute("value");
+
+ info("Current marker data: " + mkr.toSource());
+ info("Current marker output: " + $("#waterfall-details").innerHTML);
+
+ is(type, MarkerBlueprintUtils.getMarkerLabel(mkr), "Sidebar title matches markers name.");
+
+ // Values are rounded. We don't use a strict equality.
+ is(toMs(mkr.end - mkr.start), duration, "Sidebar duration is valid.");
+
+ // For some reason, anything that creates "→" here turns it into a "â" for some reason.
+ // So just check that start and end time are in there somewhere.
+ ok(tooltip.includes(toMs(mkr.start)), "Tooltip has start time.");
+ ok(tooltip.includes(toMs(mkr.end)), "Tooltip has end time.");
+ }
+
+ await teardown(panel);
+ finish();
+}
+/* eslint-enable */
diff --git a/devtools/client/performance/test/browser_timeline-waterfall-workers.js b/devtools/client/performance/test/browser_timeline-waterfall-workers.js
new file mode 100644
index 0000000000..595ca378a5
--- /dev/null
+++ b/devtools/client/performance/test/browser_timeline-waterfall-workers.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/* eslint-disable */
+/**
+ * Tests if the sidebar is properly updated with worker markers.
+ */
+
+async function spawnTest() {
+ let { panel } = await initPerformance(WORKER_URL);
+ let { $$, $, PerformanceController } = panel.panelWin;
+
+ await startRecording(panel);
+ ok(true, "Recording has started.");
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.wrappedJSObject.performWork();
+ });
+
+ await waitUntil(() => {
+ // Wait until we get the worker markers.
+ let markers = PerformanceController.getCurrentRecording().getMarkers();
+ if (!markers.some(m => m.name == "Worker") ||
+ !markers.some(m => m.workerOperation == "serializeDataOffMainThread") ||
+ !markers.some(m => m.workerOperation == "serializeDataOnMainThread") ||
+ !markers.some(m => m.workerOperation == "deserializeDataOffMainThread") ||
+ !markers.some(m => m.workerOperation == "deserializeDataOnMainThread")) {
+ return false;
+ }
+
+ testWorkerMarkerData(markers.find(m => m.name == "Worker"));
+ return true;
+ });
+
+ await stopRecording(panel);
+ ok(true, "Recording has ended.");
+
+ for (let node of $$(".waterfall-marker-name[value=Worker")) {
+ testWorkerMarkerUI(node.parentNode.parentNode);
+ }
+
+ await teardown(panel);
+ finish();
+}
+
+function testWorkerMarkerData(marker) {
+ ok(true, "Found a worker marker.");
+
+ ok("start" in marker,
+ "The start time is specified in the worker marker.");
+ ok("end" in marker,
+ "The end time is specified in the worker marker.");
+
+ ok("workerOperation" in marker,
+ "The worker operation is specified in the worker marker.");
+
+ ok("processType" in marker,
+ "The process type is specified in the worker marker.");
+ ok("isOffMainThread" in marker,
+ "The thread origin is specified in the worker marker.");
+}
+
+function testWorkerMarkerUI(node) {
+ is(node.className, "waterfall-tree-item",
+ "The marker node has the correct class name.");
+ ok(node.hasAttribute("otmt"),
+ "The marker node specifies if it is off the main thread or not.");
+}
+
+/* eslint-enable */
diff --git a/devtools/client/performance/test/doc_allocs.html b/devtools/client/performance/test/doc_allocs.html
new file mode 100644
index 0000000000..83f927e43d
--- /dev/null
+++ b/devtools/client/performance/test/doc_allocs.html
@@ -0,0 +1,26 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Performance test page</title>
+ </head>
+
+ <body>
+ <script type="text/javascript">
+ "use strict";
+ const allocs = [];
+ function test() {
+ for (let i = 0; i < 10; i++) {
+ allocs.push({});
+ }
+ }
+
+ // Prevent this script from being garbage collected.
+ window.setInterval(test, 1);
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/performance/test/doc_innerHTML.html b/devtools/client/performance/test/doc_innerHTML.html
new file mode 100644
index 0000000000..e58b32f51e
--- /dev/null
+++ b/devtools/client/performance/test/doc_innerHTML.html
@@ -0,0 +1,21 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Performance tool + innerHTML test page</title>
+ </head>
+
+ <body>
+ <script type="text/javascript">
+ "use strict";
+ window.test = function() {
+ document.body.innerHTML = "<h1>LOL</h1>";
+ };
+ setInterval(window.test, 100);
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/performance/test/doc_markers.html b/devtools/client/performance/test/doc_markers.html
new file mode 100644
index 0000000000..e7cbf84b98
--- /dev/null
+++ b/devtools/client/performance/test/doc_markers.html
@@ -0,0 +1,38 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Performance tool marker generation</title>
+ </head>
+
+ <body>
+ <script type="text/javascript">
+ "use strict";
+ function test() {
+ let i = 10;
+ // generate sync styles and reflows
+ while (--i) {
+ /* eslint-disable no-unused-vars */
+ const h = document.body.clientHeight;
+ /* eslint-enable no-unused-vars */
+ document.body.style.height = (200 + i) + "px";
+ // paint
+ document.body.style.borderTop = i + "px solid red";
+ }
+ console.time("!!!");
+ test2();
+ }
+ function test2() {
+ console.timeStamp("go");
+ console.timeEnd("!!!");
+ }
+
+ // Prevent this script from being garbage collected.
+ window.setInterval(test, 1);
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/performance/test/doc_simple-test.html b/devtools/client/performance/test/doc_simple-test.html
new file mode 100644
index 0000000000..5cda6eaa61
--- /dev/null
+++ b/devtools/client/performance/test/doc_simple-test.html
@@ -0,0 +1,27 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Performance test page</title>
+ </head>
+
+ <body>
+ <script type="text/javascript">
+ "use strict";
+ let x = 1;
+ function test() {
+ document.body.style.borderTop = x + "px solid red";
+ x = 1 ^ x;
+ // flush pending reflows
+ document.body.innerHeight;
+ }
+
+ // Prevent this script from being garbage collected.
+ window.setInterval(test, 1);
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/performance/test/doc_worker.html b/devtools/client/performance/test/doc_worker.html
new file mode 100644
index 0000000000..0637b17690
--- /dev/null
+++ b/devtools/client/performance/test/doc_worker.html
@@ -0,0 +1,29 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Performance test page</title>
+ </head>
+
+ <body>
+ <script type="text/javascript">
+ "use strict";
+
+ /* exported performWork */
+ function performWork() {
+ const worker = new Worker("js_simpleWorker.js");
+
+ worker.addEventListener("message", function(e) {
+ console.log(e.data);
+ console.timeStamp("Done");
+ });
+
+ worker.postMessage("Hello World");
+ }
+ </script>
+ </body>
+
+</html>
diff --git a/devtools/client/performance/test/head.js b/devtools/client/performance/test/head.js
new file mode 100644
index 0000000000..f97eb393b9
--- /dev/null
+++ b/devtools/client/performance/test/head.js
@@ -0,0 +1,117 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from ../../shared/test/telemetry-test-helpers.js */
+
+"use strict";
+
+const { require, loader } = ChromeUtils.import(
+ "resource://devtools/shared/Loader.jsm"
+);
+
+try {
+ Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/telemetry-test-helpers.js",
+ this
+ );
+} catch (e) {
+ ok(
+ false,
+ "MISSING DEPENDENCY ON telemetry-test-helpers.js\n" +
+ "Please add the following line in browser.ini:\n" +
+ " !/devtools/client/shared/test/telemetry-test-helpers.js\n"
+ );
+ throw e;
+}
+
+/* exported loader, either, click, dblclick, mousedown, rightMousedown, key */
+// All tests are asynchronous.
+waitForExplicitFinish();
+
+// Performance tests are much heavier because of their reliance on the
+// profiler module, memory measurements, frequent canvas operations etc. Many of
+// of them take longer than 30 seconds to finish on try server VMs, even though
+// they superficially do very little.
+requestLongerTimeout(3);
+
+// Same as `is`, but takes in two possible values.
+const either = (value, a, b, message) => {
+ if (value == a) {
+ is(value, a, message);
+ } else if (value == b) {
+ is(value, b, message);
+ } else {
+ ok(false, message);
+ }
+};
+
+// Shortcut for simulating a click on an element.
+const click = async (node, win = window) => {
+ await EventUtils.sendMouseEvent({ type: "click" }, node, win);
+};
+
+// Shortcut for simulating a double click on an element.
+const dblclick = async (node, win = window) => {
+ await EventUtils.sendMouseEvent({ type: "dblclick" }, node, win);
+};
+
+// Shortcut for simulating a mousedown on an element.
+const mousedown = async (node, win = window) => {
+ await EventUtils.sendMouseEvent({ type: "mousedown" }, node, win);
+};
+
+// Shortcut for simulating a mousedown using the right mouse button on an element.
+const rightMousedown = async (node, win = window) => {
+ await EventUtils.sendMouseEvent({ type: "mousedown", button: 2 }, node, win);
+};
+
+// Shortcut for firing a key event, like "VK_UP", "VK_DOWN", etc.
+const key = (id, win = window) => {
+ EventUtils.synthesizeKey(id, {}, win);
+};
+
+// Don't pollute global scope.
+(() => {
+ const PrefUtils = require("devtools/client/performance/test/helpers/prefs");
+
+ // Make sure all the prefs are reverted to their defaults once tests finish.
+ const stopObservingPrefs = PrefUtils.whenUnknownPrefChanged(
+ "devtools.performance",
+ pref => {
+ ok(
+ false,
+ `Unknown pref changed: ${pref}. Please add it to test/helpers/prefs.js ` +
+ "to make sure it's reverted to its default value when the tests finishes, " +
+ "and avoid interfering with future tests.\n"
+ );
+ }
+ );
+
+ // By default, enable memory flame graphs for tests for now.
+ // TODO: remove when we have flame charts via bug 1148663.
+ Services.prefs.setBoolPref(PrefUtils.UI_ENABLE_MEMORY_FLAME_CHART, true);
+
+ // By default, reduce the default buffer size to reduce the overhead when
+ // transfering the profile data. Hopefully this should help to reduce our
+ // intermittents for the performance tests.
+ Services.prefs.setIntPref(PrefUtils.PROFILER_BUFFER_SIZE_PREF, 100000);
+
+ registerCleanupFunction(() => {
+ info("finish() was called, cleaning up...");
+
+ PrefUtils.rollbackPrefsToDefault();
+ stopObservingPrefs();
+
+ // Manually stop the profiler module at the end of all tests, to hopefully
+ // avoid at least some leaks on OSX. Theoretically the module should never
+ // be active at this point. We shouldn't have to do this, but rather
+ // find and fix the leak in the module itself. Bug 1257439.
+ Services.profiler.StopProfiler();
+
+ // Forces GC, CC and shrinking GC to get rid of disconnected docshells
+ // and windows.
+ Cu.forceGC();
+ Cu.forceCC();
+ Cu.forceShrinkingGC();
+ });
+})();
diff --git a/devtools/client/performance/test/helpers/actions.js b/devtools/client/performance/test/helpers/actions.js
new file mode 100644
index 0000000000..4b169f6844
--- /dev/null
+++ b/devtools/client/performance/test/helpers/actions.js
@@ -0,0 +1,168 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const { Constants } = require("devtools/client/performance/modules/constants");
+const {
+ once,
+ times,
+} = require("devtools/client/performance/test/helpers/event-utils");
+const {
+ waitUntil,
+} = require("devtools/client/performance/test/helpers/wait-utils");
+
+/**
+ * Starts a manual recording in the given performance tool panel and
+ * waits for it to finish starting.
+ */
+exports.startRecording = function(panel, options = {}) {
+ const controller = panel.panelWin.PerformanceController;
+
+ return Promise.all([
+ controller.startRecording(),
+ exports.waitForRecordingStartedEvents(panel, options),
+ ]);
+};
+
+/**
+ * Stops the latest recording in the given performance tool panel and
+ * waits for it to finish stopping.
+ */
+exports.stopRecording = function(panel, options = {}) {
+ const controller = panel.panelWin.PerformanceController;
+
+ return Promise.all([
+ controller.stopRecording(),
+ exports.waitForRecordingStoppedEvents(panel, options),
+ ]);
+};
+
+/**
+ * Waits for all the necessary events to be emitted after a recording starts.
+ */
+exports.waitForRecordingStartedEvents = function(panel, options = {}) {
+ options.expectedViewState =
+ options.expectedViewState || /^(console-)?recording$/;
+
+ const EVENTS = panel.panelWin.EVENTS;
+ const controller = panel.panelWin.PerformanceController;
+ const view = panel.panelWin.PerformanceView;
+ const overview = panel.panelWin.OverviewView;
+
+ return Promise.all([
+ options.skipWaitingForBackendReady
+ ? null
+ : once(controller, EVENTS.BACKEND_READY_AFTER_RECORDING_START),
+ options.skipWaitingForRecordingStarted
+ ? null
+ : once(controller, EVENTS.RECORDING_STATE_CHANGE, {
+ expectedArgs: ["recording-started"],
+ }),
+ options.skipWaitingForViewState
+ ? null
+ : once(view, EVENTS.UI_STATE_CHANGED, {
+ expectedArgs: [options.expectedViewState],
+ }),
+ options.skipWaitingForOverview
+ ? null
+ : once(overview, EVENTS.UI_OVERVIEW_RENDERED, {
+ expectedArgs: [Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL],
+ }),
+ ]);
+};
+
+/**
+ * Waits for all the necessary events to be emitted after a recording finishes.
+ */
+exports.waitForRecordingStoppedEvents = function(panel, options = {}) {
+ options.expectedViewClass = options.expectedViewClass || "WaterfallView";
+ options.expectedViewEvent =
+ options.expectedViewEvent || "UI_WATERFALL_RENDERED";
+ options.expectedViewState = options.expectedViewState || "recorded";
+
+ const EVENTS = panel.panelWin.EVENTS;
+ const controller = panel.panelWin.PerformanceController;
+ const view = panel.panelWin.PerformanceView;
+ const overview = panel.panelWin.OverviewView;
+ const subview = panel.panelWin[options.expectedViewClass];
+
+ return Promise.all([
+ options.skipWaitingForBackendReady
+ ? null
+ : once(controller, EVENTS.BACKEND_READY_AFTER_RECORDING_STOP),
+ options.skipWaitingForRecordingStop
+ ? null
+ : once(controller, EVENTS.RECORDING_STATE_CHANGE, {
+ expectedArgs: ["recording-stopping"],
+ }),
+ options.skipWaitingForRecordingStop
+ ? null
+ : once(controller, EVENTS.RECORDING_STATE_CHANGE, {
+ expectedArgs: ["recording-stopped"],
+ }),
+ options.skipWaitingForViewState
+ ? null
+ : once(view, EVENTS.UI_STATE_CHANGED, {
+ expectedArgs: [options.expectedViewState],
+ }),
+ options.skipWaitingForOverview
+ ? null
+ : once(overview, EVENTS.UI_OVERVIEW_RENDERED, {
+ expectedArgs: [Constants.FRAMERATE_GRAPH_HIGH_RES_INTERVAL],
+ }),
+ options.skipWaitingForSubview
+ ? null
+ : once(subview, EVENTS[options.expectedViewEvent]),
+ ]);
+};
+
+/**
+ * Waits for rendering to happen once on all the performance tool's widgets.
+ */
+exports.waitForAllWidgetsRendered = panel => {
+ const { panelWin } = panel;
+ const { EVENTS } = panelWin;
+
+ return Promise.all([
+ once(panelWin.OverviewView, EVENTS.UI_MARKERS_GRAPH_RENDERED),
+ once(panelWin.OverviewView, EVENTS.UI_MEMORY_GRAPH_RENDERED),
+ once(panelWin.OverviewView, EVENTS.UI_FRAMERATE_GRAPH_RENDERED),
+ once(panelWin.OverviewView, EVENTS.UI_OVERVIEW_RENDERED),
+ once(panelWin.WaterfallView, EVENTS.UI_WATERFALL_RENDERED),
+ once(panelWin.JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED),
+ once(panelWin.JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED),
+ once(panelWin.MemoryCallTreeView, EVENTS.UI_MEMORY_CALL_TREE_RENDERED),
+ once(panelWin.MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED),
+ ]);
+};
+
+/**
+ * Waits for rendering to happen on the performance tool's overview graph,
+ * making sure some markers were also rendered.
+ */
+exports.waitForOverviewRenderedWithMarkers = (
+ panel,
+ minTimes = 3,
+ minMarkers = 1
+) => {
+ const { EVENTS, OverviewView, PerformanceController } = panel.panelWin;
+
+ return Promise.all([
+ times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, minTimes, {
+ expectedArgs: [Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL],
+ }),
+ waitUntil(
+ () =>
+ PerformanceController.getCurrentRecording().getMarkers().length >=
+ minMarkers
+ ),
+ ]);
+};
+
+/**
+ * Reloads the given tab target.
+ */
+exports.reload = target => {
+ target.reload();
+ return once(target, "navigate");
+};
diff --git a/devtools/client/performance/test/helpers/dom-utils.js b/devtools/client/performance/test/helpers/dom-utils.js
new file mode 100644
index 0000000000..acc09499e2
--- /dev/null
+++ b/devtools/client/performance/test/helpers/dom-utils.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const Services = require("Services");
+const {
+ waitForMozAfterPaint,
+} = require("devtools/client/performance/test/helpers/wait-utils");
+
+/**
+ * Checks if a DOM node is considered visible.
+ */
+exports.isVisible = element => {
+ return !element.classList.contains("hidden") && !element.hidden;
+};
+
+/**
+ * Appends the provided element to the provided parent node. If run in e10s
+ * mode, will also wait for MozAfterPaint to make sure the tab is rendered.
+ * Should be reviewed if Bug 1240509 lands.
+ */
+exports.appendAndWaitForPaint = function(parent, element) {
+ const isE10s = Services.appinfo.browserTabsRemoteAutostart;
+ if (isE10s) {
+ const win = parent.ownerDocument.defaultView;
+ const onMozAfterPaint = waitForMozAfterPaint(win);
+ parent.appendChild(element);
+ return onMozAfterPaint;
+ }
+ parent.appendChild(element);
+ return null;
+};
diff --git a/devtools/client/performance/test/helpers/event-utils.js b/devtools/client/performance/test/helpers/event-utils.js
new file mode 100644
index 0000000000..7f9e765f4f
--- /dev/null
+++ b/devtools/client/performance/test/helpers/event-utils.js
@@ -0,0 +1,118 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* globals dump */
+
+const Services = require("Services");
+
+const KNOWN_EE_APIS = [
+ ["on", "off"],
+ ["addEventListener", "removeEventListener"],
+ ["addListener", "removeListener"],
+];
+
+/**
+ * Listens for any event for a single time on a target, no matter what kind of
+ * event emitter it is, returning a promise resolved with the passed arguments
+ * once the event is fired.
+ */
+exports.once = function(target, eventName, options = {}) {
+ return exports.times(target, eventName, 1, options);
+};
+
+/**
+ * Waits for any event to be fired a specified amount of times on a target, no
+ * matter what kind of event emitter.
+ * Possible options: `useCapture`, `spreadArgs`, `expectedArgs`
+ */
+exports.times = function(target, eventName, receiveCount, options = {}) {
+ const msg = `Waiting for event: '${eventName}' on ${target} for ${receiveCount} time(s)`;
+ if ("expectedArgs" in options) {
+ dump(`${msg} with arguments: ${JSON.stringify(options.expectedArgs)}.\n`);
+ } else {
+ dump(`${msg}.\n`);
+ }
+
+ return new Promise((resolve, reject) => {
+ if (typeof eventName != "string") {
+ reject(new Error(`Unexpected event name: ${eventName}.`));
+ }
+
+ const API = KNOWN_EE_APIS.find(([a, r]) => a in target && r in target);
+ if (!API) {
+ reject(new Error("Target is not a supported event listener."));
+ return;
+ }
+
+ const [add, remove] = API;
+
+ target[add](
+ eventName,
+ function onEvent(...args) {
+ if ("expectedArgs" in options) {
+ for (const [index, expectedValue] of options.expectedArgs.entries()) {
+ const isExpectedValueRegExp = expectedValue instanceof RegExp;
+ if (
+ (isExpectedValueRegExp && !expectedValue.exec(args[index])) ||
+ (!isExpectedValueRegExp && expectedValue != args[index])
+ ) {
+ dump(
+ `Ignoring event '${eventName}' with unexpected argument at index ` +
+ `${index}: ${args[index]} - expected ${expectedValue}\n`
+ );
+ return;
+ }
+ }
+ }
+ if (--receiveCount > 0) {
+ dump(
+ `Event: '${eventName}' on ${target} needs to be fired ${receiveCount} ` +
+ `more time(s).\n`
+ );
+ } else if (!receiveCount) {
+ dump(`Event: '${eventName}' on ${target} received.\n`);
+ target[remove](eventName, onEvent, options.useCapture);
+ resolve(options.spreadArgs ? args : args[0]);
+ }
+ },
+ options.useCapture
+ );
+ });
+};
+
+/**
+ * Like `times`, but for observer notifications.
+ * Possible options: `expectedSubject`
+ */
+exports.observeTimes = function(notificationName, receiveCount, options = {}) {
+ dump(
+ `Waiting for notification: '${notificationName}' for ${receiveCount} time(s).\n`
+ );
+
+ return new Promise((resolve, reject) => {
+ if (typeof notificationName != "string") {
+ reject(new Error(`Unexpected notification name: ${notificationName}.`));
+ }
+
+ Services.obs.addObserver(function onObserve(subject, topic, data) {
+ if ("expectedSubject" in options && options.expectedSubject != subject) {
+ dump(
+ `Ignoring notification '${notificationName}' with unexpected subject: ` +
+ `${subject}\n`
+ );
+ return;
+ }
+ if (--receiveCount > 0) {
+ dump(
+ `Notification: '${notificationName}' needs to be fired ${receiveCount} ` +
+ `more time(s).\n`
+ );
+ } else if (!receiveCount) {
+ dump(`Notification: '${notificationName}' received.\n`);
+ Services.obs.removeObserver(onObserve, topic);
+ resolve(data);
+ }
+ }, notificationName);
+ });
+};
diff --git a/devtools/client/performance/test/helpers/input-utils.js b/devtools/client/performance/test/helpers/input-utils.js
new file mode 100644
index 0000000000..52919fec5b
--- /dev/null
+++ b/devtools/client/performance/test/helpers/input-utils.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+exports.HORIZONTAL_AXIS = 1;
+exports.VERTICAL_AXIS = 2;
+
+/**
+ * Simulates a command event on an element.
+ */
+exports.command = node => {
+ const ev = node.ownerDocument.createEvent("XULCommandEvent");
+ ev.initCommandEvent(
+ "command",
+ true,
+ true,
+ node.ownerDocument.defaultView,
+ 0,
+ false,
+ false,
+ false,
+ false,
+ null,
+ 0
+ );
+ node.dispatchEvent(ev);
+};
+
+/**
+ * Simulates a click event on a devtools canvas graph.
+ */
+exports.clickCanvasGraph = (graph, { x, y }) => {
+ x = x || 0;
+ y = y || 0;
+ x /= graph._window.devicePixelRatio;
+ y /= graph._window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+};
+
+/**
+ * Simulates a drag start event on a devtools canvas graph.
+ */
+exports.dragStartCanvasGraph = (graph, { x, y }) => {
+ x = x || 0;
+ y = y || 0;
+ x /= graph._window.devicePixelRatio;
+ y /= graph._window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseDown({ testX: x, testY: y });
+};
+
+/**
+ * Simulates a drag stop event on a devtools canvas graph.
+ */
+exports.dragStopCanvasGraph = (graph, { x, y }) => {
+ x = x || 0;
+ y = y || 0;
+ x /= graph._window.devicePixelRatio;
+ y /= graph._window.devicePixelRatio;
+ graph._onMouseMove({ testX: x, testY: y });
+ graph._onMouseUp({ testX: x, testY: y });
+};
+
+/**
+ * Simulates a scroll event on a devtools canvas graph.
+ */
+exports.scrollCanvasGraph = (graph, { axis, wheel, x, y }) => {
+ x = x || 1;
+ y = y || 1;
+ x /= graph._window.devicePixelRatio;
+ y /= graph._window.devicePixelRatio;
+ graph._onMouseMove({
+ testX: x,
+ testY: y,
+ });
+ graph._onMouseWheel({
+ testX: x,
+ testY: y,
+ axis: axis,
+ detail: wheel,
+ HORIZONTAL_AXIS: exports.HORIZONTAL_AXIS,
+ VERTICAL_AXIS: exports.VERTICAL_AXIS,
+ });
+};
diff --git a/devtools/client/performance/test/helpers/moz.build b/devtools/client/performance/test/helpers/moz.build
new file mode 100644
index 0000000000..d1b1b65472
--- /dev/null
+++ b/devtools/client/performance/test/helpers/moz.build
@@ -0,0 +1,20 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# 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(
+ "actions.js",
+ "dom-utils.js",
+ "event-utils.js",
+ "input-utils.js",
+ "panel-utils.js",
+ "prefs.js",
+ "profiler-mm-utils.js",
+ "recording-utils.js",
+ "synth-utils.js",
+ "tab-utils.js",
+ "urls.js",
+ "wait-utils.js",
+)
diff --git a/devtools/client/performance/test/helpers/panel-utils.js b/devtools/client/performance/test/helpers/panel-utils.js
new file mode 100644
index 0000000000..d078242349
--- /dev/null
+++ b/devtools/client/performance/test/helpers/panel-utils.js
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* globals dump */
+
+const { gDevTools } = require("devtools/client/framework/devtools");
+const { TargetFactory } = require("devtools/client/framework/target");
+const {
+ addTab,
+ removeTab,
+} = require("devtools/client/performance/test/helpers/tab-utils");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+/**
+ * Initializes a toolbox panel in a new tab.
+ */
+exports.initPanelInNewTab = async function({ tool, url, win }, options = {}) {
+ const tab = await addTab({ url, win }, options);
+ return exports.initPanelInTab({ tool, tab });
+};
+
+/**
+ * Initializes a toolbox panel in the specified tab.
+ */
+exports.initPanelInTab = async function({ tool, tab }) {
+ dump(`Initializing a ${tool} panel.\n`);
+
+ const target = await TargetFactory.forTab(tab);
+
+ // Open a toolbox and wait for the connection to the performance actors
+ // to be opened. This is necessary because of the WebConsole's
+ // `profile` and `profileEnd` methods.
+ const toolbox = await gDevTools.showToolbox(target, tool);
+ // ensure that the performance front is ready
+ await target.getFront("performance");
+
+ const panel = toolbox.getCurrentPanel();
+ return { target, toolbox, panel };
+};
+
+/**
+ * Initializes a performance panel in a new tab.
+ */
+exports.initPerformanceInNewTab = async function({ url, win }, options = {}) {
+ const tab = await addTab({ url, win }, options);
+ return exports.initPerformanceInTab({ tab });
+};
+
+/**
+ * Initializes a performance panel in the specified tab.
+ */
+exports.initPerformanceInTab = async function({ tab }) {
+ return exports.initPanelInTab({
+ tool: "performance",
+ tab: tab,
+ });
+};
+
+/**
+ * Initializes a webconsole panel in a new tab.
+ * Returns a console property that allows calls to `profile` and `profileEnd`.
+ */
+exports.initConsoleInNewTab = async function({ url, win }, options = {}) {
+ const tab = await addTab({ url, win }, options);
+ return exports.initConsoleInTab({ tab });
+};
+
+/**
+ * Initializes a webconsole panel in the specified tab.
+ * Returns a console property that allows calls to `profile` and `profileEnd`.
+ */
+exports.initConsoleInTab = async function({ tab }) {
+ const { target, toolbox, panel } = await exports.initPanelInTab({
+ tool: "webconsole",
+ tab: tab,
+ });
+
+ const consoleMethod = async function(method, label, event) {
+ const performanceFront = await toolbox.target.getFront("performance");
+ const recordingEventReceived = once(performanceFront, event);
+ const expression = label
+ ? `console.${method}("${label}")`
+ : `console.${method}()`;
+ await panel.hud.ui.wrapper.dispatchEvaluateExpression(expression);
+ await recordingEventReceived;
+ };
+
+ const profile = async function(label) {
+ return consoleMethod("profile", label, "recording-started");
+ };
+
+ const profileEnd = async function(label) {
+ return consoleMethod("profileEnd", label, "recording-stopped");
+ };
+
+ return { target, toolbox, panel, console: { profile, profileEnd } };
+};
+
+/**
+ * Tears down a toolbox panel and removes an associated tab.
+ */
+exports.teardownToolboxAndRemoveTab = async function(panel) {
+ dump("Destroying panel.\n");
+
+ const tab = panel.target.localTab;
+ await panel.toolbox.destroy();
+ await removeTab(tab);
+};
diff --git a/devtools/client/performance/test/helpers/prefs.js b/devtools/client/performance/test/helpers/prefs.js
new file mode 100644
index 0000000000..1b9ec63545
--- /dev/null
+++ b/devtools/client/performance/test/helpers/prefs.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const Services = require("Services");
+const { Preferences } = require("resource://gre/modules/Preferences.jsm");
+
+// Prefs to revert to default once tests finish. Keep these in sync with
+// all the preferences defined in devtools/client/preferences/devtools-client.js.
+exports.MEMORY_SAMPLE_PROB_PREF =
+ "devtools.performance.memory.sample-probability";
+exports.MEMORY_MAX_LOG_LEN_PREF = "devtools.performance.memory.max-log-length";
+exports.PROFILER_BUFFER_SIZE_PREF = "devtools.performance.profiler.buffer-size";
+exports.PROFILER_SAMPLE_RATE_PREF =
+ "devtools.performance.profiler.sample-frequency-hz";
+
+exports.UI_EXPERIMENTAL_PREF = "devtools.performance.ui.experimental";
+exports.UI_INVERT_CALL_TREE_PREF = "devtools.performance.ui.invert-call-tree";
+exports.UI_INVERT_FLAME_PREF = "devtools.performance.ui.invert-flame-graph";
+exports.UI_FLATTEN_RECURSION_PREF =
+ "devtools.performance.ui.flatten-tree-recursion";
+exports.UI_SHOW_PLATFORM_DATA_PREF =
+ "devtools.performance.ui.show-platform-data";
+exports.UI_SHOW_IDLE_BLOCKS_PREF = "devtools.performance.ui.show-idle-blocks";
+exports.UI_ENABLE_FRAMERATE_PREF = "devtools.performance.ui.enable-framerate";
+exports.UI_ENABLE_MEMORY_PREF = "devtools.performance.ui.enable-memory";
+exports.UI_ENABLE_ALLOCATIONS_PREF =
+ "devtools.performance.ui.enable-allocations";
+exports.UI_ENABLE_MEMORY_FLAME_CHART =
+ "devtools.performance.ui.enable-memory-flame";
+
+exports.DEFAULT_PREF_VALUES = [
+ "devtools.debugger.log",
+ "devtools.performance.enabled",
+ "devtools.performance.timeline.hidden-markers",
+ exports.MEMORY_SAMPLE_PROB_PREF,
+ exports.MEMORY_MAX_LOG_LEN_PREF,
+ exports.PROFILER_BUFFER_SIZE_PREF,
+ exports.PROFILER_SAMPLE_RATE_PREF,
+ exports.UI_EXPERIMENTAL_PREF,
+ exports.UI_INVERT_CALL_TREE_PREF,
+ exports.UI_INVERT_FLAME_PREF,
+ exports.UI_FLATTEN_RECURSION_PREF,
+ exports.UI_SHOW_PLATFORM_DATA_PREF,
+ exports.UI_SHOW_IDLE_BLOCKS_PREF,
+ exports.UI_ENABLE_FRAMERATE_PREF,
+ exports.UI_ENABLE_MEMORY_PREF,
+ exports.UI_ENABLE_ALLOCATIONS_PREF,
+ exports.UI_ENABLE_MEMORY_FLAME_CHART,
+ "devtools.performance.ui.show-jit-optimizations",
+ "devtools.performance.ui.show-triggers-for-gc-types",
+].reduce((prefValues, prefName) => {
+ prefValues[prefName] = Preferences.get(prefName);
+ return prefValues;
+}, {});
+
+/**
+ * Invokes callback when a pref which is not in the `DEFAULT_PREF_VALUES` store
+ * is changed. Returns a cleanup function.
+ */
+exports.whenUnknownPrefChanged = function(branch, callback) {
+ function onObserve(subject, topic, data) {
+ if (!(data in exports.DEFAULT_PREF_VALUES)) {
+ callback(data);
+ }
+ }
+ Services.prefs.addObserver(branch, onObserve);
+ return () => Services.prefs.removeObserver(branch, onObserve);
+};
+
+/**
+ * Reverts all known preferences to their default values.
+ */
+exports.rollbackPrefsToDefault = function() {
+ for (const prefName of Object.keys(exports.DEFAULT_PREF_VALUES)) {
+ Preferences.set(prefName, exports.DEFAULT_PREF_VALUES[prefName]);
+ }
+};
diff --git a/devtools/client/performance/test/helpers/profiler-mm-utils.js b/devtools/client/performance/test/helpers/profiler-mm-utils.js
new file mode 100644
index 0000000000..c2c2100903
--- /dev/null
+++ b/devtools/client/performance/test/helpers/profiler-mm-utils.js
@@ -0,0 +1,82 @@
+/* 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 following functions are used in testing to control and inspect
+ * the nsIProfiler in child process content. These should be called from
+ * the parent process.
+ */
+
+let gSelectedBrowser = null;
+
+/**
+ * Loads the relevant frame scripts into the provided browser's message manager.
+ */
+exports.pmmInitWithBrowser = gBrowser => {
+ gSelectedBrowser = gBrowser.selectedBrowser;
+};
+
+/**
+ * Checks if the nsProfiler module is active.
+ */
+exports.pmmIsProfilerActive = () => {
+ return exports.pmmSendProfilerCommand("IsActive");
+};
+
+/**
+ * Starts the nsProfiler module.
+ */
+exports.pmmStartProfiler = async function({ entries, interval, features }) {
+ const isActive = (await exports.pmmSendProfilerCommand("IsActive")).isActive;
+ if (!isActive) {
+ return exports.pmmSendProfilerCommand("StartProfiler", [
+ entries,
+ interval,
+ features,
+ features.length,
+ ]);
+ }
+ return null;
+};
+/**
+ * Stops the nsProfiler module.
+ */
+exports.pmmStopProfiler = async function() {
+ const isActive = (await exports.pmmSendProfilerCommand("IsActive")).isActive;
+ if (isActive) {
+ return exports.pmmSendProfilerCommand("StopProfiler");
+ }
+ return null;
+};
+
+/**
+ * Calls a method on the nsProfiler module.
+ */
+exports.pmmSendProfilerCommand = (method, args = []) => {
+ // This script is loaded via the CommonJS module so the global
+ // SpecialPowers isn't available, so get it from the browser's window.
+ return gSelectedBrowser.ownerGlobal.SpecialPowers.spawn(
+ gSelectedBrowser,
+ [method, args],
+ (methodChild, argsChild) => {
+ return Services.profiler[methodChild](...argsChild);
+ }
+ );
+};
+
+/**
+ * Evaluates a console method in content.
+ */
+exports.pmmConsoleMethod = function(method, ...args) {
+ // This script is loaded via the CommonJS module so the global
+ // SpecialPowers isn't available, so get it from the browser's window.
+ return gSelectedBrowser.ownerGlobal.SpecialPowers.spawn(
+ gSelectedBrowser,
+ [method, args],
+ (methodChild, argsChild) => {
+ content.console[methodChild].apply(content.console, argsChild);
+ }
+ );
+};
diff --git a/devtools/client/performance/test/helpers/recording-utils.js b/devtools/client/performance/test/helpers/recording-utils.js
new file mode 100644
index 0000000000..621193b798
--- /dev/null
+++ b/devtools/client/performance/test/helpers/recording-utils.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";
+
+/**
+ * These utilities provide a functional interface for accessing the particulars
+ * about the recording's details.
+ */
+
+/**
+ * Access the selected view from the panel's recording list.
+ *
+ * @param {object} panel - The current panel.
+ * @return {object} The recording model.
+ */
+exports.getSelectedRecording = function(panel) {
+ const view = panel.panelWin.RecordingsView;
+ return view.selected;
+};
+
+/**
+ * Set the selected index of the recording via the panel.
+ *
+ * @param {object} panel - The current panel.
+ * @return {number} index
+ */
+exports.setSelectedRecording = function(panel, index) {
+ const view = panel.panelWin.RecordingsView;
+ view.setSelectedByIndex(index);
+ return index;
+};
+
+/**
+ * Access the selected view from the panel's recording list.
+ *
+ * @param {object} panel - The current panel.
+ * @return {number} index
+ */
+exports.getSelectedRecordingIndex = function(panel) {
+ const view = panel.panelWin.RecordingsView;
+ return view.getSelectedIndex();
+};
+
+exports.getDurationLabelText = function(panel, elementIndex) {
+ const { $$ } = panel.panelWin;
+ const elements = $$(".recording-list-item-duration", panel.panelWin.document);
+ return elements[elementIndex].innerHTML;
+};
+
+exports.getRecordingsCount = function(panel) {
+ const { $$ } = panel.panelWin;
+ return $$(".recording-list-item", panel.panelWin.document).length;
+};
diff --git a/devtools/client/performance/test/helpers/synth-utils.js b/devtools/client/performance/test/helpers/synth-utils.js
new file mode 100644
index 0000000000..6e8438d79d
--- /dev/null
+++ b/devtools/client/performance/test/helpers/synth-utils.js
@@ -0,0 +1,152 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Generates a generalized profile with some samples.
+ */
+exports.synthesizeProfile = () => {
+ const {
+ CATEGORY_INDEX,
+ } = require("devtools/client/performance/modules/categories");
+ const RecordingUtils = require("devtools/shared/performance/recording-utils");
+
+ return RecordingUtils.deflateProfile({
+ meta: { version: 2 },
+ threads: [
+ {
+ samples: [
+ {
+ time: 1,
+ frames: [
+ { category: CATEGORY_INDEX("other"), location: "(root)" },
+ {
+ category: CATEGORY_INDEX("js"),
+ location: "A (http://foo/bar/baz:12:9)",
+ },
+ {
+ category: CATEGORY_INDEX("layout"),
+ location: "B InterruptibleLayout",
+ },
+ {
+ category: CATEGORY_INDEX("js"),
+ location: "C (http://foo/bar/baz:56)",
+ },
+ ],
+ },
+ {
+ time: 1 + 1,
+ frames: [
+ { category: CATEGORY_INDEX("other"), location: "(root)" },
+ {
+ category: CATEGORY_INDEX("js"),
+ location: "A (http://foo/bar/baz:12:9)",
+ },
+ {
+ category: CATEGORY_INDEX("layout"),
+ location: "B InterruptibleLayout",
+ },
+ {
+ category: CATEGORY_INDEX("gc"),
+ location: "D INTER_SLICE_GC",
+ },
+ ],
+ },
+ {
+ time: 1 + 1 + 2,
+ frames: [
+ { category: CATEGORY_INDEX("other"), location: "(root)" },
+ {
+ category: CATEGORY_INDEX("js"),
+ location: "A (http://foo/bar/baz:12:9)",
+ },
+ {
+ category: CATEGORY_INDEX("layout"),
+ location: "B InterruptibleLayout",
+ },
+ {
+ category: CATEGORY_INDEX("gc"),
+ location: "D INTER_SLICE_GC",
+ },
+ ],
+ },
+ {
+ time: 1 + 1 + 2 + 3,
+ frames: [
+ { category: CATEGORY_INDEX("other"), location: "(root)" },
+ {
+ category: CATEGORY_INDEX("js"),
+ location: "A (http://foo/bar/baz:12:9)",
+ },
+ {
+ category: CATEGORY_INDEX("gc"),
+ location: "E",
+ },
+ {
+ category: CATEGORY_INDEX("network"),
+ location: "F",
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ });
+};
+
+/**
+ * Generates a simple implementation for a tree class.
+ */
+exports.synthesizeCustomTreeClass = () => {
+ const {
+ AbstractTreeItem,
+ } = require("resource://devtools/client/shared/widgets/AbstractTreeItem.jsm");
+ const { extend } = require("devtools/shared/extend");
+
+ function MyCustomTreeItem(dataSrc, properties) {
+ AbstractTreeItem.call(this, properties);
+ this.itemDataSrc = dataSrc;
+ }
+
+ MyCustomTreeItem.prototype = extend(AbstractTreeItem.prototype, {
+ _displaySelf: function(document, arrowNode) {
+ const node = document.createXULElement("hbox");
+ node.style.marginInlineStart = this.level * 10 + "px";
+ node.appendChild(arrowNode);
+ node.appendChild(document.createTextNode(this.itemDataSrc.label));
+ return node;
+ },
+
+ _populateSelf: function(children) {
+ for (const childDataSrc of this.itemDataSrc.children) {
+ children.push(
+ new MyCustomTreeItem(childDataSrc, {
+ parent: this,
+ level: this.level + 1,
+ })
+ );
+ }
+ },
+ });
+
+ const myDataSrc = {
+ label: "root",
+ children: [
+ {
+ label: "foo",
+ children: [],
+ },
+ {
+ label: "bar",
+ children: [
+ {
+ label: "baz",
+ children: [],
+ },
+ ],
+ },
+ ],
+ };
+
+ return { MyCustomTreeItem, myDataSrc };
+};
diff --git a/devtools/client/performance/test/helpers/tab-utils.js b/devtools/client/performance/test/helpers/tab-utils.js
new file mode 100644
index 0000000000..e9bd6354de
--- /dev/null
+++ b/devtools/client/performance/test/helpers/tab-utils.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* globals dump */
+
+const {
+ BrowserTestUtils,
+} = require("resource://testing-common/BrowserTestUtils.jsm");
+
+/**
+ * Gets a random integer in between an interval. Used to uniquely identify
+ * added tabs by augmenting the URL.
+ */
+function getRandomInt(min, max) {
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+}
+
+/**
+ * Adds a browser tab with the given url in the specified window and waits
+ * for it to load.
+ */
+exports.addTab = function({ url, win }, options = {}) {
+ const id = getRandomInt(0, Number.MAX_SAFE_INTEGER - 1);
+ url += `#${id}`;
+
+ dump(`Adding tab with url: ${url}.\n`);
+
+ const { gBrowser } = win || window;
+ return BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ url,
+ !options.dontWaitForTabReady
+ );
+};
+
+/**
+ * Removes a browser tab from the specified window and waits for it to close.
+ */
+exports.removeTab = function(tab) {
+ dump(`Removing tab: ${tab.linkedBrowser.currentURI.spec}.\n`);
+
+ BrowserTestUtils.removeTab(tab);
+};
diff --git a/devtools/client/performance/test/helpers/urls.js b/devtools/client/performance/test/helpers/urls.js
new file mode 100644
index 0000000000..cd5247823f
--- /dev/null
+++ b/devtools/client/performance/test/helpers/urls.js
@@ -0,0 +1,9 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+exports.EXAMPLE_URL =
+ "http://example.com/browser/devtools/client/performance/test";
+exports.SIMPLE_URL = `${exports.EXAMPLE_URL}/doc_simple-test.html`;
+// Used to test a page running on main process.
+exports.MAIN_PROCESS_URL = "about:robots";
diff --git a/devtools/client/performance/test/helpers/wait-utils.js b/devtools/client/performance/test/helpers/wait-utils.js
new file mode 100644
index 0000000000..48dc31c791
--- /dev/null
+++ b/devtools/client/performance/test/helpers/wait-utils.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* globals dump */
+
+const { CC } = require("chrome");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const {
+ once,
+} = require("devtools/client/performance/test/helpers/event-utils");
+
+/**
+ * Blocks the main thread for the specified amount of time.
+ */
+exports.busyWait = function(time) {
+ dump(`Busy waiting for: ${time} milliseconds.\n`);
+ const start = Date.now();
+ /* eslint-disable no-unused-vars */
+ let stack;
+ while (Date.now() - start < time) {
+ stack = CC.stack;
+ }
+ /* eslint-enable no-unused-vars */
+};
+
+/**
+ * Idly waits for the specified amount of time.
+ */
+exports.idleWait = function(time) {
+ dump(`Idly waiting for: ${time} milliseconds.\n`);
+ return DevToolsUtils.waitForTime(time);
+};
+
+/**
+ * Waits until a predicate returns true.
+ */
+exports.waitUntil = async function(predicate, interval = 100, tries = 100) {
+ for (let i = 1; i <= tries; i++) {
+ if (await predicate()) {
+ dump(`Predicate returned true after ${i} tries.\n`);
+ return;
+ }
+ await exports.idleWait(interval);
+ }
+ throw new Error(`Predicate returned false after ${tries} tries, aborting.\n`);
+};
+
+/**
+ * Waits for a `MozAfterPaint` event to be fired on the specified window.
+ */
+exports.waitForMozAfterPaint = function(window) {
+ return once(window, "MozAfterPaint");
+};
diff --git a/devtools/client/performance/test/js_simpleWorker.js b/devtools/client/performance/test/js_simpleWorker.js
new file mode 100644
index 0000000000..1f77e27e4f
--- /dev/null
+++ b/devtools/client/performance/test/js_simpleWorker.js
@@ -0,0 +1,6 @@
+"use strict";
+
+self.addEventListener("message", function(e) {
+ self.postMessage(e.data);
+ self.close();
+});
diff --git a/devtools/client/performance/test/moz.build b/devtools/client/performance/test/moz.build
new file mode 100644
index 0000000000..728f6a155b
--- /dev/null
+++ b/devtools/client/performance/test/moz.build
@@ -0,0 +1,8 @@
+# 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/.
+
+DIRS += [
+ "helpers",
+]
diff --git a/devtools/client/performance/test/xpcshell/.eslintrc.js b/devtools/client/performance/test/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..7f6b62a9e5
--- /dev/null
+++ b/devtools/client/performance/test/xpcshell/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ extends: "../../../../.eslintrc.xpcshell.js",
+};
diff --git a/devtools/client/performance/test/xpcshell/head.js b/devtools/client/performance/test/xpcshell/head.js
new file mode 100644
index 0000000000..7b684c67d7
--- /dev/null
+++ b/devtools/client/performance/test/xpcshell/head.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/* exported Cc, Ci, Cu, Cr, Services, console, PLATFORM_DATA_PREF, getFrameNodePath,
+ synthesizeProfileForTest */
+var { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
+var Services = require("Services");
+const RecordingUtils = require("devtools/shared/performance/recording-utils");
+const PLATFORM_DATA_PREF = "devtools.performance.ui.show-platform-data";
+
+/**
+ * Get a path in a FrameNode call tree.
+ */
+function getFrameNodePath(root, path) {
+ let calls = root.calls;
+ let foundNode;
+ for (const key of path.split(" > ")) {
+ foundNode = calls.find(node => node.key == key);
+ if (!foundNode) {
+ break;
+ }
+ calls = foundNode.calls;
+ }
+ return foundNode;
+}
+
+/**
+ * Synthesize a profile for testing.
+ */
+function synthesizeProfileForTest(samples) {
+ samples.unshift({
+ time: 0,
+ frames: [{ location: "(root)" }],
+ });
+
+ const uniqueStacks = new RecordingUtils.UniqueStacks();
+ return RecordingUtils.deflateThread(
+ {
+ samples: samples,
+ markers: [],
+ },
+ uniqueStacks
+ );
+}
diff --git a/devtools/client/performance/test/xpcshell/test_frame-utils-01.js b/devtools/client/performance/test/xpcshell/test_frame-utils-01.js
new file mode 100644
index 0000000000..3df04964e0
--- /dev/null
+++ b/devtools/client/performance/test/xpcshell/test_frame-utils-01.js
@@ -0,0 +1,293 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that frame-utils isContent and parseLocation work as intended
+ * when parsing over frames from the profiler.
+ */
+
+const CONTENT_LOCATIONS = [
+ "hello/<.world (https://foo/bar.js:123:987)",
+ "hello/<.world (http://foo/bar.js:123:987)",
+ "hello/<.world (http://foo/bar.js:123)",
+ "hello/<.world (http://foo/bar.js#baz:123:987)",
+ "hello/<.world (http://foo/bar.js?myquery=params&search=1:123:987)",
+ "hello/<.world (http://foo/#bar:123:987)",
+ "hello/<.world (http://foo/:123:987)",
+
+ // Test scripts with port numbers (bug 1164131)
+ "hello/<.world (http://localhost:8888/file.js:100:1)",
+ "hello/<.world (http://localhost:8888/file.js:100)",
+
+ // Eval
+ "hello/<.world (http://localhost:8888/file.js line 65 > eval:1)",
+
+ // Occurs when executing an inline script on a root html page with port
+ // (I've never seen it with a column number but check anyway) bug 1164131
+ "hello/<.world (http://localhost:8888/:1)",
+ "hello/<.world (http://localhost:8888/:100:50)",
+
+ // bug 1197636
+ 'Native["arraycopy(blah)"] (http://localhost:8888/profiler.html:4)',
+ 'Native["arraycopy(blah)"] (http://localhost:8888/profiler.html:4:5)',
+].map(argify);
+
+const CHROME_LOCATIONS = [
+ { location: "Startup::XRE_InitChildProcess", line: 456, column: 123 },
+ { location: "chrome://browser/content/content.js", line: 456, column: 123 },
+ "setTimeout_timer (resource://gre/foo.js:123:434)",
+ "hello/<.world (jar:file://Users/mcurie/Dev/jetpacks.js)",
+ "hello/<.world (resource://foo.js -> http://bar/baz.js:123:987)",
+ "EnterJIT",
+].map(argify);
+
+add_task(function() {
+ const {
+ computeIsContentAndCategory,
+ parseLocation,
+ } = require("devtools/client/performance/modules/logic/frame-utils");
+ const isContent = frame => {
+ computeIsContentAndCategory(frame);
+ return frame.isContent;
+ };
+
+ for (const frame of CONTENT_LOCATIONS) {
+ ok(
+ isContent.apply(null, frameify(frame)),
+ `${frame[0]} should be considered a content frame.`
+ );
+ }
+
+ for (const frame of CHROME_LOCATIONS) {
+ ok(
+ !isContent.apply(null, frameify(frame)),
+ `${frame[0]} should not be considered a content frame.`
+ );
+ }
+
+ // functionName, fileName, host, url, line, column
+ const FIELDS = [
+ "functionName",
+ "fileName",
+ "host",
+ "url",
+ "line",
+ "column",
+ "host",
+ "port",
+ ];
+
+ /* eslint-disable max-len */
+ const PARSED_CONTENT = [
+ [
+ "hello/<.world",
+ "bar.js",
+ "foo",
+ "https://foo/bar.js",
+ 123,
+ 987,
+ "foo",
+ null,
+ ],
+ [
+ "hello/<.world",
+ "bar.js",
+ "foo",
+ "http://foo/bar.js",
+ 123,
+ 987,
+ "foo",
+ null,
+ ],
+ [
+ "hello/<.world",
+ "bar.js",
+ "foo",
+ "http://foo/bar.js",
+ 123,
+ null,
+ "foo",
+ null,
+ ],
+ [
+ "hello/<.world",
+ "bar.js",
+ "foo",
+ "http://foo/bar.js#baz",
+ 123,
+ 987,
+ "foo",
+ null,
+ ],
+ [
+ "hello/<.world",
+ "bar.js",
+ "foo",
+ "http://foo/bar.js?myquery=params&search=1",
+ 123,
+ 987,
+ "foo",
+ null,
+ ],
+ ["hello/<.world", "/", "foo", "http://foo/#bar", 123, 987, "foo", null],
+ ["hello/<.world", "/", "foo", "http://foo/", 123, 987, "foo", null],
+ [
+ "hello/<.world",
+ "file.js",
+ "localhost:8888",
+ "http://localhost:8888/file.js",
+ 100,
+ 1,
+ "localhost:8888",
+ 8888,
+ ],
+ [
+ "hello/<.world",
+ "file.js",
+ "localhost:8888",
+ "http://localhost:8888/file.js",
+ 100,
+ null,
+ "localhost:8888",
+ 8888,
+ ],
+ [
+ "hello/<.world",
+ "file.js (eval:1)",
+ "localhost:8888",
+ "http://localhost:8888/file.js",
+ 65,
+ null,
+ "localhost:8888",
+ 8888,
+ ],
+ [
+ "hello/<.world",
+ "/",
+ "localhost:8888",
+ "http://localhost:8888/",
+ 1,
+ null,
+ "localhost:8888",
+ 8888,
+ ],
+ [
+ "hello/<.world",
+ "/",
+ "localhost:8888",
+ "http://localhost:8888/",
+ 100,
+ 50,
+ "localhost:8888",
+ 8888,
+ ],
+ [
+ 'Native["arraycopy(blah)"]',
+ "profiler.html",
+ "localhost:8888",
+ "http://localhost:8888/profiler.html",
+ 4,
+ null,
+ "localhost:8888",
+ 8888,
+ ],
+ [
+ 'Native["arraycopy(blah)"]',
+ "profiler.html",
+ "localhost:8888",
+ "http://localhost:8888/profiler.html",
+ 4,
+ 5,
+ "localhost:8888",
+ 8888,
+ ],
+ ];
+ /* eslint-enable max-len */
+
+ for (let i = 0; i < PARSED_CONTENT.length; i++) {
+ const parsed = parseLocation.apply(null, CONTENT_LOCATIONS[i]);
+ for (let j = 0; j < FIELDS.length; j++) {
+ equal(
+ parsed[FIELDS[j]],
+ PARSED_CONTENT[i][j],
+ `${CONTENT_LOCATIONS[i]} was parsed to correct ${FIELDS[j]}`
+ );
+ }
+ }
+
+ const PARSED_CHROME = [
+ ["Startup::XRE_InitChildProcess", null, null, null, 456, 123, null, null],
+ [
+ "chrome://browser/content/content.js",
+ null,
+ null,
+ null,
+ 456,
+ 123,
+ null,
+ null,
+ ],
+ [
+ "setTimeout_timer",
+ "foo.js",
+ null,
+ "resource://gre/foo.js",
+ 123,
+ 434,
+ null,
+ null,
+ ],
+ [
+ "hello/<.world (jar:file://Users/mcurie/Dev/jetpacks.js)",
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ ],
+ [
+ "hello/<.world",
+ "baz.js",
+ "bar",
+ "http://bar/baz.js",
+ 123,
+ 987,
+ "bar",
+ null,
+ ],
+ ["EnterJIT", null, null, null, null, null, null, null],
+ ];
+
+ for (let i = 0; i < PARSED_CHROME.length; i++) {
+ const parsed = parseLocation.apply(null, CHROME_LOCATIONS[i]);
+ for (let j = 0; j < FIELDS.length; j++) {
+ equal(
+ parsed[FIELDS[j]],
+ PARSED_CHROME[i][j],
+ `${CHROME_LOCATIONS[i]} was parsed to correct ${FIELDS[j]}`
+ );
+ }
+ }
+});
+
+/**
+ * Takes either a string or an object and turns it into an array that
+ * parseLocation.apply expects.
+ */
+function argify(val) {
+ if (typeof val === "string") {
+ return [val];
+ }
+ return [val.location, val.line, val.column];
+}
+
+/**
+ * Takes the result of argify and turns it into an array that can be passed to
+ * isContent.apply.
+ */
+function frameify(val) {
+ return [{ location: val[0] }];
+}
diff --git a/devtools/client/performance/test/xpcshell/test_frame-utils-02.js b/devtools/client/performance/test/xpcshell/test_frame-utils-02.js
new file mode 100644
index 0000000000..9d607c4046
--- /dev/null
+++ b/devtools/client/performance/test/xpcshell/test_frame-utils-02.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests the function testing whether or not a frame is content or chrome
+ * works properly.
+ */
+
+add_task(function() {
+ const FrameUtils = require("devtools/client/performance/modules/logic/frame-utils");
+
+ const isContent = frame => {
+ FrameUtils.computeIsContentAndCategory(frame);
+ return frame.isContent;
+ };
+
+ ok(
+ isContent({ location: "http://foo" }),
+ "Verifying content/chrome frames is working properly."
+ );
+ ok(
+ isContent({ location: "https://foo" }),
+ "Verifying content/chrome frames is working properly."
+ );
+ ok(
+ isContent({ location: "file://foo" }),
+ "Verifying content/chrome frames is working properly."
+ );
+
+ ok(
+ !isContent({ location: "chrome://foo" }),
+ "Verifying content/chrome frames is working properly."
+ );
+ ok(
+ !isContent({ location: "resource://foo" }),
+ "Verifying content/chrome frames is working properly."
+ );
+
+ ok(
+ !isContent({ location: "chrome://foo -> http://bar" }),
+ "Verifying content/chrome frames is working properly."
+ );
+ ok(
+ !isContent({ location: "chrome://foo -> https://bar" }),
+ "Verifying content/chrome frames is working properly."
+ );
+ ok(
+ !isContent({ location: "chrome://foo -> file://bar" }),
+ "Verifying content/chrome frames is working properly."
+ );
+
+ ok(
+ !isContent({ location: "resource://foo -> http://bar" }),
+ "Verifying content/chrome frames is working properly."
+ );
+ ok(
+ !isContent({ location: "resource://foo -> https://bar" }),
+ "Verifying content/chrome frames is working properly."
+ );
+ ok(
+ !isContent({ location: "resource://foo -> file://bar" }),
+ "Verifying content/chrome frames is working properly."
+ );
+
+ ok(
+ !isContent({ category: 1, location: "chrome://foo" }),
+ "Verifying content/chrome frames is working properly."
+ );
+ ok(
+ !isContent({ category: 1, location: "resource://foo" }),
+ "Verifying content/chrome frames is working properly."
+ );
+
+ ok(
+ isContent({ category: 1, location: "file://foo -> http://bar" }),
+ "Verifying content/chrome frames is working properly."
+ );
+ ok(
+ isContent({ category: 1, location: "file://foo -> https://bar" }),
+ "Verifying content/chrome frames is working properly."
+ );
+ ok(
+ isContent({ category: 1, location: "file://foo -> file://bar" }),
+ "Verifying content/chrome frames is working properly."
+ );
+});
diff --git a/devtools/client/performance/test/xpcshell/test_jit-graph-data.js b/devtools/client/performance/test/xpcshell/test_jit-graph-data.js
new file mode 100644
index 0000000000..3f893cd8c3
--- /dev/null
+++ b/devtools/client/performance/test/xpcshell/test_jit-graph-data.js
@@ -0,0 +1,238 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Unit test for `createTierGraphDataFromFrameNode` function.
+ */
+
+const SAMPLE_COUNT = 1000;
+const RESOLUTION = 50;
+const TIME_PER_SAMPLE = 5;
+
+// Offset needed since ThreadNode requires the first sample to be strictly
+// greater than its start time. This lets us still have pretty numbers
+// in this test to keep it (more) simple, which it sorely needs.
+const TIME_OFFSET = 5;
+
+add_task(function test() {
+ const {
+ ThreadNode,
+ } = require("devtools/client/performance/modules/logic/tree-model");
+ const {
+ createTierGraphDataFromFrameNode,
+ } = require("devtools/client/performance/modules/logic/jit");
+
+ // Select the second half of the set of samples
+ const startTime = (SAMPLE_COUNT / 2) * TIME_PER_SAMPLE - TIME_OFFSET;
+ const endTime = SAMPLE_COUNT * TIME_PER_SAMPLE - TIME_OFFSET;
+ const invertTree = true;
+
+ const root = new ThreadNode(gThread, { invertTree, startTime, endTime });
+
+ equal(root.samples, SAMPLE_COUNT / 2, "root has correct amount of samples");
+ equal(
+ root.sampleTimes.length,
+ SAMPLE_COUNT / 2,
+ "root has correct amount of sample times"
+ );
+ // Add time offset since the first sample begins TIME_OFFSET after startTime
+ equal(
+ root.sampleTimes[0],
+ startTime + TIME_OFFSET,
+ "root recorded first sample time in scope"
+ );
+ equal(
+ root.sampleTimes[root.sampleTimes.length - 1],
+ endTime,
+ "root recorded last sample time in scope"
+ );
+
+ const frame = getFrameNodePath(root, "X");
+ let data = createTierGraphDataFromFrameNode(
+ frame,
+ root.sampleTimes,
+ (endTime - startTime) / RESOLUTION
+ );
+
+ const TIME_PER_WINDOW = (SAMPLE_COUNT / 2 / RESOLUTION) * TIME_PER_SAMPLE;
+
+ // Filter out the dupes created with the same delta so the graph
+ // can render correctly.
+ const filteredData = [];
+ for (let i = 0; i < data.length; i++) {
+ if (!i || data[i].delta !== data[i - 1].delta) {
+ filteredData.push(data[i]);
+ }
+ }
+ data = filteredData;
+
+ for (let i = 0; i < 11; i++) {
+ equal(
+ data[i].delta,
+ startTime + TIME_OFFSET + TIME_PER_WINDOW * i,
+ "first window has correct x"
+ );
+ equal(data[i].values[0], 0.2, "first window has 2 frames in interpreter");
+ equal(data[i].values[1], 0.2, "first window has 2 frames in baseline");
+ equal(data[i].values[2], 0.2, "first window has 2 frames in ion");
+ }
+ // Start on 11, since i===10 is where the values change, and the new value (0,0,0)
+ // is removed in `filteredData`
+ for (let i = 11; i < 20; i++) {
+ equal(
+ data[i].delta,
+ startTime + TIME_OFFSET + TIME_PER_WINDOW * i,
+ "second window has correct x"
+ );
+ equal(data[i].values[0], 0, "second window observed no optimizations");
+ equal(data[i].values[1], 0, "second window observed no optimizations");
+ equal(data[i].values[2], 0, "second window observed no optimizations");
+ }
+ // Start on 21, since i===20 is where the values change, and the new value (0.3,0,0)
+ // is removed in `filteredData`
+ for (let i = 21; i < 30; i++) {
+ equal(
+ data[i].delta,
+ startTime + TIME_OFFSET + TIME_PER_WINDOW * i,
+ "third window has correct x"
+ );
+ equal(data[i].values[0], 0.3, "third window has 3 frames in interpreter");
+ equal(data[i].values[1], 0, "third window has 0 frames in baseline");
+ equal(data[i].values[2], 0, "third window has 0 frames in ion");
+ }
+});
+
+var gUniqueStacks = new RecordingUtils.UniqueStacks();
+
+function uniqStr(s) {
+ return gUniqueStacks.getOrAddStringIndex(s);
+}
+
+const TIER_PATTERNS = [
+ // 0-99
+ ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"],
+ // 100-199
+ ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"],
+ // 200-299
+ ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"],
+ // 300-399
+ ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"],
+ // 400-499
+ ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"],
+
+ // 500-599
+ // Test current frames in all opts
+ ["A", "A", "A", "A", "X_1", "X_2", "X_1", "X_2", "X_0", "X_0"],
+
+ // 600-699
+ // Nothing for current frame
+ ["A", "B", "A", "B", "A", "B", "A", "B", "A", "B"],
+
+ // 700-799
+ // A few frames where the frame is not the leaf node
+ ["X_2 -> Y", "X_2 -> Y", "X_2 -> Y", "X_0", "X_0", "X_0", "A", "A", "A", "A"],
+
+ // 800-899
+ ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"],
+ // 900-999
+ ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"],
+];
+
+function createSample(i, frames) {
+ const sample = {};
+ sample.time = i * TIME_PER_SAMPLE;
+ sample.frames = [{ location: "(root)" }];
+ if (i === 0) {
+ return sample;
+ }
+ if (frames) {
+ frames
+ .split(" -> ")
+ .forEach(frame => sample.frames.push({ location: frame }));
+ }
+ return sample;
+}
+
+var SAMPLES = (function() {
+ const samples = [];
+
+ for (let i = 0; i < SAMPLE_COUNT; ) {
+ const pattern = TIER_PATTERNS[Math.floor(i / 100)];
+ for (let j = 0; j < pattern.length; j++) {
+ samples.push(createSample(i + j, pattern[j]));
+ }
+ i += 10;
+ }
+
+ return samples;
+})();
+
+var gThread = RecordingUtils.deflateThread(
+ { samples: SAMPLES, markers: [] },
+ gUniqueStacks
+);
+
+var gRawSite1 = {
+ line: 12,
+ column: 2,
+ types: [
+ {
+ mirType: uniqStr("Object"),
+ site: uniqStr("B (http://foo/bar:10)"),
+ typeset: [
+ {
+ keyedBy: uniqStr("constructor"),
+ name: uniqStr("Foo"),
+ location: uniqStr("B (http://foo/bar:10)"),
+ },
+ {
+ keyedBy: uniqStr("primitive"),
+ location: uniqStr("self-hosted"),
+ },
+ ],
+ },
+ ],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1,
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ [uniqStr("Inlined"), uniqStr("SomeGetter3")],
+ ],
+ },
+};
+
+function serialize(x) {
+ return JSON.parse(JSON.stringify(x));
+}
+
+gThread.frameTable.data.forEach(frame => {
+ const LOCATION_SLOT = gThread.frameTable.schema.location;
+ const OPTIMIZATIONS_SLOT = gThread.frameTable.schema.optimizations;
+ const IMPLEMENTATION_SLOT = gThread.frameTable.schema.implementation;
+
+ const l = gThread.stringTable[frame[LOCATION_SLOT]];
+ switch (l) {
+ // Rename some of the location sites so we can register different
+ // frames with different opt sites
+ case "X_0":
+ frame[LOCATION_SLOT] = uniqStr("X");
+ frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
+ frame[IMPLEMENTATION_SLOT] = null;
+ break;
+ case "X_1":
+ frame[LOCATION_SLOT] = uniqStr("X");
+ frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
+ frame[IMPLEMENTATION_SLOT] = uniqStr("baseline");
+ break;
+ case "X_2":
+ frame[LOCATION_SLOT] = uniqStr("X");
+ frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
+ frame[IMPLEMENTATION_SLOT] = uniqStr("ion");
+ break;
+ }
+});
diff --git a/devtools/client/performance/test/xpcshell/test_jit-model-01.js b/devtools/client/performance/test/xpcshell/test_jit-model-01.js
new file mode 100644
index 0000000000..3dd4dad559
--- /dev/null
+++ b/devtools/client/performance/test/xpcshell/test_jit-model-01.js
@@ -0,0 +1,154 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that JITOptimizations track optimization sites and create
+ * an OptimizationSiteProfile when adding optimization sites, like from the
+ * FrameNode, and the returning of that data is as expected.
+ */
+
+add_task(function test() {
+ const {
+ JITOptimizations,
+ } = require("devtools/client/performance/modules/logic/jit");
+
+ const rawSites = [];
+ rawSites.push(gRawSite2);
+ rawSites.push(gRawSite2);
+ rawSites.push(gRawSite1);
+ rawSites.push(gRawSite1);
+ rawSites.push(gRawSite2);
+ rawSites.push(gRawSite3);
+
+ const jit = new JITOptimizations(rawSites, gStringTable.stringTable);
+ const sites = jit.optimizationSites;
+
+ const [first, second, third] = sites;
+
+ equal(first.id, 0, "site id is array index");
+ equal(
+ first.samples,
+ 3,
+ "first OptimizationSiteProfile has correct sample count"
+ );
+ equal(
+ first.data.line,
+ 34,
+ "includes OptimizationSite as reference under `data`"
+ );
+ equal(second.id, 1, "site id is array index");
+ equal(
+ second.samples,
+ 2,
+ "second OptimizationSiteProfile has correct sample count"
+ );
+ equal(
+ second.data.line,
+ 12,
+ "includes OptimizationSite as reference under `data`"
+ );
+ equal(third.id, 2, "site id is array index");
+ equal(
+ third.samples,
+ 1,
+ "third OptimizationSiteProfile has correct sample count"
+ );
+ equal(
+ third.data.line,
+ 78,
+ "includes OptimizationSite as reference under `data`"
+ );
+});
+
+var gStringTable = new RecordingUtils.UniqueStrings();
+
+function uniqStr(s) {
+ return gStringTable.getOrAddStringIndex(s);
+}
+
+var gRawSite1 = {
+ line: 12,
+ column: 2,
+ types: [
+ {
+ mirType: uniqStr("Object"),
+ site: uniqStr("A (http://foo/bar/bar:12)"),
+ typeset: [
+ {
+ keyedBy: uniqStr("constructor"),
+ name: uniqStr("Foo"),
+ location: uniqStr("A (http://foo/bar/baz:12)"),
+ },
+ {
+ keyedBy: uniqStr("primitive"),
+ location: uniqStr("self-hosted"),
+ },
+ ],
+ },
+ ],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1,
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ [uniqStr("Inlined"), uniqStr("SomeGetter3")],
+ ],
+ },
+};
+
+var gRawSite2 = {
+ line: 34,
+ types: [
+ {
+ mirType: uniqStr("Int32"),
+ site: uniqStr("Receiver"),
+ },
+ ],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1,
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ [uniqStr("Failure3"), uniqStr("SomeGetter3")],
+ ],
+ },
+};
+
+var gRawSite3 = {
+ line: 78,
+ types: [
+ {
+ mirType: uniqStr("Object"),
+ site: uniqStr("A (http://foo/bar/bar:12)"),
+ typeset: [
+ {
+ keyedBy: uniqStr("constructor"),
+ name: uniqStr("Foo"),
+ location: uniqStr("A (http://foo/bar/baz:12)"),
+ },
+ {
+ keyedBy: uniqStr("primitive"),
+ location: uniqStr("self-hosted"),
+ },
+ ],
+ },
+ ],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1,
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ [uniqStr("GenericSuccess"), uniqStr("SomeGetter3")],
+ ],
+ },
+};
diff --git a/devtools/client/performance/test/xpcshell/test_jit-model-02.js b/devtools/client/performance/test/xpcshell/test_jit-model-02.js
new file mode 100644
index 0000000000..3da3325c23
--- /dev/null
+++ b/devtools/client/performance/test/xpcshell/test_jit-model-02.js
@@ -0,0 +1,196 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that JITOptimizations create OptimizationSites, and the underlying
+ * hasSuccessfulOutcome/isSuccessfulOutcome work as intended.
+ */
+
+add_task(function test() {
+ const {
+ JITOptimizations,
+ hasSuccessfulOutcome,
+ isSuccessfulOutcome,
+ SUCCESSFUL_OUTCOMES,
+ } = require("devtools/client/performance/modules/logic/jit");
+
+ const rawSites = [];
+ rawSites.push(gRawSite2);
+ rawSites.push(gRawSite2);
+ rawSites.push(gRawSite1);
+ rawSites.push(gRawSite1);
+ rawSites.push(gRawSite2);
+ rawSites.push(gRawSite3);
+
+ const jit = new JITOptimizations(rawSites, gStringTable.stringTable);
+ const sites = jit.optimizationSites;
+
+ const [first, second, third] = sites;
+
+ /* hasSuccessfulOutcome */
+ equal(
+ hasSuccessfulOutcome(first),
+ false,
+ "hasSuccessfulOutcome() returns expected (1)"
+ );
+ equal(
+ hasSuccessfulOutcome(second),
+ true,
+ "hasSuccessfulOutcome() returns expected (2)"
+ );
+ equal(
+ hasSuccessfulOutcome(third),
+ true,
+ "hasSuccessfulOutcome() returns expected (3)"
+ );
+
+ /* .data.attempts */
+ equal(
+ first.data.attempts.length,
+ 2,
+ "optSite.data.attempts has the correct amount of attempts (1)"
+ );
+ equal(
+ second.data.attempts.length,
+ 5,
+ "optSite.data.attempts has the correct amount of attempts (2)"
+ );
+ equal(
+ third.data.attempts.length,
+ 3,
+ "optSite.data.attempts has the correct amount of attempts (3)"
+ );
+
+ /* .data.types */
+ equal(
+ first.data.types.length,
+ 1,
+ "optSite.data.types has the correct amount of IonTypes (1)"
+ );
+ equal(
+ second.data.types.length,
+ 2,
+ "optSite.data.types has the correct amount of IonTypes (2)"
+ );
+ equal(
+ third.data.types.length,
+ 1,
+ "optSite.data.types has the correct amount of IonTypes (3)"
+ );
+
+ /* isSuccessfulOutcome */
+ ok(
+ SUCCESSFUL_OUTCOMES.length,
+ "Have some successful outcomes in SUCCESSFUL_OUTCOMES"
+ );
+ SUCCESSFUL_OUTCOMES.forEach(outcome =>
+ ok(
+ isSuccessfulOutcome(outcome),
+ `${outcome} considered a successful outcome via isSuccessfulOutcome()`
+ )
+ );
+});
+
+var gStringTable = new RecordingUtils.UniqueStrings();
+
+function uniqStr(s) {
+ return gStringTable.getOrAddStringIndex(s);
+}
+
+var gRawSite1 = {
+ line: 12,
+ column: 2,
+ types: [
+ {
+ mirType: uniqStr("Object"),
+ site: uniqStr("A (http://foo/bar/bar:12)"),
+ typeset: [
+ {
+ keyedBy: uniqStr("constructor"),
+ name: uniqStr("Foo"),
+ location: uniqStr("A (http://foo/bar/baz:12)"),
+ },
+ {
+ keyedBy: uniqStr("constructor"),
+ location: uniqStr("A (http://foo/bar/baz:12)"),
+ },
+ ],
+ },
+ {
+ mirType: uniqStr("Int32"),
+ site: uniqStr("A (http://foo/bar/bar:12)"),
+ typeset: [
+ {
+ keyedBy: uniqStr("primitive"),
+ location: uniqStr("self-hosted"),
+ },
+ ],
+ },
+ ],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1,
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ [uniqStr("Inlined"), uniqStr("SomeGetter3")],
+ ],
+ },
+};
+
+var gRawSite2 = {
+ line: 34,
+ types: [
+ {
+ mirType: uniqStr("Int32"),
+ site: uniqStr("Receiver"),
+ },
+ ],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1,
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ ],
+ },
+};
+
+var gRawSite3 = {
+ line: 78,
+ types: [
+ {
+ mirType: uniqStr("Object"),
+ site: uniqStr("A (http://foo/bar/bar:12)"),
+ typeset: [
+ {
+ keyedBy: uniqStr("constructor"),
+ name: uniqStr("Foo"),
+ location: uniqStr("A (http://foo/bar/baz:12)"),
+ },
+ {
+ keyedBy: uniqStr("primitive"),
+ location: uniqStr("self-hosted"),
+ },
+ ],
+ },
+ ],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1,
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ [uniqStr("GenericSuccess"), uniqStr("SomeGetter3")],
+ ],
+ },
+};
diff --git a/devtools/client/performance/test/xpcshell/test_marker-blueprint.js b/devtools/client/performance/test/xpcshell/test_marker-blueprint.js
new file mode 100644
index 0000000000..986b8e399f
--- /dev/null
+++ b/devtools/client/performance/test/xpcshell/test_marker-blueprint.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/**
+ * Tests if the timeline blueprint has a correct structure.
+ */
+
+add_task(function() {
+ const {
+ TIMELINE_BLUEPRINT,
+ } = require("devtools/client/performance/modules/markers");
+
+ ok(TIMELINE_BLUEPRINT, "A timeline blueprint should be available.");
+
+ ok(
+ Object.keys(TIMELINE_BLUEPRINT).length,
+ "The timeline blueprint has at least one entry."
+ );
+
+ for (const value of Object.values(TIMELINE_BLUEPRINT)) {
+ ok(
+ "group" in value,
+ "Each entry in the timeline blueprint contains a `group` key."
+ );
+ ok(
+ "colorName" in value,
+ "Each entry in the timeline blueprint contains a `colorName` key."
+ );
+ ok(
+ "label" in value,
+ "Each entry in the timeline blueprint contains a `label` key."
+ );
+ }
+});
diff --git a/devtools/client/performance/test/xpcshell/test_marker-utils.js b/devtools/client/performance/test/xpcshell/test_marker-utils.js
new file mode 100644
index 0000000000..1feef709ac
--- /dev/null
+++ b/devtools/client/performance/test/xpcshell/test_marker-utils.js
@@ -0,0 +1,204 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests the marker utils methods.
+ */
+
+add_task(function() {
+ const {
+ TIMELINE_BLUEPRINT,
+ } = require("devtools/client/performance/modules/markers");
+ const { PREFS } = require("devtools/client/performance/modules/global");
+ const {
+ MarkerBlueprintUtils,
+ } = require("devtools/client/performance/modules/marker-blueprint-utils");
+
+ PREFS.registerObserver();
+
+ Services.prefs.setBoolPref(PLATFORM_DATA_PREF, false);
+
+ equal(
+ MarkerBlueprintUtils.getMarkerLabel({ name: "DOMEvent" }),
+ "DOM Event",
+ "getMarkerLabel() returns a simple label"
+ );
+ equal(
+ MarkerBlueprintUtils.getMarkerLabel({
+ name: "Javascript",
+ causeName: "setTimeout handler",
+ }),
+ "setTimeout",
+ "getMarkerLabel() returns a label defined via function"
+ );
+ equal(
+ MarkerBlueprintUtils.getMarkerLabel({
+ name: "GarbageCollection",
+ causeName: "ALLOC_TRIGGER",
+ }),
+ "Incremental GC",
+ "getMarkerLabel() returns a label for a function that is generalizable"
+ );
+
+ ok(
+ MarkerBlueprintUtils.getMarkerFields({ name: "Paint" }).length === 0,
+ "getMarkerFields() returns an empty array when no fields defined"
+ );
+
+ let fields = MarkerBlueprintUtils.getMarkerFields({
+ name: "ConsoleTime",
+ causeName: "snowstorm",
+ });
+ equal(
+ fields[0].label,
+ "Timer Name:",
+ "getMarkerFields() returns an array with proper label"
+ );
+ equal(
+ fields[0].value,
+ "snowstorm",
+ "getMarkerFields() returns an array with proper value"
+ );
+
+ fields = MarkerBlueprintUtils.getMarkerFields({
+ name: "DOMEvent",
+ type: "mouseclick",
+ });
+ equal(
+ fields.length,
+ 1,
+ "getMarkerFields() ignores fields that are not found on marker"
+ );
+ equal(
+ fields[0].label,
+ "Event Type:",
+ "getMarkerFields() returns an array with proper label"
+ );
+ equal(
+ fields[0].value,
+ "mouseclick",
+ "getMarkerFields() returns an array with proper value"
+ );
+
+ fields = MarkerBlueprintUtils.getMarkerFields({
+ name: "DOMEvent",
+ eventPhase: Event.AT_TARGET,
+ type: "mouseclick",
+ });
+ equal(
+ fields.length,
+ 2,
+ "getMarkerFields() returns multiple fields when using a fields function"
+ );
+ equal(
+ fields[0].label,
+ "Event Type:",
+ "getMarkerFields() correctly returns fields via function (1)"
+ );
+ equal(
+ fields[0].value,
+ "mouseclick",
+ "getMarkerFields() correctly returns fields via function (2)"
+ );
+ equal(
+ fields[1].label,
+ "Phase:",
+ "getMarkerFields() correctly returns fields via function (3)"
+ );
+ equal(
+ fields[1].value,
+ "Target",
+ "getMarkerFields() correctly returns fields via function (4)"
+ );
+
+ fields = MarkerBlueprintUtils.getMarkerFields({
+ name: "GarbageCollection",
+ causeName: "ALLOC_TRIGGER",
+ });
+ equal(fields[0].value, "Too Many Allocations", "Uses L10N for GC reasons");
+
+ fields = MarkerBlueprintUtils.getMarkerFields({
+ name: "GarbageCollection",
+ causeName: "NOT_A_GC_REASON",
+ });
+ equal(
+ fields[0].value,
+ "NOT_A_GC_REASON",
+ "Defaults to enum for GC reasons when not L10N'd"
+ );
+
+ equal(
+ MarkerBlueprintUtils.getMarkerFields({
+ name: "Javascript",
+ causeName: "Some Platform Field",
+ })[0].value,
+ "(Gecko)",
+ "Correctly obfuscates JS markers when platform data is off."
+ );
+ Services.prefs.setBoolPref(PLATFORM_DATA_PREF, true);
+ equal(
+ MarkerBlueprintUtils.getMarkerFields({
+ name: "Javascript",
+ causeName: "Some Platform Field",
+ })[0].value,
+ "Some Platform Field",
+ "Correctly deobfuscates JS markers when platform data is on."
+ );
+
+ equal(
+ MarkerBlueprintUtils.getMarkerGenericName("Javascript"),
+ "Function Call",
+ "getMarkerGenericName() returns correct string when defined via function"
+ );
+ equal(
+ MarkerBlueprintUtils.getMarkerGenericName("GarbageCollection"),
+ "Garbage Collection",
+ "getMarkerGenericName() returns correct string when defined via function"
+ );
+ equal(
+ MarkerBlueprintUtils.getMarkerGenericName("Reflow"),
+ "Layout",
+ "getMarkerGenericName() returns correct string when defined via string"
+ );
+
+ TIMELINE_BLUEPRINT.fakemarker = { group: 0 };
+ try {
+ MarkerBlueprintUtils.getMarkerGenericName("fakemarker");
+ ok(
+ false,
+ "getMarkerGenericName() should throw when no label on blueprint."
+ );
+ } catch (e) {
+ ok(true, "getMarkerGenericName() should throw when no label on blueprint.");
+ }
+
+ TIMELINE_BLUEPRINT.fakemarker = { group: 0, label: () => void 0 };
+ try {
+ MarkerBlueprintUtils.getMarkerGenericName("fakemarker");
+ ok(
+ false,
+ "getMarkerGenericName() should throw when label function returnd undefined."
+ );
+ } catch (e) {
+ ok(
+ true,
+ "getMarkerGenericName() should throw when label function returnd undefined."
+ );
+ }
+
+ delete TIMELINE_BLUEPRINT.fakemarker;
+
+ equal(
+ MarkerBlueprintUtils.getBlueprintFor({ name: "Reflow" }).label,
+ "Layout",
+ "getBlueprintFor() should return marker def for passed in marker."
+ );
+ equal(
+ MarkerBlueprintUtils.getBlueprintFor({ name: "Not sure!" }).label(),
+ "Unknown",
+ "getBlueprintFor() should return a default marker def if the marker is undefined."
+ );
+
+ PREFS.unregisterObserver();
+});
diff --git a/devtools/client/performance/test/xpcshell/test_perf-utils-allocations-to-samples.js b/devtools/client/performance/test/xpcshell/test_perf-utils-allocations-to-samples.js
new file mode 100644
index 0000000000..e1c2c145e8
--- /dev/null
+++ b/devtools/client/performance/test/xpcshell/test_perf-utils-allocations-to-samples.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if allocations data received from the performance actor is properly
+ * converted to something that follows the same structure as the samples data
+ * received from the profiler.
+ */
+
+add_task(function() {
+ const {
+ getProfileThreadFromAllocations,
+ } = require("devtools/shared/performance/recording-utils");
+ const output = getProfileThreadFromAllocations(TEST_DATA);
+ equal(
+ JSON.stringify(output),
+ JSON.stringify(EXPECTED_OUTPUT),
+ "The output is correct."
+ );
+});
+
+var TEST_DATA = {
+ sites: [0, 0, 1, 2, 3],
+ timestamps: [50, 100, 150, 200, 250],
+ sizes: [0, 0, 100, 200, 300],
+ frames: [
+ null,
+ {
+ source: "A",
+ line: 1,
+ column: 2,
+ functionDisplayName: "x",
+ parent: 0,
+ },
+ {
+ source: "B",
+ line: 3,
+ column: 4,
+ functionDisplayName: "y",
+ parent: 1,
+ },
+ {
+ source: "C",
+ line: 5,
+ column: 6,
+ functionDisplayName: null,
+ parent: 2,
+ },
+ ],
+};
+
+var EXPECTED_OUTPUT = {
+ name: "allocations",
+ samples: {
+ schema: {
+ stack: 0,
+ time: 1,
+ size: 2,
+ },
+ data: [
+ [1, 150, 100],
+ [2, 200, 200],
+ [3, 250, 300],
+ ],
+ },
+ stackTable: {
+ schema: {
+ prefix: 0,
+ frame: 1,
+ },
+ data: [
+ null,
+ [null, 1], // x (A:1:2)
+ [1, 2], // x (A:1:2) > y (B:3:4)
+ [2, 3], // x (A:1:2) > y (B:3:4) > C:5:6
+ ],
+ },
+ frameTable: {
+ schema: {
+ location: 0,
+ implementation: 1,
+ optimizations: 2,
+ line: 3,
+ category: 4,
+ },
+ data: [null, [0], [1], [2]],
+ },
+ stringTable: ["x (A:1:2)", "y (B:3:4)", "C:5:6"],
+};
diff --git a/devtools/client/performance/test/xpcshell/test_profiler-categories.js b/devtools/client/performance/test/xpcshell/test_profiler-categories.js
new file mode 100644
index 0000000000..ae21efbae9
--- /dev/null
+++ b/devtools/client/performance/test/xpcshell/test_profiler-categories.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the profiler categories are mapped correctly.
+ */
+
+add_task(function() {
+ const {
+ CATEGORIES,
+ } = require("devtools/client/performance/modules/categories");
+ const { L10N } = require("devtools/client/performance/modules/global");
+ const count = CATEGORIES.length;
+
+ ok(count, "Should have a non-empty list of categories available.");
+
+ ok(
+ CATEGORIES.some(e => e.color),
+ "All categories have an associated color."
+ );
+
+ ok(
+ CATEGORIES.every(e => e.label),
+ "All categories have an associated label."
+ );
+
+ ok(
+ CATEGORIES.every(e => e.label === L10N.getStr("category." + e.abbrev)),
+ "All categories have a correctly localized label."
+ );
+});
diff --git a/devtools/client/performance/test/xpcshell/test_tree-model-01.js b/devtools/client/performance/test/xpcshell/test_tree-model-01.js
new file mode 100644
index 0000000000..2bb361496e
--- /dev/null
+++ b/devtools/client/performance/test/xpcshell/test_tree-model-01.js
@@ -0,0 +1,255 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if a call tree model can be correctly computed from a samples array.
+ */
+
+add_task(function test() {
+ const {
+ ThreadNode,
+ } = require("devtools/client/performance/modules/logic/tree-model");
+
+ // Create a root node from a given samples array.
+
+ const threadNode = new ThreadNode(gThread, { startTime: 0, endTime: 20 });
+ const root = getFrameNodePath(threadNode, "(root)");
+
+ // Test the root node.
+
+ equal(
+ threadNode.getInfo().nodeType,
+ "Thread",
+ "The correct node type was retrieved for the root node."
+ );
+
+ equal(
+ threadNode.duration,
+ 20,
+ "The correct duration was calculated for the ThreadNode."
+ );
+ equal(
+ root.getInfo().functionName,
+ "(root)",
+ "The correct function name was retrieved for the root node."
+ );
+ equal(
+ root.getInfo().categoryData.abbrev,
+ "other",
+ "The correct empty category data was retrieved for the root node."
+ );
+
+ equal(
+ root.calls.length,
+ 1,
+ "The correct number of child calls were calculated for the root node."
+ );
+ ok(
+ getFrameNodePath(root, "A"),
+ "The root node's only child call is correct."
+ );
+
+ // Test all the descendant nodes.
+
+ equal(
+ getFrameNodePath(root, "A").calls.length,
+ 2,
+ "The correct number of child calls were calculated for the 'A' node."
+ );
+ ok(getFrameNodePath(root, "A > B"), "The 'A' node has a 'B' child call.");
+ ok(getFrameNodePath(root, "A > E"), "The 'A' node has a 'E' child call.");
+
+ equal(
+ getFrameNodePath(root, "A > B").calls.length,
+ 2,
+ "The correct number of child calls were calculated for the 'A > B' node."
+ );
+ ok(
+ getFrameNodePath(root, "A > B > C"),
+ "The 'A > B' node has a 'C' child call."
+ );
+ ok(
+ getFrameNodePath(root, "A > B > D"),
+ "The 'A > B' node has a 'D' child call."
+ );
+
+ equal(
+ getFrameNodePath(root, "A > E").calls.length,
+ 1,
+ "The correct number of child calls were calculated for the 'A > E' node."
+ );
+ ok(
+ getFrameNodePath(root, "A > E > F"),
+ "The 'A > E' node has a 'F' child call."
+ );
+
+ equal(
+ getFrameNodePath(root, "A > B > C").calls.length,
+ 1,
+ "The correct number of child calls were calculated for the 'A > B > C' node."
+ );
+ ok(
+ getFrameNodePath(root, "A > B > C > D"),
+ "The 'A > B > C' node has a 'D' child call."
+ );
+
+ equal(
+ getFrameNodePath(root, "A > B > C > D").calls.length,
+ 1,
+ "The correct number of child calls were calculated for the 'A > B > C > D' node."
+ );
+ ok(
+ getFrameNodePath(root, "A > B > C > D > E"),
+ "The 'A > B > C > D' node has a 'E' child call."
+ );
+
+ equal(
+ getFrameNodePath(root, "A > B > C > D > E").calls.length,
+ 1,
+ "The correct number of child calls were calculated for the 'A > B > C > D > E' " +
+ "node."
+ );
+ ok(
+ getFrameNodePath(root, "A > B > C > D > E > F"),
+ "The 'A > B > C > D > E' node has a 'F' child call."
+ );
+
+ equal(
+ getFrameNodePath(root, "A > B > C > D > E > F").calls.length,
+ 1,
+ "The correct number of child calls were calculated for the 'A > B > C > D > E > F' " +
+ "node."
+ );
+ ok(
+ getFrameNodePath(root, "A > B > C > D > E > F > G"),
+ "The 'A > B > C > D > E > F' node has a 'G' child call."
+ );
+
+ equal(
+ getFrameNodePath(root, "A > B > C > D > E > F > G").calls.length,
+ 0,
+ "The correct number of child calls were calculated for the " +
+ "'A > B > C > D > E > F > G' node."
+ );
+ equal(
+ getFrameNodePath(root, "A > B > D").calls.length,
+ 0,
+ "The correct number of child calls were calculated for the 'A > B > D' node."
+ );
+ equal(
+ getFrameNodePath(root, "A > E > F").calls.length,
+ 0,
+ "The correct number of child calls were calculated for the 'A > E > F' node."
+ );
+
+ // Check the location, sample times, and samples of the root.
+
+ equal(
+ getFrameNodePath(root, "A").location,
+ "A",
+ "The 'A' node has the correct location."
+ );
+ equal(
+ getFrameNodePath(root, "A").youngestFrameSamples,
+ 0,
+ "The 'A' has correct `youngestFrameSamples`"
+ );
+ equal(
+ getFrameNodePath(root, "A").samples,
+ 4,
+ "The 'A' has correct `samples`"
+ );
+
+ // A frame that is both a leaf and caught in another stack
+ equal(
+ getFrameNodePath(root, "A > B > C").youngestFrameSamples,
+ 1,
+ "The 'A > B > C' has correct `youngestFrameSamples`"
+ );
+ equal(
+ getFrameNodePath(root, "A > B > C").samples,
+ 2,
+ "The 'A > B > C' has correct `samples`"
+ );
+
+ // ...and the rightmost leaf.
+
+ equal(
+ getFrameNodePath(root, "A > E > F").location,
+ "F",
+ "The 'A > E > F' node has the correct location."
+ );
+ equal(
+ getFrameNodePath(root, "A > E > F").samples,
+ 1,
+ "The 'A > E > F' node has the correct number of samples."
+ );
+ equal(
+ getFrameNodePath(root, "A > E > F").youngestFrameSamples,
+ 1,
+ "The 'A > E > F' node has the correct number of youngestFrameSamples."
+ );
+
+ // ...and the leftmost leaf.
+
+ equal(
+ getFrameNodePath(root, "A > B > C > D > E > F > G").location,
+ "G",
+ "The 'A > B > C > D > E > F > G' node has the correct location."
+ );
+ equal(
+ getFrameNodePath(root, "A > B > C > D > E > F > G").samples,
+ 1,
+ "The 'A > B > C > D > E > F > G' node has the correct number of samples."
+ );
+ equal(
+ getFrameNodePath(root, "A > B > C > D > E > F > G").youngestFrameSamples,
+ 1,
+ "The 'A > B > C > D > E > F > G' node has the correct number of " +
+ "youngestFrameSamples."
+ );
+});
+
+var gThread = synthesizeProfileForTest([
+ {
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" },
+ ],
+ },
+ {
+ time: 5 + 6,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" },
+ ],
+ },
+ {
+ time: 5 + 6 + 7,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "E" },
+ { location: "F" },
+ ],
+ },
+ {
+ time: 20,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" },
+ { location: "D" },
+ { location: "E" },
+ { location: "F" },
+ { location: "G" },
+ ],
+ },
+]);
diff --git a/devtools/client/performance/test/xpcshell/test_tree-model-02.js b/devtools/client/performance/test/xpcshell/test_tree-model-02.js
new file mode 100644
index 0000000000..91baca393a
--- /dev/null
+++ b/devtools/client/performance/test/xpcshell/test_tree-model-02.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if a call tree model ignores samples with no timing information.
+ */
+
+add_task(function test() {
+ const {
+ ThreadNode,
+ } = require("devtools/client/performance/modules/logic/tree-model");
+
+ // Create a root node from a given samples array.
+
+ const thread = new ThreadNode(gThread, { startTime: 0, endTime: 10 });
+ const root = getFrameNodePath(thread, "(root)");
+
+ // Test the ThreadNode, only node with a duration.
+ equal(
+ thread.duration,
+ 10,
+ "The correct duration was calculated for the ThreadNode."
+ );
+
+ equal(
+ root.calls.length,
+ 1,
+ "The correct number of child calls were calculated for the root node."
+ );
+ ok(
+ getFrameNodePath(root, "A"),
+ "The root node's only child call is correct."
+ );
+
+ // Test all the descendant nodes.
+
+ equal(
+ getFrameNodePath(root, "A").calls.length,
+ 1,
+ "The correct number of child calls were calculated for the 'A' node."
+ );
+ ok(
+ getFrameNodePath(root, "A > B"),
+ "The 'A' node's only child call is correct."
+ );
+
+ equal(
+ getFrameNodePath(root, "A > B").calls.length,
+ 1,
+ "The correct number of child calls were calculated for the 'A > B' node."
+ );
+ ok(
+ getFrameNodePath(root, "A > B > C"),
+ "The 'A > B' node's only child call is correct."
+ );
+
+ equal(
+ getFrameNodePath(root, "A > B > C").calls.length,
+ 0,
+ "The correct number of child calls were calculated for the 'A > B > C' node."
+ );
+});
+
+var gThread = synthesizeProfileForTest([
+ {
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" },
+ ],
+ },
+ {
+ time: null,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" },
+ ],
+ },
+]);
diff --git a/devtools/client/performance/test/xpcshell/test_tree-model-03.js b/devtools/client/performance/test/xpcshell/test_tree-model-03.js
new file mode 100644
index 0000000000..6fb09e5ff2
--- /dev/null
+++ b/devtools/client/performance/test/xpcshell/test_tree-model-03.js
@@ -0,0 +1,123 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if a call tree model can be correctly computed from a samples array,
+ * while at the same time filtering by duration.
+ */
+
+add_task(function test() {
+ const {
+ ThreadNode,
+ } = require("devtools/client/performance/modules/logic/tree-model");
+
+ // Create a root node from a given samples array, filtering by time.
+ //
+ // Filtering from 5 to 18 includes the 2nd and 3rd samples. The 2nd sample
+ // starts exactly on 5 and ends at 11. The 3rd sample starts at 11 and ends
+ // exactly at 18.
+ const startTime = 5;
+ const endTime = 18;
+ const thread = new ThreadNode(gThread, { startTime, endTime });
+ const root = getFrameNodePath(thread, "(root)");
+
+ // Test the root node.
+
+ equal(
+ thread.duration,
+ endTime - startTime,
+ "The correct duration was calculated for the ThreadNode."
+ );
+
+ equal(
+ root.calls.length,
+ 1,
+ "The correct number of child calls were calculated for the root node."
+ );
+ ok(
+ getFrameNodePath(root, "A"),
+ "The root node's only child call is correct."
+ );
+
+ // Test all the descendant nodes.
+
+ equal(
+ getFrameNodePath(root, "A").calls.length,
+ 2,
+ "The correct number of child calls were calculated for the 'A' node."
+ );
+ ok(getFrameNodePath(root, "A > B"), "The 'A' node has a 'B' child call.");
+ ok(getFrameNodePath(root, "A > E"), "The 'A' node has a 'E' child call.");
+
+ equal(
+ getFrameNodePath(root, "A > B").calls.length,
+ 1,
+ "The correct number of child calls were calculated for the 'A > B' node."
+ );
+ ok(
+ getFrameNodePath(root, "A > B > D"),
+ "The 'A > B' node's only child call is correct."
+ );
+
+ equal(
+ getFrameNodePath(root, "A > E").calls.length,
+ 1,
+ "The correct number of child calls were calculated for the 'A > E' node."
+ );
+ ok(
+ getFrameNodePath(root, "A > E > F"),
+ "The 'A > E' node's only child call is correct."
+ );
+
+ equal(
+ getFrameNodePath(root, "A > B > D").calls.length,
+ 0,
+ "The correct number of child calls were calculated for the 'A > B > D' node."
+ );
+ equal(
+ getFrameNodePath(root, "A > E > F").calls.length,
+ 0,
+ "The correct number of child calls were calculated for the 'A > E > F' node."
+ );
+});
+
+var gThread = synthesizeProfileForTest([
+ {
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" },
+ ],
+ },
+ {
+ time: 5 + 6,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" },
+ ],
+ },
+ {
+ time: 5 + 6 + 7,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "E" },
+ { location: "F" },
+ ],
+ },
+ {
+ time: 5 + 6 + 7 + 8,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" },
+ { location: "D" },
+ ],
+ },
+]);
diff --git a/devtools/client/performance/test/xpcshell/test_tree-model-04.js b/devtools/client/performance/test/xpcshell/test_tree-model-04.js
new file mode 100644
index 0000000000..a89dcac86f
--- /dev/null
+++ b/devtools/client/performance/test/xpcshell/test_tree-model-04.js
@@ -0,0 +1,126 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if a call tree model can be correctly computed from a samples array,
+ * while at the same time filtering by duration and content-only frames.
+ */
+
+add_task(function test() {
+ const {
+ ThreadNode,
+ } = require("devtools/client/performance/modules/logic/tree-model");
+
+ // Create a root node from a given samples array, filtering by time.
+
+ const startTime = 5;
+ const endTime = 18;
+ const thread = new ThreadNode(gThread, {
+ startTime,
+ endTime,
+ contentOnly: true,
+ });
+ const root = getFrameNodePath(thread, "(root)");
+
+ // Test the ThreadNode, only node which should have duration
+ equal(
+ thread.duration,
+ endTime - startTime,
+ "The correct duration was calculated for the root ThreadNode."
+ );
+
+ equal(
+ root.calls.length,
+ 2,
+ "The correct number of child calls were calculated for the root node."
+ );
+ ok(
+ getFrameNodePath(root, "http://D"),
+ "The root has a 'http://D' child call."
+ );
+ ok(
+ getFrameNodePath(root, "http://A"),
+ "The root has a 'http://A' child call."
+ );
+
+ // Test all the descendant nodes.
+
+ equal(
+ getFrameNodePath(root, "http://A").calls.length,
+ 1,
+ "The correct number of child calls were calculated for the 'http://A' node."
+ );
+ ok(
+ getFrameNodePath(root, "http://A > https://E"),
+ "The 'http://A' node's only child call is correct."
+ );
+
+ equal(
+ getFrameNodePath(root, "http://A > https://E").calls.length,
+ 1,
+ "The correct number of child calls were calculated for the 'http://A > http://E' node."
+ );
+ ok(
+ getFrameNodePath(root, "http://A > https://E > file://F"),
+ "The 'http://A > https://E' node's only child call is correct."
+ );
+
+ equal(
+ getFrameNodePath(root, "http://A > https://E > file://F").calls.length,
+ 1,
+ "The correct number of child calls were calculated for the 'http://A > https://E >> file://F' node."
+ );
+ ok(
+ getFrameNodePath(root, "http://A > https://E > file://F > app://H"),
+ "The 'http://A > https://E >> file://F' node's only child call is correct."
+ );
+
+ equal(
+ getFrameNodePath(root, "http://D").calls.length,
+ 0,
+ "The correct number of child calls were calculated for the 'http://D' node."
+ );
+});
+
+var gThread = synthesizeProfileForTest([
+ {
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "http://A" },
+ { location: "http://B" },
+ { location: "http://C" },
+ ],
+ },
+ {
+ time: 5 + 6,
+ frames: [
+ { location: "(root)" },
+ { location: "chrome://A" },
+ { location: "resource://B" },
+ { location: "jar:file://G" },
+ { location: "http://D" },
+ ],
+ },
+ {
+ time: 5 + 6 + 7,
+ frames: [
+ { location: "(root)" },
+ { location: "http://A" },
+ { location: "https://E" },
+ { location: "file://F" },
+ { location: "app://H" },
+ ],
+ },
+ {
+ time: 5 + 6 + 7 + 8,
+ frames: [
+ { location: "(root)" },
+ { location: "http://A" },
+ { location: "http://B" },
+ { location: "http://C" },
+ { location: "http://D" },
+ ],
+ },
+]);
diff --git a/devtools/client/performance/test/xpcshell/test_tree-model-05.js b/devtools/client/performance/test/xpcshell/test_tree-model-05.js
new file mode 100644
index 0000000000..6c5f538c3b
--- /dev/null
+++ b/devtools/client/performance/test/xpcshell/test_tree-model-05.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if an inverted call tree model can be correctly computed from a samples
+ * array.
+ */
+
+var time = 1;
+
+var gThread = synthesizeProfileForTest([
+ {
+ time: time++,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" },
+ ],
+ },
+ {
+ time: time++,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "D" },
+ { location: "C" },
+ ],
+ },
+ {
+ time: time++,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "E" },
+ { location: "C" },
+ ],
+ },
+ {
+ time: time++,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "F" },
+ ],
+ },
+]);
+
+add_task(function test() {
+ const {
+ ThreadNode,
+ } = require("devtools/client/performance/modules/logic/tree-model");
+
+ const root = new ThreadNode(gThread, {
+ invertTree: true,
+ startTime: 0,
+ endTime: 4,
+ });
+
+ equal(
+ root.calls.length,
+ 2,
+ "Should get the 2 youngest frames, not the 1 oldest frame"
+ );
+
+ const C = getFrameNodePath(root, "C");
+ ok(C, "Should have C as a child of the root.");
+
+ equal(C.calls.length, 3, "Should have 3 frames that called C.");
+ ok(getFrameNodePath(C, "B"), "B called C.");
+ ok(getFrameNodePath(C, "D"), "D called C.");
+ ok(getFrameNodePath(C, "E"), "E called C.");
+
+ equal(getFrameNodePath(C, "B").calls.length, 1);
+ ok(getFrameNodePath(C, "B > A"), "A called B called C");
+ equal(getFrameNodePath(C, "D").calls.length, 1);
+ ok(getFrameNodePath(C, "D > A"), "A called D called C");
+ equal(getFrameNodePath(C, "E").calls.length, 1);
+ ok(getFrameNodePath(C, "E > A"), "A called E called C");
+
+ const F = getFrameNodePath(root, "F");
+ ok(F, "Should have F as a child of the root.");
+
+ equal(F.calls.length, 1);
+ ok(getFrameNodePath(F, "B"), "B called F");
+
+ equal(getFrameNodePath(F, "B").calls.length, 1);
+ ok(getFrameNodePath(F, "B > A"), "A called B called F");
+});
diff --git a/devtools/client/performance/test/xpcshell/test_tree-model-06.js b/devtools/client/performance/test/xpcshell/test_tree-model-06.js
new file mode 100644
index 0000000000..9548ff0c8a
--- /dev/null
+++ b/devtools/client/performance/test/xpcshell/test_tree-model-06.js
@@ -0,0 +1,213 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that when constructing FrameNodes, if optimization data is available,
+ * the FrameNodes have the correct optimization data after iterating over samples,
+ * and only youngest frames capture optimization data.
+ */
+
+add_task(function test() {
+ const {
+ ThreadNode,
+ } = require("devtools/client/performance/modules/logic/tree-model");
+ const root = getFrameNodePath(
+ new ThreadNode(gThread, { startTime: 0, endTime: 30 }),
+ "(root)"
+ );
+
+ const A = getFrameNodePath(root, "A");
+ const B = getFrameNodePath(A, "B");
+ const C = getFrameNodePath(B, "C");
+ const Aopts = A.getOptimizations();
+ const Bopts = B.getOptimizations();
+ const Copts = C.getOptimizations();
+
+ ok(
+ !Aopts,
+ "A() was never youngest frame, so should not have optimization data"
+ );
+
+ equal(
+ Bopts.length,
+ 2,
+ "B() only has optimization data when it was a youngest frame"
+ );
+
+ // Check a few properties on the OptimizationSites.
+ const optSitesObserved = new Set();
+ for (const opt of Bopts) {
+ if (opt.data.line === 12) {
+ equal(
+ opt.samples,
+ 2,
+ "Correct amount of samples for B()'s first opt site"
+ );
+ equal(opt.data.attempts.length, 3, "First opt site has 3 attempts");
+ equal(
+ opt.data.attempts[0].strategy,
+ "SomeGetter1",
+ "inflated strategy name"
+ );
+ equal(opt.data.attempts[0].outcome, "Failure1", "inflated outcome name");
+ equal(
+ opt.data.types[0].typeset[0].keyedBy,
+ "constructor",
+ "inflates type info"
+ );
+ optSitesObserved.add("first");
+ } else {
+ equal(
+ opt.samples,
+ 1,
+ "Correct amount of samples for B()'s second opt site"
+ );
+ optSitesObserved.add("second");
+ }
+ }
+
+ ok(optSitesObserved.has("first"), "first opt site for B() was checked");
+ ok(optSitesObserved.has("second"), "second opt site for B() was checked");
+
+ equal(Copts.length, 1, "C() always youngest frame, so has optimization data");
+});
+
+var gUniqueStacks = new RecordingUtils.UniqueStacks();
+
+function uniqStr(s) {
+ return gUniqueStacks.getOrAddStringIndex(s);
+}
+
+var gThread = RecordingUtils.deflateThread(
+ {
+ samples: [
+ {
+ time: 0,
+ frames: [{ location: "(root)" }],
+ },
+ {
+ time: 10,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B_LEAF_1" },
+ ],
+ },
+ {
+ time: 15,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B_NOTLEAF" },
+ { location: "C" },
+ ],
+ },
+ {
+ time: 20,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B_LEAF_2" },
+ ],
+ },
+ {
+ time: 25,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B_LEAF_2" },
+ ],
+ },
+ ],
+ markers: [],
+ },
+ gUniqueStacks
+);
+
+var gRawSite1 = {
+ line: 12,
+ column: 2,
+ types: [
+ {
+ mirType: uniqStr("Object"),
+ site: uniqStr("B (http://foo/bar:10)"),
+ typeset: [
+ {
+ keyedBy: uniqStr("constructor"),
+ name: uniqStr("Foo"),
+ location: uniqStr("B (http://foo/bar:10)"),
+ },
+ {
+ keyedBy: uniqStr("primitive"),
+ location: uniqStr("self-hosted"),
+ },
+ ],
+ },
+ ],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1,
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ [uniqStr("Inlined"), uniqStr("SomeGetter3")],
+ ],
+ },
+};
+
+var gRawSite2 = {
+ line: 22,
+ types: [
+ {
+ mirType: uniqStr("Int32"),
+ site: uniqStr("Receiver"),
+ },
+ ],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1,
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ [uniqStr("Failure3"), uniqStr("SomeGetter3")],
+ ],
+ },
+};
+
+function serialize(x) {
+ return JSON.parse(JSON.stringify(x));
+}
+
+gThread.frameTable.data.forEach(frame => {
+ const LOCATION_SLOT = gThread.frameTable.schema.location;
+ const OPTIMIZATIONS_SLOT = gThread.frameTable.schema.optimizations;
+
+ const l = gThread.stringTable[frame[LOCATION_SLOT]];
+ switch (l) {
+ case "A":
+ frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
+ break;
+ // Rename some of the location sites so we can register different
+ // frames with different opt sites
+ case "B_LEAF_1":
+ frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite2);
+ frame[LOCATION_SLOT] = uniqStr("B");
+ break;
+ case "B_LEAF_2":
+ frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
+ frame[LOCATION_SLOT] = uniqStr("B");
+ break;
+ case "B_NOTLEAF":
+ frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
+ frame[LOCATION_SLOT] = uniqStr("B");
+ break;
+ case "C":
+ frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
+ break;
+ }
+});
diff --git a/devtools/client/performance/test/xpcshell/test_tree-model-07.js b/devtools/client/performance/test/xpcshell/test_tree-model-07.js
new file mode 100644
index 0000000000..899cde1988
--- /dev/null
+++ b/devtools/client/performance/test/xpcshell/test_tree-model-07.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that when displaying only content nodes, platform nodes are generalized.
+ */
+
+var {
+ CATEGORY_INDEX,
+} = require("devtools/client/performance/modules/categories");
+
+add_task(function test() {
+ const {
+ ThreadNode,
+ } = require("devtools/client/performance/modules/logic/tree-model");
+ const url = n => `http://content/${n}`;
+
+ // Create a root node from a given samples array.
+
+ const root = getFrameNodePath(
+ new ThreadNode(gThread, { startTime: 5, endTime: 30, contentOnly: true }),
+ "(root)"
+ );
+
+ /*
+ * should have a tree like:
+ * root
+ * - (JS)
+ * - A
+ * - (GC)
+ * - B
+ * - C
+ * - D
+ * - E
+ * - F
+ * - (JS)
+ */
+
+ // Test the root node.
+
+ equal(root.calls.length, 2, "root has 2 children");
+ ok(getFrameNodePath(root, url("A")), "root has content child");
+ ok(
+ getFrameNodePath(root, `${CATEGORY_INDEX("js")}`),
+ "root has platform generalized child"
+ );
+ equal(
+ getFrameNodePath(root, `${CATEGORY_INDEX("js")}`).calls.length,
+ 0,
+ "platform generalized child is a leaf."
+ );
+
+ ok(
+ getFrameNodePath(root, `${url("A")} > ${CATEGORY_INDEX("gc")}`),
+ "A has platform generalized child of another type"
+ );
+ equal(
+ getFrameNodePath(root, `${url("A")} > ${CATEGORY_INDEX("gc")}`).calls
+ .length,
+ 0,
+ "second generalized type is a leaf."
+ );
+
+ ok(
+ getFrameNodePath(
+ root,
+ `${url("A")} > ${url("E")} > ${url("F")} > ${CATEGORY_INDEX("js")}`
+ ),
+ "a second leaf of the first generalized type exists deep in the tree."
+ );
+ ok(
+ getFrameNodePath(root, `${url("A")} > ${CATEGORY_INDEX("gc")}`),
+ "A has platform generalized child of another type"
+ );
+
+ equal(
+ getFrameNodePath(root, `${CATEGORY_INDEX("js")}`).category,
+ getFrameNodePath(
+ root,
+ `${url("A")} > ${url("E")} > ${url("F")} > ${CATEGORY_INDEX("js")}`
+ ).category,
+ "generalized frames of same type are duplicated in top-down view"
+ );
+});
+
+var gThread = synthesizeProfileForTest([
+ {
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "http://content/A" },
+ { location: "http://content/B" },
+ { location: "http://content/C" },
+ ],
+ },
+ {
+ time: 5 + 6,
+ frames: [
+ { location: "(root)" },
+ { location: "http://content/A" },
+ { location: "http://content/B" },
+ { location: "contentY", category: CATEGORY_INDEX("layout") },
+ { location: "http://content/D" },
+ ],
+ },
+ {
+ time: 5 + 6 + 7,
+ frames: [
+ { location: "(root)" },
+ { location: "http://content/A" },
+ { location: "contentY", category: CATEGORY_INDEX("layout") },
+ { location: "http://content/E" },
+ { location: "http://content/F" },
+ { location: "contentY", category: CATEGORY_INDEX("js") },
+ ],
+ },
+ {
+ time: 5 + 20,
+ frames: [
+ { location: "(root)" },
+ { location: "contentX", category: CATEGORY_INDEX("js") },
+ ],
+ },
+ {
+ time: 5 + 25,
+ frames: [
+ { location: "(root)" },
+ { location: "http://content/A" },
+ { location: "contentZ", category: CATEGORY_INDEX("gc") },
+ ],
+ },
+]);
diff --git a/devtools/client/performance/test/xpcshell/test_tree-model-08.js b/devtools/client/performance/test/xpcshell/test_tree-model-08.js
new file mode 100644
index 0000000000..075e9c265e
--- /dev/null
+++ b/devtools/client/performance/test/xpcshell/test_tree-model-08.js
@@ -0,0 +1,250 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Verifies if FrameNodes retain and parse their data appropriately.
+ */
+
+add_task(function test() {
+ const FrameUtils = require("devtools/client/performance/modules/logic/frame-utils");
+ const {
+ FrameNode,
+ } = require("devtools/client/performance/modules/logic/tree-model");
+ const {
+ CATEGORY_INDEX,
+ } = require("devtools/client/performance/modules/categories");
+ const compute = frame => {
+ FrameUtils.computeIsContentAndCategory(frame);
+ return frame;
+ };
+
+ const frames = [
+ new FrameNode(
+ "hello/<.world (http://foo/bar.js:123:987)",
+ compute({
+ location: "hello/<.world (http://foo/bar.js:123:987)",
+ line: 456,
+ }),
+ false
+ ),
+ new FrameNode(
+ "hello/<.world (http://foo/bar.js#baz:123:987)",
+ compute({
+ location: "hello/<.world (http://foo/bar.js#baz:123:987)",
+ line: 456,
+ }),
+ false
+ ),
+ new FrameNode(
+ "hello/<.world (http://foo/#bar:123:987)",
+ compute({
+ location: "hello/<.world (http://foo/#bar:123:987)",
+ line: 456,
+ }),
+ false
+ ),
+ new FrameNode(
+ "hello/<.world (http://foo/:123:987)",
+ compute({
+ location: "hello/<.world (http://foo/:123:987)",
+ line: 456,
+ }),
+ false
+ ),
+ new FrameNode(
+ "hello/<.world (resource://foo.js -> http://bar/baz.js:123:987)",
+ compute({
+ location:
+ "hello/<.world (resource://foo.js -> http://bar/baz.js:123:987)",
+ line: 456,
+ }),
+ false
+ ),
+ new FrameNode(
+ "Foo::Bar::Baz",
+ compute({
+ location: "Foo::Bar::Baz",
+ line: 456,
+ category: CATEGORY_INDEX("other"),
+ }),
+ false
+ ),
+ new FrameNode(
+ "EnterJIT",
+ compute({
+ location: "EnterJIT",
+ }),
+ false
+ ),
+ new FrameNode(
+ "chrome://browser/content/content.js",
+ compute({
+ location: "chrome://browser/content/content.js",
+ line: 456,
+ column: 123,
+ }),
+ false
+ ),
+ new FrameNode(
+ "hello/<.world (resource://gre/foo.js:123:434)",
+ compute({
+ location: "hello/<.world (resource://gre/foo.js:123:434)",
+ line: 456,
+ }),
+ false
+ ),
+ new FrameNode(
+ "main (http://localhost:8888/file.js:123:987)",
+ compute({
+ location: "main (http://localhost:8888/file.js:123:987)",
+ line: 123,
+ }),
+ false
+ ),
+ new FrameNode(
+ "main (resource://devtools/timeline.js:123)",
+ compute({
+ location: "main (resource://devtools/timeline.js:123)",
+ }),
+ false
+ ),
+ ];
+
+ const fields = [
+ "nodeType",
+ "functionName",
+ "fileName",
+ "host",
+ "url",
+ "line",
+ "column",
+ "categoryData.abbrev",
+ "isContent",
+ "port",
+ ];
+ const expected = [
+ // nodeType, functionName, fileName, host, url, line, column, categoryData.abbrev,
+ // isContent, port
+ [
+ "Frame",
+ "hello/<.world",
+ "bar.js",
+ "foo",
+ "http://foo/bar.js",
+ 123,
+ 987,
+ void 0,
+ true,
+ ],
+ [
+ "Frame",
+ "hello/<.world",
+ "bar.js",
+ "foo",
+ "http://foo/bar.js#baz",
+ 123,
+ 987,
+ void 0,
+ true,
+ ],
+ [
+ "Frame",
+ "hello/<.world",
+ "/",
+ "foo",
+ "http://foo/#bar",
+ 123,
+ 987,
+ void 0,
+ true,
+ ],
+ [
+ "Frame",
+ "hello/<.world",
+ "/",
+ "foo",
+ "http://foo/",
+ 123,
+ 987,
+ void 0,
+ true,
+ ],
+ [
+ "Frame",
+ "hello/<.world",
+ "baz.js",
+ "bar",
+ "http://bar/baz.js",
+ 123,
+ 987,
+ "other",
+ false,
+ ],
+ ["Frame", "Foo::Bar::Baz", null, null, null, 456, void 0, "other", false],
+ ["Frame", "EnterJIT", null, null, null, null, null, "js", false],
+ [
+ "Frame",
+ "chrome://browser/content/content.js",
+ null,
+ null,
+ null,
+ 456,
+ null,
+ "other",
+ false,
+ ],
+ [
+ "Frame",
+ "hello/<.world",
+ "foo.js",
+ null,
+ "resource://gre/foo.js",
+ 123,
+ 434,
+ "other",
+ false,
+ ],
+ [
+ "Frame",
+ "main",
+ "file.js",
+ "localhost:8888",
+ "http://localhost:8888/file.js",
+ 123,
+ 987,
+ null,
+ true,
+ 8888,
+ ],
+ [
+ "Frame",
+ "main",
+ "timeline.js",
+ null,
+ "resource://devtools/timeline.js",
+ 123,
+ null,
+ "tools",
+ false,
+ ],
+ ];
+
+ for (let i = 0; i < frames.length; i++) {
+ const info = frames[i].getInfo();
+ const expect = expected[i];
+
+ for (let j = 0; j < fields.length; j++) {
+ const field = fields[j];
+ const value =
+ field === "categoryData.abbrev"
+ ? info.categoryData.abbrev
+ : info[field];
+ equal(
+ value,
+ expect[j],
+ `${field} for frame #${i} is correct: ${expect[j]}`
+ );
+ }
+ }
+});
diff --git a/devtools/client/performance/test/xpcshell/test_tree-model-09.js b/devtools/client/performance/test/xpcshell/test_tree-model-09.js
new file mode 100644
index 0000000000..393fbce50c
--- /dev/null
+++ b/devtools/client/performance/test/xpcshell/test_tree-model-09.js
@@ -0,0 +1,116 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that when displaying only content nodes, platform nodes are generalized.
+ */
+
+var {
+ CATEGORY_INDEX,
+} = require("devtools/client/performance/modules/categories");
+
+add_task(function test() {
+ const {
+ ThreadNode,
+ } = require("devtools/client/performance/modules/logic/tree-model");
+ const url = n => `http://content/${n}`;
+
+ // Create a root node from a given samples array.
+
+ const root = getFrameNodePath(
+ new ThreadNode(gThread, { startTime: 5, endTime: 25, contentOnly: true }),
+ "(root)"
+ );
+
+ /*
+ * should have a tree like:
+ * root
+ * - (Tools)
+ * - A
+ * - B
+ * - C
+ * - D
+ * - E
+ * - F
+ * - (Tools)
+ */
+
+ // Test the root node.
+
+ equal(root.calls.length, 2, "root has 2 children");
+ ok(getFrameNodePath(root, url("A")), "root has content child");
+ ok(
+ getFrameNodePath(root, `${CATEGORY_INDEX("tools")}`),
+ "root has platform generalized child from Chrome JS"
+ );
+ equal(
+ getFrameNodePath(root, `${CATEGORY_INDEX("tools")}`).calls.length,
+ 0,
+ "platform generalized child is a leaf."
+ );
+
+ ok(
+ getFrameNodePath(
+ root,
+ `${url("A")} > ${url("E")} > ${url("F")} > ${CATEGORY_INDEX("tools")}`
+ ),
+ "a second leaf of the generalized Chrome JS exists."
+ );
+
+ equal(
+ getFrameNodePath(root, `${CATEGORY_INDEX("tools")}`).category,
+ getFrameNodePath(
+ root,
+ `${url("A")} > ${url("E")} > ${url("F")} > ${CATEGORY_INDEX("tools")}`
+ ).category,
+ "generalized frames of same type are duplicated in top-down view"
+ );
+});
+
+var gThread = synthesizeProfileForTest([
+ {
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "http://content/A" },
+ { location: "http://content/B" },
+ { location: "http://content/C" },
+ ],
+ },
+ {
+ time: 5 + 6,
+ frames: [
+ { location: "(root)" },
+ { location: "http://content/A" },
+ { location: "http://content/B" },
+ {
+ location:
+ "fn (resource://loader.js -> resource://devtools/timeline.js)",
+ },
+ { location: "http://content/D" },
+ ],
+ },
+ {
+ time: 5 + 6 + 7,
+ frames: [
+ { location: "(root)" },
+ { location: "http://content/A" },
+ { location: "http://content/E" },
+ { location: "http://content/F" },
+ {
+ location: "fn (resource://loader.js -> resource://devtools/promise.js)",
+ },
+ ],
+ },
+ {
+ time: 5 + 20,
+ frames: [
+ { location: "(root)" },
+ {
+ location:
+ "somefn (resource://loader.js -> resource://devtools/framerate.js)",
+ },
+ ],
+ },
+]);
diff --git a/devtools/client/performance/test/xpcshell/test_tree-model-10.js b/devtools/client/performance/test/xpcshell/test_tree-model-10.js
new file mode 100644
index 0000000000..b928c99e4b
--- /dev/null
+++ b/devtools/client/performance/test/xpcshell/test_tree-model-10.js
@@ -0,0 +1,157 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the tree model calculates correct costs/percentages for
+ * frame nodes. The model-only version of browser_profiler-tree-view-10.js
+ */
+
+add_task(function() {
+ const {
+ ThreadNode,
+ } = require("devtools/client/performance/modules/logic/tree-model");
+ const thread = new ThreadNode(gThread, {
+ invertTree: true,
+ startTime: 0,
+ endTime: 50,
+ });
+
+ /**
+ * Samples
+ *
+ * A->C
+ * A->B
+ * A->B->C x4
+ * A->B->D x4
+ *
+ * Expected Tree
+ * +--total--+--self--+--tree-------------+
+ * | 50% | 50% | C
+ * | 40% | 0 | -> B
+ * | 30% | 0 | -> A
+ * | 10% | 0 | -> A
+ *
+ * | 40% | 40% | D
+ * | 40% | 0 | -> B
+ * | 40% | 0 | -> A
+ *
+ * | 10% | 10% | B
+ * | 10% | 0 | -> A
+ */
+
+ [
+ // total, self, name
+ [
+ 50,
+ 50,
+ "C",
+ [
+ [40, 0, "B", [[30, 0, "A"]]],
+ [10, 0, "A"],
+ ],
+ ],
+ [40, 40, "D", [[40, 0, "B", [[40, 0, "A"]]]]],
+ [10, 10, "B", [[10, 0, "A"]]],
+ ].forEach(compareFrameInfo(thread));
+});
+
+function compareFrameInfo(root, parent) {
+ parent = parent || root;
+ return function(def) {
+ const [total, self, name, children] = def;
+ const node = getFrameNodePath(parent, name);
+ const data = node.getInfo({ root });
+ equal(
+ total,
+ data.totalPercentage,
+ `${name} has correct total percentage: ${data.totalPercentage}`
+ );
+ equal(
+ self,
+ data.selfPercentage,
+ `${name} has correct self percentage: ${data.selfPercentage}`
+ );
+ if (children) {
+ children.forEach(compareFrameInfo(root, node));
+ }
+ };
+}
+
+var gThread = synthesizeProfileForTest([
+ {
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" },
+ ],
+ },
+ {
+ time: 10,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" },
+ ],
+ },
+ {
+ time: 15,
+ frames: [{ location: "(root)" }, { location: "A" }, { location: "C" }],
+ },
+ {
+ time: 20,
+ frames: [{ location: "(root)" }, { location: "A" }, { location: "B" }],
+ },
+ {
+ time: 25,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" },
+ ],
+ },
+ {
+ time: 30,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" },
+ ],
+ },
+ {
+ time: 35,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" },
+ ],
+ },
+ {
+ time: 40,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" },
+ ],
+ },
+ {
+ time: 45,
+ frames: [{ location: "(root)" }, { location: "B" }, { location: "C" }],
+ },
+ {
+ time: 50,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" },
+ ],
+ },
+]);
diff --git a/devtools/client/performance/test/xpcshell/test_tree-model-11.js b/devtools/client/performance/test/xpcshell/test_tree-model-11.js
new file mode 100644
index 0000000000..859637fbe5
--- /dev/null
+++ b/devtools/client/performance/test/xpcshell/test_tree-model-11.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the costs for recursive frames does not overcount the collapsed
+ * samples.
+ */
+
+add_task(function() {
+ const {
+ ThreadNode,
+ } = require("devtools/client/performance/modules/logic/tree-model");
+ const thread = new ThreadNode(gThread, {
+ startTime: 0,
+ endTime: 50,
+ flattenRecursion: true,
+ });
+
+ /**
+ * Samples
+ *
+ * A->B->C
+ * A->B->B->B->C
+ * A->B
+ * A->B->B->B
+ */
+
+ [
+ // total, self, name
+ [100, 0, "(root)", [[100, 0, "A", [[100, 50, "B", [[50, 50, "C"]]]]]]],
+ ].forEach(compareFrameInfo(thread));
+});
+
+function compareFrameInfo(root, parent) {
+ parent = parent || root;
+ return function(def) {
+ const [total, self, name, children] = def;
+ const node = getFrameNodePath(parent, name);
+ const data = node.getInfo({ root });
+ equal(
+ total,
+ data.totalPercentage,
+ `${name} has correct total percentage: ${data.totalPercentage}`
+ );
+ equal(
+ self,
+ data.selfPercentage,
+ `${name} has correct self percentage: ${data.selfPercentage}`
+ );
+ if (children) {
+ children.forEach(compareFrameInfo(root, node));
+ }
+ };
+}
+
+var gThread = synthesizeProfileForTest([
+ {
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "B" },
+ { location: "B" },
+ { location: "C" },
+ ],
+ },
+ {
+ time: 10,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" },
+ ],
+ },
+ {
+ time: 15,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "B" },
+ { location: "B" },
+ ],
+ },
+ {
+ time: 20,
+ frames: [{ location: "(root)" }, { location: "A" }, { location: "B" }],
+ },
+]);
diff --git a/devtools/client/performance/test/xpcshell/test_tree-model-12.js b/devtools/client/performance/test/xpcshell/test_tree-model-12.js
new file mode 100644
index 0000000000..9576970784
--- /dev/null
+++ b/devtools/client/performance/test/xpcshell/test_tree-model-12.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that uninverting the call tree works correctly when there are stacks
+// in the profile that prefixes of other stacks.
+
+add_task(function() {
+ const {
+ ThreadNode,
+ } = require("devtools/client/performance/modules/logic/tree-model");
+ const thread = new ThreadNode(gThread, { startTime: 0, endTime: 50 });
+ const root = getFrameNodePath(thread, "(root)");
+
+ /**
+ * Samples
+ *
+ * A->B
+ * C->B
+ * B
+ * A
+ * Z->Y->X
+ * W->Y->X
+ * Y->X
+ */
+
+ equal(
+ getFrameNodePath(root, "A > B").youngestFrameSamples,
+ 1,
+ "A > B has the correct self count"
+ );
+ equal(
+ getFrameNodePath(root, "C > B").youngestFrameSamples,
+ 1,
+ "C > B has the correct self count"
+ );
+ equal(
+ getFrameNodePath(root, "B").youngestFrameSamples,
+ 1,
+ "B has the correct self count"
+ );
+ equal(
+ getFrameNodePath(root, "A").youngestFrameSamples,
+ 1,
+ "A has the correct self count"
+ );
+ equal(
+ getFrameNodePath(root, "Z > Y > X").youngestFrameSamples,
+ 1,
+ "Z > Y > X has the correct self count"
+ );
+ equal(
+ getFrameNodePath(root, "W > Y > X").youngestFrameSamples,
+ 1,
+ "W > Y > X has the correct self count"
+ );
+ equal(
+ getFrameNodePath(root, "Y > X").youngestFrameSamples,
+ 1,
+ "Y > X has the correct self count"
+ );
+});
+
+var gThread = synthesizeProfileForTest([
+ {
+ time: 5,
+ frames: [{ location: "(root)" }, { location: "A" }, { location: "B" }],
+ },
+ {
+ time: 10,
+ frames: [{ location: "(root)" }, { location: "C" }, { location: "B" }],
+ },
+ {
+ time: 15,
+ frames: [{ location: "(root)" }, { location: "B" }],
+ },
+ {
+ time: 20,
+ frames: [{ location: "(root)" }, { location: "A" }],
+ },
+ {
+ time: 21,
+ frames: [
+ { location: "(root)" },
+ { location: "Z" },
+ { location: "Y" },
+ { location: "X" },
+ ],
+ },
+ {
+ time: 22,
+ frames: [
+ { location: "(root)" },
+ { location: "W" },
+ { location: "Y" },
+ { location: "X" },
+ ],
+ },
+ {
+ time: 23,
+ frames: [{ location: "(root)" }, { location: "Y" }, { location: "X" }],
+ },
+]);
diff --git a/devtools/client/performance/test/xpcshell/test_tree-model-13.js b/devtools/client/performance/test/xpcshell/test_tree-model-13.js
new file mode 100644
index 0000000000..0580d49956
--- /dev/null
+++ b/devtools/client/performance/test/xpcshell/test_tree-model-13.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Like test_tree-model-12, but inverted.
+
+add_task(function() {
+ const {
+ ThreadNode,
+ } = require("devtools/client/performance/modules/logic/tree-model");
+ const root = new ThreadNode(gThread, {
+ invertTree: true,
+ startTime: 0,
+ endTime: 50,
+ });
+
+ /**
+ * Samples
+ *
+ * A->B
+ * C->B
+ * B
+ * A
+ * Z->Y->X
+ * W->Y->X
+ * Y->X
+ */
+
+ equal(
+ getFrameNodePath(root, "B").youngestFrameSamples,
+ 3,
+ "B has the correct self count"
+ );
+ equal(
+ getFrameNodePath(root, "A").youngestFrameSamples,
+ 1,
+ "A has the correct self count"
+ );
+ equal(
+ getFrameNodePath(root, "X").youngestFrameSamples,
+ 3,
+ "X has the correct self count"
+ );
+ equal(
+ getFrameNodePath(root, "X > Y").samples,
+ 3,
+ "X > Y has the correct total count"
+ );
+});
+
+var gThread = synthesizeProfileForTest([
+ {
+ time: 5,
+ frames: [{ location: "(root)" }, { location: "A" }, { location: "B" }],
+ },
+ {
+ time: 10,
+ frames: [{ location: "(root)" }, { location: "C" }, { location: "B" }],
+ },
+ {
+ time: 15,
+ frames: [{ location: "(root)" }, { location: "B" }],
+ },
+ {
+ time: 20,
+ frames: [{ location: "(root)" }, { location: "A" }],
+ },
+ {
+ time: 21,
+ frames: [
+ { location: "(root)" },
+ { location: "Z" },
+ { location: "Y" },
+ { location: "X" },
+ ],
+ },
+ {
+ time: 22,
+ frames: [
+ { location: "(root)" },
+ { location: "W" },
+ { location: "Y" },
+ { location: "X" },
+ ],
+ },
+ {
+ time: 23,
+ frames: [{ location: "(root)" }, { location: "Y" }, { location: "X" }],
+ },
+]);
diff --git a/devtools/client/performance/test/xpcshell/test_tree-model-allocations-01.js b/devtools/client/performance/test/xpcshell/test_tree-model-allocations-01.js
new file mode 100644
index 0000000000..cd4850dc97
--- /dev/null
+++ b/devtools/client/performance/test/xpcshell/test_tree-model-allocations-01.js
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/**
+ * Tests that the tree model calculates correct costs/percentages for
+ * allocation frame nodes.
+ */
+
+add_task(function() {
+ const {
+ ThreadNode,
+ } = require("devtools/client/performance/modules/logic/tree-model");
+ const {
+ getProfileThreadFromAllocations,
+ } = require("devtools/shared/performance/recording-utils");
+ const allocationData = getProfileThreadFromAllocations(TEST_DATA);
+ const thread = new ThreadNode(allocationData, {
+ startTime: 0,
+ endTime: 1000,
+ });
+
+ /* eslint-disable max-len */
+ /**
+ * Values are in order according to:
+ * +-------------+------------+-------------+-------------+------------------------------+
+ * | Self Bytes | Self Count | Total Bytes | Total Count | Function |
+ * +-------------+------------+-------------+-------------+------------------------------+
+ * | 1790272 41% | 8307 17% | 1790372 42% | 8317 18% | V someFunc @ a.j:345:6 |
+ * | 100 1% | 10 1% | 100 1% | 10 1% | > callerFunc @ b.j:765:34 |
+ * +-------------+------------+-------------+-------------+------------------------------+
+ */
+ /* eslint-enable max-len */
+ [
+ [
+ 100,
+ 10,
+ 1,
+ 33,
+ 1000,
+ 100,
+ 3,
+ 100,
+ "x (A:1:2)",
+ [
+ [
+ 200,
+ 20,
+ 1,
+ 33,
+ 900,
+ 90,
+ 2,
+ 66,
+ "y (B:3:4)",
+ [[700, 70, 1, 33, 700, 70, 1, 33, "z (C:5:6)"]],
+ ],
+ ],
+ ],
+ ].forEach(compareFrameInfo(thread));
+});
+
+function compareFrameInfo(root, parent) {
+ parent = parent || root;
+ const fields = [
+ "selfSize",
+ "selfSizePercentage",
+ "selfCount",
+ "selfCountPercentage",
+ "totalSize",
+ "totalSizePercentage",
+ "totalCount",
+ "totalCountPercentage",
+ ];
+ return function(def) {
+ let children;
+ if (Array.isArray(def[def.length - 1])) {
+ children = def.pop();
+ }
+ const name = def.pop();
+ const expected = def;
+
+ const node = getFrameNodePath(parent, name);
+ const data = node.getInfo({ root, allocations: true });
+
+ fields.forEach((field, i) => {
+ let actual = data[field];
+ if (/percentage/i.test(field)) {
+ actual = Number.parseInt(actual, 10);
+ }
+ equal(
+ actual,
+ expected[i],
+ `${name} has correct ${field}: ${expected[i]}`
+ );
+ });
+
+ if (children) {
+ children.forEach(compareFrameInfo(root, node));
+ }
+ };
+}
+
+var TEST_DATA = {
+ sites: [1, 2, 3],
+ timestamps: [150, 200, 250],
+ sizes: [100, 200, 700],
+ frames: [
+ null,
+ {
+ source: "A",
+ line: 1,
+ column: 2,
+ functionDisplayName: "x",
+ parent: 0,
+ },
+ {
+ source: "B",
+ line: 3,
+ column: 4,
+ functionDisplayName: "y",
+ parent: 1,
+ },
+ {
+ source: "C",
+ line: 5,
+ column: 6,
+ functionDisplayName: "z",
+ parent: 2,
+ },
+ ],
+};
diff --git a/devtools/client/performance/test/xpcshell/test_tree-model-allocations-02.js b/devtools/client/performance/test/xpcshell/test_tree-model-allocations-02.js
new file mode 100644
index 0000000000..ec7f71eb6b
--- /dev/null
+++ b/devtools/client/performance/test/xpcshell/test_tree-model-allocations-02.js
@@ -0,0 +1,151 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the tree model calculates correct costs/percentages for
+ * allocation frame nodes. Inverted version of test_tree-model-allocations-01.js
+ */
+
+add_task(function() {
+ const {
+ ThreadNode,
+ } = require("devtools/client/performance/modules/logic/tree-model");
+ const {
+ getProfileThreadFromAllocations,
+ } = require("devtools/shared/performance/recording-utils");
+ const allocationData = getProfileThreadFromAllocations(TEST_DATA);
+ const thread = new ThreadNode(allocationData, {
+ invertTree: true,
+ startTime: 0,
+ endTime: 1000,
+ });
+
+ /* eslint-disable max-len */
+ /**
+ * Values are in order according to:
+ * +-------------+------------+-------------+-------------+------------------------------+
+ * | Self Bytes | Self Count | Total Bytes | Total Count | Function |
+ * +-------------+------------+-------------+-------------+------------------------------+
+ * | 1790272 41% | 8307 17% | 1790372 42% | 8317 18% | V someFunc @ a.j:345:6 |
+ * | 100 1% | 10 1% | 100 1% | 10 1% | > callerFunc @ b.j:765:34 |
+ * +-------------+------------+-------------+-------------+------------------------------+
+ */
+ /* eslint-enable max-len */
+ [
+ [
+ 700,
+ 70,
+ 1,
+ 33,
+ 700,
+ 70,
+ 1,
+ 33,
+ "z (C:5:6)",
+ [
+ [
+ 0,
+ 0,
+ 0,
+ 0,
+ 700,
+ 70,
+ 1,
+ 33,
+ "y (B:3:4)",
+ [[0, 0, 0, 0, 700, 70, 1, 33, "x (A:1:2)"]],
+ ],
+ ],
+ ],
+ [
+ 200,
+ 20,
+ 1,
+ 33,
+ 200,
+ 20,
+ 1,
+ 33,
+ "y (B:3:4)",
+ [[0, 0, 0, 0, 200, 20, 1, 33, "x (A:1:2)"]],
+ ],
+ [100, 10, 1, 33, 100, 10, 1, 33, "x (A:1:2)"],
+ ].forEach(compareFrameInfo(thread));
+});
+
+function compareFrameInfo(root, parent) {
+ parent = parent || root;
+ const fields = [
+ "selfSize",
+ "selfSizePercentage",
+ "selfCount",
+ "selfCountPercentage",
+ "totalSize",
+ "totalSizePercentage",
+ "totalCount",
+ "totalCountPercentage",
+ ];
+
+ return function(def) {
+ let children;
+
+ if (Array.isArray(def[def.length - 1])) {
+ children = def.pop();
+ }
+
+ const name = def.pop();
+ const expected = def;
+
+ const node = getFrameNodePath(parent, name);
+ const data = node.getInfo({ root, allocations: true });
+
+ fields.forEach((field, i) => {
+ let actual = data[field];
+ if (/percentage/i.test(field)) {
+ actual = Number.parseInt(actual, 10);
+ }
+ equal(
+ actual,
+ expected[i],
+ `${name} has correct ${field}: ${expected[i]}`
+ );
+ });
+
+ if (children) {
+ children.forEach(compareFrameInfo(root, node));
+ }
+ };
+}
+
+var TEST_DATA = {
+ sites: [0, 1, 2, 3],
+ timestamps: [0, 150, 200, 250],
+ sizes: [0, 100, 200, 700],
+ frames: [
+ {
+ source: "(root)",
+ },
+ {
+ source: "A",
+ line: 1,
+ column: 2,
+ functionDisplayName: "x",
+ parent: 0,
+ },
+ {
+ source: "B",
+ line: 3,
+ column: 4,
+ functionDisplayName: "y",
+ parent: 1,
+ },
+ {
+ source: "C",
+ line: 5,
+ column: 6,
+ functionDisplayName: "z",
+ parent: 2,
+ },
+ ],
+};
diff --git a/devtools/client/performance/test/xpcshell/test_waterfall-utils-collapse-01.js b/devtools/client/performance/test/xpcshell/test_waterfall-utils-collapse-01.js
new file mode 100644
index 0000000000..9dc5267cbc
--- /dev/null
+++ b/devtools/client/performance/test/xpcshell/test_waterfall-utils-collapse-01.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the waterfall collapsing logic works properly.
+ */
+
+add_task(function test() {
+ const WaterfallUtils = require("devtools/client/performance/modules/logic/waterfall-utils");
+
+ const rootMarkerNode = WaterfallUtils.createParentNode({ name: "(root)" });
+
+ WaterfallUtils.collapseMarkersIntoNode({
+ rootNode: rootMarkerNode,
+ markersList: gTestMarkers,
+ });
+
+ function compare(marker, expected) {
+ for (const prop in expected) {
+ if (prop === "submarkers") {
+ for (let i = 0; i < expected.submarkers.length; i++) {
+ compare(marker.submarkers[i], expected.submarkers[i]);
+ }
+ } else if (prop !== "uid") {
+ equal(marker[prop], expected[prop], `${expected.name} matches ${prop}`);
+ }
+ }
+ }
+
+ compare(rootMarkerNode, gExpectedOutput);
+});
+
+const gTestMarkers = [
+ { start: 1, end: 18, name: "DOMEvent" },
+ // Test that JS markers can fold in DOM events and have marker children
+ { start: 2, end: 16, name: "Javascript" },
+ // Test all these markers can be children
+ { start: 3, end: 4, name: "Paint" },
+ { start: 5, end: 6, name: "Reflow" },
+ { start: 7, end: 8, name: "Styles" },
+ { start: 9, end: 9, name: "TimeStamp" },
+ { start: 10, end: 11, name: "Parse HTML" },
+ { start: 12, end: 13, name: "Parse XML" },
+ { start: 14, end: 15, name: "GarbageCollection" },
+ // Test that JS markers can be parents without being a child of DOM events
+ { start: 25, end: 30, name: "Javascript" },
+ { start: 26, end: 27, name: "Paint" },
+];
+
+const gExpectedOutput = {
+ name: "(root)",
+ submarkers: [
+ {
+ start: 1,
+ end: 18,
+ name: "DOMEvent",
+ submarkers: [
+ {
+ start: 2,
+ end: 16,
+ name: "Javascript",
+ submarkers: [
+ { start: 3, end: 4, name: "Paint" },
+ { start: 5, end: 6, name: "Reflow" },
+ { start: 7, end: 8, name: "Styles" },
+ { start: 9, end: 9, name: "TimeStamp" },
+ { start: 10, end: 11, name: "Parse HTML" },
+ { start: 12, end: 13, name: "Parse XML" },
+ { start: 14, end: 15, name: "GarbageCollection" },
+ ],
+ },
+ ],
+ },
+ {
+ start: 25,
+ end: 30,
+ name: "Javascript",
+ submarkers: [{ start: 26, end: 27, name: "Paint" }],
+ },
+ ],
+};
diff --git a/devtools/client/performance/test/xpcshell/test_waterfall-utils-collapse-02.js b/devtools/client/performance/test/xpcshell/test_waterfall-utils-collapse-02.js
new file mode 100644
index 0000000000..eb550d7867
--- /dev/null
+++ b/devtools/client/performance/test/xpcshell/test_waterfall-utils-collapse-02.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the waterfall collapsing logic works properly for console.time/console.timeEnd
+ * markers, as they should ignore any sort of collapsing.
+ */
+
+add_task(function test() {
+ const WaterfallUtils = require("devtools/client/performance/modules/logic/waterfall-utils");
+
+ const rootMarkerNode = WaterfallUtils.createParentNode({ name: "(root)" });
+
+ WaterfallUtils.collapseMarkersIntoNode({
+ rootNode: rootMarkerNode,
+ markersList: gTestMarkers,
+ });
+
+ function compare(marker, expected) {
+ for (const prop in expected) {
+ if (prop === "submarkers") {
+ for (let i = 0; i < expected.submarkers.length; i++) {
+ compare(marker.submarkers[i], expected.submarkers[i]);
+ }
+ } else if (prop !== "uid") {
+ equal(marker[prop], expected[prop], `${expected.name} matches ${prop}`);
+ }
+ }
+ }
+
+ compare(rootMarkerNode, gExpectedOutput);
+});
+
+const gTestMarkers = [
+ { start: 2, end: 9, name: "Javascript" },
+ { start: 3, end: 4, name: "Paint" },
+ // Time range starting in nest, ending outside
+ { start: 5, end: 12, name: "ConsoleTime", causeName: "1" },
+
+ // Time range starting outside of nest, ending inside
+ { start: 15, end: 21, name: "ConsoleTime", causeName: "2" },
+ { start: 18, end: 22, name: "Javascript" },
+ { start: 19, end: 20, name: "Paint" },
+
+ // Time range completely eclipsing nest
+ { start: 30, end: 40, name: "ConsoleTime", causeName: "3" },
+ { start: 34, end: 39, name: "Javascript" },
+ { start: 35, end: 36, name: "Paint" },
+
+ // Time range completely eclipsed by nest
+ { start: 50, end: 60, name: "Javascript" },
+ { start: 54, end: 59, name: "ConsoleTime", causeName: "4" },
+ { start: 56, end: 57, name: "Paint" },
+];
+
+const gExpectedOutput = {
+ name: "(root)",
+ submarkers: [
+ {
+ start: 2,
+ end: 9,
+ name: "Javascript",
+ submarkers: [{ start: 3, end: 4, name: "Paint" }],
+ },
+ { start: 5, end: 12, name: "ConsoleTime", causeName: "1" },
+
+ { start: 15, end: 21, name: "ConsoleTime", causeName: "2" },
+ {
+ start: 18,
+ end: 22,
+ name: "Javascript",
+ submarkers: [{ start: 19, end: 20, name: "Paint" }],
+ },
+
+ { start: 30, end: 40, name: "ConsoleTime", causeName: "3" },
+ {
+ start: 34,
+ end: 39,
+ name: "Javascript",
+ submarkers: [{ start: 35, end: 36, name: "Paint" }],
+ },
+
+ {
+ start: 50,
+ end: 60,
+ name: "Javascript",
+ submarkers: [{ start: 56, end: 57, name: "Paint" }],
+ },
+ { start: 54, end: 59, name: "ConsoleTime", causeName: "4" },
+ ],
+};
diff --git a/devtools/client/performance/test/xpcshell/test_waterfall-utils-collapse-03.js b/devtools/client/performance/test/xpcshell/test_waterfall-utils-collapse-03.js
new file mode 100644
index 0000000000..b9124c6dfd
--- /dev/null
+++ b/devtools/client/performance/test/xpcshell/test_waterfall-utils-collapse-03.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the waterfall collapsing works when atleast two
+ * collapsible markers downward, and the following marker is outside of both ranges.
+ */
+
+add_task(function test() {
+ const WaterfallUtils = require("devtools/client/performance/modules/logic/waterfall-utils");
+
+ const rootMarkerNode = WaterfallUtils.createParentNode({ name: "(root)" });
+
+ WaterfallUtils.collapseMarkersIntoNode({
+ rootNode: rootMarkerNode,
+ markersList: gTestMarkers,
+ });
+
+ function compare(marker, expected) {
+ for (const prop in expected) {
+ if (prop === "submarkers") {
+ for (let i = 0; i < expected.submarkers.length; i++) {
+ compare(marker.submarkers[i], expected.submarkers[i]);
+ }
+ } else if (prop !== "uid") {
+ equal(marker[prop], expected[prop], `${expected.name} matches ${prop}`);
+ }
+ }
+ }
+
+ compare(rootMarkerNode, gExpectedOutput);
+});
+
+const gTestMarkers = [
+ { start: 2, end: 10, name: "DOMEvent" },
+ { start: 3, end: 9, name: "Javascript" },
+ { start: 4, end: 8, name: "GarbageCollection" },
+ { start: 11, end: 12, name: "Styles" },
+ { start: 13, end: 14, name: "Styles" },
+ { start: 15, end: 25, name: "DOMEvent" },
+ { start: 17, end: 24, name: "Javascript" },
+ { start: 18, end: 19, name: "GarbageCollection" },
+];
+
+const gExpectedOutput = {
+ name: "(root)",
+ submarkers: [
+ {
+ start: 2,
+ end: 10,
+ name: "DOMEvent",
+ submarkers: [
+ {
+ start: 3,
+ end: 9,
+ name: "Javascript",
+ submarkers: [{ start: 4, end: 8, name: "GarbageCollection" }],
+ },
+ ],
+ },
+ { start: 11, end: 12, name: "Styles" },
+ { start: 13, end: 14, name: "Styles" },
+ {
+ start: 15,
+ end: 25,
+ name: "DOMEvent",
+ submarkers: [
+ {
+ start: 17,
+ end: 24,
+ name: "Javascript",
+ submarkers: [{ start: 18, end: 19, name: "GarbageCollection" }],
+ },
+ ],
+ },
+ ],
+};
diff --git a/devtools/client/performance/test/xpcshell/test_waterfall-utils-collapse-04.js b/devtools/client/performance/test/xpcshell/test_waterfall-utils-collapse-04.js
new file mode 100644
index 0000000000..b0ba5c89b6
--- /dev/null
+++ b/devtools/client/performance/test/xpcshell/test_waterfall-utils-collapse-04.js
@@ -0,0 +1,118 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the waterfall collapsing logic works properly
+ * when filtering parents and children.
+ */
+
+add_task(function test() {
+ const WaterfallUtils = require("devtools/client/performance/modules/logic/waterfall-utils");
+
+ [
+ [["DOMEvent"], gExpectedOutputNoDOMEvent],
+ [["Javascript"], gExpectedOutputNoJS],
+ [["DOMEvent", "Javascript"], gExpectedOutputNoDOMEventOrJS],
+ ].forEach(([filter, expected]) => {
+ const rootMarkerNode = WaterfallUtils.createParentNode({ name: "(root)" });
+
+ WaterfallUtils.collapseMarkersIntoNode({
+ rootNode: rootMarkerNode,
+ markersList: gTestMarkers,
+ filter,
+ });
+
+ compare(rootMarkerNode, expected);
+ });
+
+ function compare(marker, expected) {
+ for (const prop in expected) {
+ if (prop === "submarkers") {
+ for (let i = 0; i < expected.submarkers.length; i++) {
+ compare(marker.submarkers[i], expected.submarkers[i]);
+ }
+ } else if (prop !== "uid") {
+ equal(marker[prop], expected[prop], `${expected.name} matches ${prop}`);
+ }
+ }
+ }
+});
+
+const gTestMarkers = [
+ { start: 1, end: 18, name: "DOMEvent" },
+ // Test that JS markers can fold in DOM events and have marker children
+ { start: 2, end: 16, name: "Javascript" },
+ // Test all these markers can be children
+ { start: 3, end: 4, name: "Paint" },
+ { start: 5, end: 6, name: "Reflow" },
+ { start: 7, end: 8, name: "Styles" },
+ { start: 9, end: 9, name: "TimeStamp" },
+ { start: 10, end: 11, name: "Parse HTML" },
+ { start: 12, end: 13, name: "Parse XML" },
+ { start: 14, end: 15, name: "GarbageCollection" },
+ // Test that JS markers can be parents without being a child of DOM events
+ { start: 25, end: 30, name: "Javascript" },
+ { start: 26, end: 27, name: "Paint" },
+];
+
+const gExpectedOutputNoJS = {
+ name: "(root)",
+ submarkers: [
+ {
+ start: 1,
+ end: 18,
+ name: "DOMEvent",
+ submarkers: [
+ { start: 3, end: 4, name: "Paint" },
+ { start: 5, end: 6, name: "Reflow" },
+ { start: 7, end: 8, name: "Styles" },
+ { start: 9, end: 9, name: "TimeStamp" },
+ { start: 10, end: 11, name: "Parse HTML" },
+ { start: 12, end: 13, name: "Parse XML" },
+ { start: 14, end: 15, name: "GarbageCollection" },
+ ],
+ },
+ { start: 26, end: 27, name: "Paint" },
+ ],
+};
+
+const gExpectedOutputNoDOMEvent = {
+ name: "(root)",
+ submarkers: [
+ {
+ start: 2,
+ end: 16,
+ name: "Javascript",
+ submarkers: [
+ { start: 3, end: 4, name: "Paint" },
+ { start: 5, end: 6, name: "Reflow" },
+ { start: 7, end: 8, name: "Styles" },
+ { start: 9, end: 9, name: "TimeStamp" },
+ { start: 10, end: 11, name: "Parse HTML" },
+ { start: 12, end: 13, name: "Parse XML" },
+ { start: 14, end: 15, name: "GarbageCollection" },
+ ],
+ },
+ {
+ start: 25,
+ end: 30,
+ name: "Javascript",
+ submarkers: [{ start: 26, end: 27, name: "Paint" }],
+ },
+ ],
+};
+
+const gExpectedOutputNoDOMEventOrJS = {
+ name: "(root)",
+ submarkers: [
+ { start: 3, end: 4, name: "Paint" },
+ { start: 5, end: 6, name: "Reflow" },
+ { start: 7, end: 8, name: "Styles" },
+ { start: 9, end: 9, name: "TimeStamp" },
+ { start: 10, end: 11, name: "Parse HTML" },
+ { start: 12, end: 13, name: "Parse XML" },
+ { start: 14, end: 15, name: "GarbageCollection" },
+ { start: 26, end: 27, name: "Paint" },
+ ],
+};
diff --git a/devtools/client/performance/test/xpcshell/test_waterfall-utils-collapse-05.js b/devtools/client/performance/test/xpcshell/test_waterfall-utils-collapse-05.js
new file mode 100644
index 0000000000..da645dd526
--- /dev/null
+++ b/devtools/client/performance/test/xpcshell/test_waterfall-utils-collapse-05.js
@@ -0,0 +1,185 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the waterfall collapsing logic works properly
+ * when dealing with OTMT markers.
+ */
+
+add_task(function test() {
+ const WaterfallUtils = require("devtools/client/performance/modules/logic/waterfall-utils");
+
+ const rootMarkerNode = WaterfallUtils.createParentNode({ name: "(root)" });
+
+ WaterfallUtils.collapseMarkersIntoNode({
+ rootNode: rootMarkerNode,
+ markersList: gTestMarkers,
+ });
+
+ compare(rootMarkerNode, gExpectedOutput);
+
+ function compare(marker, expected) {
+ for (const prop in expected) {
+ if (prop === "submarkers") {
+ for (let i = 0; i < expected.submarkers.length; i++) {
+ compare(marker.submarkers[i], expected.submarkers[i]);
+ }
+ } else if (prop !== "uid") {
+ equal(marker[prop], expected[prop], `${expected.name} matches ${prop}`);
+ }
+ }
+ }
+});
+
+const gTestMarkers = [
+ { start: 1, end: 4, name: "A1-mt", processType: 1, isOffMainThread: false },
+ // This should collapse only under A1-mt
+ { start: 2, end: 3, name: "B1", processType: 1, isOffMainThread: false },
+ // This should never collapse.
+ { start: 2, end: 3, name: "C1", processType: 1, isOffMainThread: true },
+
+ { start: 5, end: 8, name: "A1-otmt", processType: 1, isOffMainThread: true },
+ // This should collapse only under A1-mt
+ { start: 6, end: 7, name: "B2", processType: 1, isOffMainThread: false },
+ // This should never collapse.
+ { start: 6, end: 7, name: "C2", processType: 1, isOffMainThread: true },
+
+ { start: 9, end: 12, name: "A2-mt", processType: 2, isOffMainThread: false },
+ // This should collapse only under A2-mt
+ { start: 10, end: 11, name: "D1", processType: 2, isOffMainThread: false },
+ // This should never collapse.
+ { start: 10, end: 11, name: "E1", processType: 2, isOffMainThread: true },
+
+ {
+ start: 13,
+ end: 16,
+ name: "A2-otmt",
+ processType: 2,
+ isOffMainThread: true,
+ },
+ // This should collapse only under A2-mt
+ { start: 14, end: 15, name: "D2", processType: 2, isOffMainThread: false },
+ // This should never collapse.
+ { start: 14, end: 15, name: "E2", processType: 2, isOffMainThread: true },
+
+ // This should not collapse, because there's no parent in this process.
+ { start: 14, end: 15, name: "F", processType: 3, isOffMainThread: false },
+
+ // This should never collapse.
+ { start: 14, end: 15, name: "G", processType: 3, isOffMainThread: true },
+];
+
+const gExpectedOutput = {
+ name: "(root)",
+ submarkers: [
+ {
+ start: 1,
+ end: 4,
+ name: "A1-mt",
+ processType: 1,
+ isOffMainThread: false,
+ submarkers: [
+ {
+ start: 2,
+ end: 3,
+ name: "B1",
+ processType: 1,
+ isOffMainThread: false,
+ },
+ ],
+ },
+ {
+ start: 2,
+ end: 3,
+ name: "C1",
+ processType: 1,
+ isOffMainThread: true,
+ },
+ {
+ start: 5,
+ end: 8,
+ name: "A1-otmt",
+ processType: 1,
+ isOffMainThread: true,
+ submarkers: [
+ {
+ start: 6,
+ end: 7,
+ name: "B2",
+ processType: 1,
+ isOffMainThread: false,
+ },
+ ],
+ },
+ {
+ start: 6,
+ end: 7,
+ name: "C2",
+ processType: 1,
+ isOffMainThread: true,
+ },
+ {
+ start: 9,
+ end: 12,
+ name: "A2-mt",
+ processType: 2,
+ isOffMainThread: false,
+ submarkers: [
+ {
+ start: 10,
+ end: 11,
+ name: "D1",
+ processType: 2,
+ isOffMainThread: false,
+ },
+ ],
+ },
+ {
+ start: 10,
+ end: 11,
+ name: "E1",
+ processType: 2,
+ isOffMainThread: true,
+ },
+ {
+ start: 13,
+ end: 16,
+ name: "A2-otmt",
+ processType: 2,
+ isOffMainThread: true,
+ submarkers: [
+ {
+ start: 14,
+ end: 15,
+ name: "D2",
+ processType: 2,
+ isOffMainThread: false,
+ },
+ ],
+ },
+ {
+ start: 14,
+ end: 15,
+ name: "E2",
+ processType: 2,
+ isOffMainThread: true,
+ },
+ {
+ start: 14,
+ end: 15,
+ name: "F",
+ processType: 3,
+ isOffMainThread: false,
+ submarkers: [],
+ },
+ {
+ start: 14,
+ end: 15,
+ name: "G",
+ processType: 3,
+ isOffMainThread: true,
+ submarkers: [],
+ },
+ ],
+};
diff --git a/devtools/client/performance/test/xpcshell/xpcshell.ini b/devtools/client/performance/test/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..3860d0308f
--- /dev/null
+++ b/devtools/client/performance/test/xpcshell/xpcshell.ini
@@ -0,0 +1,35 @@
+[DEFAULT]
+tags = devtools
+head = head.js
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+
+[test_frame-utils-01.js]
+[test_frame-utils-02.js]
+[test_marker-blueprint.js]
+[test_marker-utils.js]
+[test_profiler-categories.js]
+[test_jit-graph-data.js]
+[test_jit-model-01.js]
+[test_jit-model-02.js]
+[test_perf-utils-allocations-to-samples.js]
+[test_tree-model-01.js]
+[test_tree-model-02.js]
+[test_tree-model-03.js]
+[test_tree-model-04.js]
+[test_tree-model-05.js]
+[test_tree-model-06.js]
+[test_tree-model-07.js]
+[test_tree-model-08.js]
+[test_tree-model-09.js]
+[test_tree-model-10.js]
+[test_tree-model-11.js]
+[test_tree-model-12.js]
+[test_tree-model-13.js]
+[test_tree-model-allocations-01.js]
+[test_tree-model-allocations-02.js]
+[test_waterfall-utils-collapse-01.js]
+[test_waterfall-utils-collapse-02.js]
+[test_waterfall-utils-collapse-03.js]
+[test_waterfall-utils-collapse-04.js]
+[test_waterfall-utils-collapse-05.js]
diff --git a/devtools/client/performance/views/details-abstract-subview.js b/devtools/client/performance/views/details-abstract-subview.js
new file mode 100644
index 0000000000..074d003229
--- /dev/null
+++ b/devtools/client/performance/views/details-abstract-subview.js
@@ -0,0 +1,232 @@
+/* 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/. */
+/* globals PerformanceController, OverviewView, DetailsView */
+"use strict";
+
+const {
+ setNamedTimeout,
+ clearNamedTimeout,
+} = require("devtools/client/shared/widgets/view-helpers");
+const EVENTS = require("devtools/client/performance/events");
+
+/**
+ * A base class from which all detail views inherit.
+ */
+const DetailsSubview = {
+ /**
+ * Sets up the view with event binding.
+ */
+ initialize: function() {
+ this._onRecordingStoppedOrSelected = this._onRecordingStoppedOrSelected.bind(
+ this
+ );
+ this._onOverviewRangeChange = this._onOverviewRangeChange.bind(this);
+ this._onDetailsViewSelected = this._onDetailsViewSelected.bind(this);
+ this._onPrefChanged = this._onPrefChanged.bind(this);
+
+ PerformanceController.on(
+ EVENTS.RECORDING_STATE_CHANGE,
+ this._onRecordingStoppedOrSelected
+ );
+ PerformanceController.on(
+ EVENTS.RECORDING_SELECTED,
+ this._onRecordingStoppedOrSelected
+ );
+ PerformanceController.on(EVENTS.PREF_CHANGED, this._onPrefChanged);
+ OverviewView.on(
+ EVENTS.UI_OVERVIEW_RANGE_SELECTED,
+ this._onOverviewRangeChange
+ );
+ DetailsView.on(
+ EVENTS.UI_DETAILS_VIEW_SELECTED,
+ this._onDetailsViewSelected
+ );
+
+ const self = this;
+ const originalRenderFn = this.render;
+ const afterRenderFn = () => {
+ this._wasRendered = true;
+ };
+
+ this.render = async function(...args) {
+ const maybeRetval = await originalRenderFn.apply(self, args);
+ afterRenderFn();
+ return maybeRetval;
+ };
+ },
+
+ /**
+ * Unbinds events.
+ */
+ destroy: function() {
+ clearNamedTimeout("range-change-debounce");
+
+ PerformanceController.off(
+ EVENTS.RECORDING_STATE_CHANGE,
+ this._onRecordingStoppedOrSelected
+ );
+ PerformanceController.off(
+ EVENTS.RECORDING_SELECTED,
+ this._onRecordingStoppedOrSelected
+ );
+ PerformanceController.off(EVENTS.PREF_CHANGED, this._onPrefChanged);
+ OverviewView.off(
+ EVENTS.UI_OVERVIEW_RANGE_SELECTED,
+ this._onOverviewRangeChange
+ );
+ DetailsView.off(
+ EVENTS.UI_DETAILS_VIEW_SELECTED,
+ this._onDetailsViewSelected
+ );
+ },
+
+ /**
+ * Returns true if this view was rendered at least once.
+ */
+ get wasRenderedAtLeastOnce() {
+ return !!this._wasRendered;
+ },
+
+ /**
+ * Amount of time (in milliseconds) to wait until this view gets updated,
+ * when the range is changed in the overview.
+ */
+ rangeChangeDebounceTime: 0,
+
+ /**
+ * When the overview range changes, all details views will require a
+ * rerendering at a later point, determined by `shouldUpdateWhenShown` and
+ * `canUpdateWhileHidden` and whether or not its the current view.
+ * Set `requiresUpdateOnRangeChange` to false to not invalidate the view
+ * when the range changes.
+ */
+ requiresUpdateOnRangeChange: true,
+
+ /**
+ * Flag specifying if this view should be updated when selected. This will
+ * be set to true, for example, when the range changes in the overview and
+ * this view is not currently visible.
+ */
+ shouldUpdateWhenShown: false,
+
+ /**
+ * Flag specifying if this view may get updated even when it's not selected.
+ * Should only be used in tests.
+ */
+ canUpdateWhileHidden: false,
+
+ /**
+ * An array of preferences under `devtools.performance.ui.` that the view should
+ * rerender and callback `this._onRerenderPrefChanged` upon change.
+ */
+ rerenderPrefs: [],
+
+ /**
+ * An array of preferences under `devtools.performance.` that the view should
+ * observe and callback `this._onObservedPrefChange` upon change.
+ */
+ observedPrefs: [],
+
+ /**
+ * Flag specifying if this view should update while the overview selection
+ * area is actively being dragged by the mouse.
+ */
+ shouldUpdateWhileMouseIsActive: false,
+
+ /**
+ * Called when recording stops or is selected.
+ */
+ _onRecordingStoppedOrSelected: function(state, recording) {
+ if (typeof state !== "string") {
+ recording = state;
+ }
+ if (arguments.length === 3 && state !== "recording-stopped") {
+ return;
+ }
+
+ if (!recording || !recording.isCompleted()) {
+ return;
+ }
+ if (DetailsView.isViewSelected(this) || this.canUpdateWhileHidden) {
+ this.render(OverviewView.getTimeInterval());
+ } else {
+ this.shouldUpdateWhenShown = true;
+ }
+ },
+
+ /**
+ * Fired when a range is selected or cleared in the OverviewView.
+ */
+ _onOverviewRangeChange: function(interval) {
+ if (!this.requiresUpdateOnRangeChange) {
+ return;
+ }
+ if (DetailsView.isViewSelected(this)) {
+ const debounced = () => {
+ if (
+ !this.shouldUpdateWhileMouseIsActive &&
+ OverviewView.isMouseActive
+ ) {
+ // Don't render yet, while the selection is still being dragged.
+ setNamedTimeout(
+ "range-change-debounce",
+ this.rangeChangeDebounceTime,
+ debounced
+ );
+ } else {
+ this.render(interval);
+ }
+ };
+ setNamedTimeout(
+ "range-change-debounce",
+ this.rangeChangeDebounceTime,
+ debounced
+ );
+ } else {
+ this.shouldUpdateWhenShown = true;
+ }
+ },
+
+ /**
+ * Fired when a view is selected in the DetailsView.
+ */
+ _onDetailsViewSelected: function() {
+ if (DetailsView.isViewSelected(this) && this.shouldUpdateWhenShown) {
+ this.render(OverviewView.getTimeInterval());
+ this.shouldUpdateWhenShown = false;
+ }
+ },
+
+ /**
+ * Fired when a preference in `devtools.performance.ui.` is changed.
+ */
+ _onPrefChanged: function(prefName, prefValue) {
+ if (~this.observedPrefs.indexOf(prefName) && this._onObservedPrefChange) {
+ this._onObservedPrefChange(prefName);
+ }
+
+ // All detail views require a recording to be complete, so do not
+ // attempt to render if recording is in progress or does not exist.
+ const recording = PerformanceController.getCurrentRecording();
+ if (!recording || !recording.isCompleted()) {
+ return;
+ }
+
+ if (!~this.rerenderPrefs.indexOf(prefName)) {
+ return;
+ }
+
+ if (this._onRerenderPrefChanged) {
+ this._onRerenderPrefChanged(prefName);
+ }
+
+ if (DetailsView.isViewSelected(this) || this.canUpdateWhileHidden) {
+ this.render(OverviewView.getTimeInterval());
+ } else {
+ this.shouldUpdateWhenShown = true;
+ }
+ },
+};
+
+exports.DetailsSubview = DetailsSubview;
diff --git a/devtools/client/performance/views/details-js-call-tree.js b/devtools/client/performance/views/details-js-call-tree.js
new file mode 100644
index 0000000000..7ef2d05483
--- /dev/null
+++ b/devtools/client/performance/views/details-js-call-tree.js
@@ -0,0 +1,234 @@
+/* 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/. */
+/* globals $, PerformanceController */
+"use strict";
+
+const { extend } = require("devtools/shared/extend");
+
+const React = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+
+const EVENTS = require("devtools/client/performance/events");
+const {
+ CallView,
+} = require("devtools/client/performance/modules/widgets/tree-view");
+const {
+ ThreadNode,
+} = require("devtools/client/performance/modules/logic/tree-model");
+const {
+ DetailsSubview,
+} = require("devtools/client/performance/views/details-abstract-subview");
+
+const JITOptimizationsView = React.createFactory(
+ require("devtools/client/performance/components/JITOptimizations")
+);
+
+const EventEmitter = require("devtools/shared/event-emitter");
+
+/**
+ * CallTree view containing profiler call tree, controlled by DetailsView.
+ */
+const JsCallTreeView = extend(DetailsSubview, {
+ rerenderPrefs: [
+ "invert-call-tree",
+ "show-platform-data",
+ "flatten-tree-recursion",
+ "show-jit-optimizations",
+ ],
+
+ // Units are in milliseconds.
+ rangeChangeDebounceTime: 75,
+
+ /**
+ * Sets up the view with event binding.
+ */
+ initialize: function() {
+ DetailsSubview.initialize.call(this);
+
+ this._onLink = this._onLink.bind(this);
+ this._onFocus = this._onFocus.bind(this);
+
+ this.container = $("#js-calltree-view .call-tree-cells-container");
+
+ this.optimizationsElement = $("#jit-optimizations-view");
+ },
+
+ /**
+ * Unbinds events.
+ */
+ destroy: function() {
+ ReactDOM.unmountComponentAtNode(this.optimizationsElement);
+ this.optimizationsElement = null;
+ this.container = null;
+ this.threadNode = null;
+ DetailsSubview.destroy.call(this);
+ },
+
+ /**
+ * Method for handling all the set up for rendering a new call tree.
+ *
+ * @param object interval [optional]
+ * The { startTime, endTime }, in milliseconds.
+ */
+ render: function(interval = {}) {
+ const recording = PerformanceController.getCurrentRecording();
+ const profile = recording.getProfile();
+ const showOptimizations = PerformanceController.getOption(
+ "show-jit-optimizations"
+ );
+
+ const options = {
+ contentOnly: !PerformanceController.getOption("show-platform-data"),
+ invertTree: PerformanceController.getOption("invert-call-tree"),
+ flattenRecursion: PerformanceController.getOption(
+ "flatten-tree-recursion"
+ ),
+ showOptimizationHint: showOptimizations,
+ };
+ const threadNode = (this.threadNode = this._prepareCallTree(
+ profile,
+ interval,
+ options
+ ));
+ this._populateCallTree(threadNode, options);
+
+ // For better or worse, re-rendering loses frame selection,
+ // so we should always hide opts on rerender
+ this.hideOptimizations();
+
+ this.emit(EVENTS.UI_JS_CALL_TREE_RENDERED);
+ },
+
+ showOptimizations: function() {
+ this.optimizationsElement.classList.remove("hidden");
+ },
+
+ hideOptimizations: function() {
+ this.optimizationsElement.classList.add("hidden");
+ },
+
+ _onFocus: function(treeItem) {
+ const showOptimizations = PerformanceController.getOption(
+ "show-jit-optimizations"
+ );
+ const frameNode = treeItem.frame;
+ const optimizationSites =
+ frameNode && frameNode.hasOptimizations()
+ ? frameNode.getOptimizations().optimizationSites
+ : [];
+
+ if (!showOptimizations || !frameNode || optimizationSites.length === 0) {
+ this.hideOptimizations();
+ this.emit("focus", treeItem);
+ return;
+ }
+
+ this.showOptimizations();
+
+ const frameData = frameNode.getInfo();
+ const optimizations = JITOptimizationsView({
+ frameData,
+ optimizationSites,
+ onViewSourceInDebugger: ({ url, line, column }) => {
+ PerformanceController.viewSourceInDebugger(url, line, column).then(
+ success => {
+ if (success) {
+ this.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
+ } else {
+ this.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER);
+ }
+ }
+ );
+ },
+ });
+
+ ReactDOM.render(optimizations, this.optimizationsElement);
+
+ this.emit("focus", treeItem);
+ },
+
+ /**
+ * Fired on the "link" event for the call tree in this container.
+ */
+ _onLink: function(treeItem) {
+ const { url, line, column } = treeItem.frame.getInfo();
+ PerformanceController.viewSourceInDebugger(url, line, column).then(
+ success => {
+ if (success) {
+ this.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
+ } else {
+ this.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER);
+ }
+ }
+ );
+ },
+
+ /**
+ * Called when the recording is stopped and prepares data to
+ * populate the call tree.
+ */
+ _prepareCallTree: function(profile, { startTime, endTime }, options) {
+ const thread = profile.threads[0];
+ const { contentOnly, invertTree, flattenRecursion } = options;
+ const threadNode = new ThreadNode(thread, {
+ startTime,
+ endTime,
+ contentOnly,
+ invertTree,
+ flattenRecursion,
+ });
+
+ // Real profiles from nsProfiler (i.e. not synthesized from allocation
+ // logs) always have a (root) node. Go down one level in the uninverted
+ // view to avoid displaying both the synthesized root node and the (root)
+ // node from the profiler.
+ if (!invertTree) {
+ threadNode.calls = threadNode.calls[0].calls;
+ }
+
+ return threadNode;
+ },
+
+ /**
+ * Renders the call tree.
+ */
+ _populateCallTree: function(frameNode, options = {}) {
+ // If we have an empty profile (no samples), then don't invert the tree, as
+ // it would hide the root node and a completely blank call tree space can be
+ // mis-interpreted as an error.
+ const inverted = options.invertTree && frameNode.samples > 0;
+
+ const root = new CallView({
+ frame: frameNode,
+ inverted: inverted,
+ // The synthesized root node is hidden in inverted call trees.
+ hidden: inverted,
+ // Call trees should only auto-expand when not inverted. Passing undefined
+ // will default to the CALL_TREE_AUTO_EXPAND depth.
+ autoExpandDepth: inverted ? 0 : undefined,
+ showOptimizationHint: options.showOptimizationHint,
+ });
+
+ // Bind events.
+ root.on("link", this._onLink);
+ root.on("focus", this._onFocus);
+
+ // Clear out other call trees.
+ this.container.innerHTML = "";
+ root.attachTo(this.container);
+
+ // When platform data isn't shown, hide the cateogry labels, since they're
+ // only available for C++ frames. Pass *false* to make them invisible.
+ root.toggleCategories(!options.contentOnly);
+
+ // Return the CallView for tests
+ return root;
+ },
+
+ toString: () => "[object JsCallTreeView]",
+});
+
+EventEmitter.decorate(JsCallTreeView);
+
+exports.JsCallTreeView = JsCallTreeView;
diff --git a/devtools/client/performance/views/details-js-flamegraph.js b/devtools/client/performance/views/details-js-flamegraph.js
new file mode 100644
index 0000000000..bacdb018f5
--- /dev/null
+++ b/devtools/client/performance/views/details-js-flamegraph.js
@@ -0,0 +1,143 @@
+/* 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/. */
+/* globals $, PerformanceController, OverviewView */
+"use strict";
+
+const { extend } = require("devtools/shared/extend");
+
+const EVENTS = require("devtools/client/performance/events");
+const { L10N } = require("devtools/client/performance/modules/global");
+const {
+ DetailsSubview,
+} = require("devtools/client/performance/views/details-abstract-subview");
+
+const {
+ FlameGraph,
+ FlameGraphUtils,
+} = require("devtools/client/shared/widgets/FlameGraph");
+
+const EventEmitter = require("devtools/shared/event-emitter");
+
+/**
+ * FlameGraph view containing a pyramid-like visualization of a profile,
+ * controlled by DetailsView.
+ */
+const JsFlameGraphView = extend(DetailsSubview, {
+ shouldUpdateWhileMouseIsActive: true,
+
+ rerenderPrefs: [
+ "invert-flame-graph",
+ "flatten-tree-recursion",
+ "show-platform-data",
+ "show-idle-blocks",
+ ],
+
+ /**
+ * Sets up the view with event binding.
+ */
+ async initialize() {
+ DetailsSubview.initialize.call(this);
+
+ this.graph = new FlameGraph($("#js-flamegraph-view"));
+ this.graph.timelineTickUnits = L10N.getStr("graphs.ms");
+ this.graph.setTheme(PerformanceController.getTheme());
+ await this.graph.ready();
+
+ this._onRangeChangeInGraph = this._onRangeChangeInGraph.bind(this);
+ this._onThemeChanged = this._onThemeChanged.bind(this);
+
+ PerformanceController.on(EVENTS.THEME_CHANGED, this._onThemeChanged);
+ this.graph.on("selecting", this._onRangeChangeInGraph);
+ },
+
+ /**
+ * Unbinds events.
+ */
+ async destroy() {
+ DetailsSubview.destroy.call(this);
+
+ PerformanceController.off(EVENTS.THEME_CHANGED, this._onThemeChanged);
+ this.graph.off("selecting", this._onRangeChangeInGraph);
+
+ await this.graph.destroy();
+ },
+
+ /**
+ * Method for handling all the set up for rendering a new flamegraph.
+ *
+ * @param object interval [optional]
+ * The { startTime, endTime }, in milliseconds.
+ */
+ render: function(interval = {}) {
+ const recording = PerformanceController.getCurrentRecording();
+ const duration = recording.getDuration();
+ const profile = recording.getProfile();
+ const thread = profile.threads[0];
+
+ const data = FlameGraphUtils.createFlameGraphDataFromThread(thread, {
+ invertTree: PerformanceController.getOption("invert-flame-graph"),
+ flattenRecursion: PerformanceController.getOption(
+ "flatten-tree-recursion"
+ ),
+ contentOnly: !PerformanceController.getOption("show-platform-data"),
+ showIdleBlocks:
+ PerformanceController.getOption("show-idle-blocks") &&
+ L10N.getStr("table.idle"),
+ });
+
+ this.graph.setData({
+ data,
+ bounds: {
+ startTime: 0,
+ endTime: duration,
+ },
+ visible: {
+ startTime: interval.startTime || 0,
+ endTime: interval.endTime || duration,
+ },
+ });
+
+ this.graph.focus();
+
+ this.emit(EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ },
+
+ /**
+ * Fired when a range is selected or cleared in the FlameGraph.
+ */
+ _onRangeChangeInGraph: function() {
+ const interval = this.graph.getViewRange();
+
+ // Squelch rerendering this view when we update the range here
+ // to avoid recursion, as our FlameGraph handles rerendering itself
+ // when originating from within the graph.
+ this.requiresUpdateOnRangeChange = false;
+ OverviewView.setTimeInterval(interval);
+ this.requiresUpdateOnRangeChange = true;
+ },
+
+ /**
+ * Called whenever a pref is changed and this view needs to be rerendered.
+ */
+ _onRerenderPrefChanged: function() {
+ const recording = PerformanceController.getCurrentRecording();
+ const profile = recording.getProfile();
+ const thread = profile.threads[0];
+ FlameGraphUtils.removeFromCache(thread);
+ },
+
+ /**
+ * Called when `devtools.theme` changes.
+ */
+ _onThemeChanged: function(theme) {
+ this.graph.setTheme(theme);
+ this.graph.refresh({ force: true });
+ },
+
+ toString: () => "[object JsFlameGraphView]",
+});
+
+EventEmitter.decorate(JsFlameGraphView);
+
+exports.JsFlameGraphView = JsFlameGraphView;
diff --git a/devtools/client/performance/views/details-memory-call-tree.js b/devtools/client/performance/views/details-memory-call-tree.js
new file mode 100644
index 0000000000..17fcfcad0c
--- /dev/null
+++ b/devtools/client/performance/views/details-memory-call-tree.js
@@ -0,0 +1,145 @@
+/* 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/. */
+/* globals $, PerformanceController */
+"use strict";
+
+const { extend } = require("devtools/shared/extend");
+
+const EVENTS = require("devtools/client/performance/events");
+const {
+ ThreadNode,
+} = require("devtools/client/performance/modules/logic/tree-model");
+const {
+ CallView,
+} = require("devtools/client/performance/modules/widgets/tree-view");
+const {
+ DetailsSubview,
+} = require("devtools/client/performance/views/details-abstract-subview");
+
+const RecordingUtils = require("devtools/shared/performance/recording-utils");
+const EventEmitter = require("devtools/shared/event-emitter");
+
+/**
+ * CallTree view containing memory allocation sites, controlled by DetailsView.
+ */
+const MemoryCallTreeView = extend(DetailsSubview, {
+ rerenderPrefs: ["invert-call-tree"],
+
+ // Units are in milliseconds.
+ rangeChangeDebounceTime: 100,
+
+ /**
+ * Sets up the view with event binding.
+ */
+ initialize: function() {
+ DetailsSubview.initialize.call(this);
+
+ this._onLink = this._onLink.bind(this);
+
+ this.container = $("#memory-calltree-view > .call-tree-cells-container");
+ },
+
+ /**
+ * Unbinds events.
+ */
+ destroy: function() {
+ DetailsSubview.destroy.call(this);
+ },
+
+ /**
+ * Method for handling all the set up for rendering a new call tree.
+ *
+ * @param object interval [optional]
+ * The { startTime, endTime }, in milliseconds.
+ */
+ render: function(interval = {}) {
+ const options = {
+ invertTree: PerformanceController.getOption("invert-call-tree"),
+ };
+ const recording = PerformanceController.getCurrentRecording();
+ const allocations = recording.getAllocations();
+ const threadNode = this._prepareCallTree(allocations, interval, options);
+ this._populateCallTree(threadNode, options);
+ this.emit(EVENTS.UI_MEMORY_CALL_TREE_RENDERED);
+ },
+
+ /**
+ * Fired on the "link" event for the call tree in this container.
+ */
+ _onLink: function(treeItem) {
+ const { url, line, column } = treeItem.frame.getInfo();
+ PerformanceController.viewSourceInDebugger(url, line, column).then(
+ success => {
+ if (success) {
+ this.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
+ } else {
+ this.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER);
+ }
+ }
+ );
+ },
+
+ /**
+ * Called when the recording is stopped and prepares data to
+ * populate the call tree.
+ */
+ _prepareCallTree: function(allocations, { startTime, endTime }, options) {
+ const thread = RecordingUtils.getProfileThreadFromAllocations(allocations);
+ const { invertTree } = options;
+
+ return new ThreadNode(thread, { startTime, endTime, invertTree });
+ },
+
+ /**
+ * Renders the call tree.
+ */
+ _populateCallTree: function(frameNode, options = {}) {
+ // If we have an empty profile (no samples), then don't invert the tree, as
+ // it would hide the root node and a completely blank call tree space can be
+ // mis-interpreted as an error.
+ const inverted = options.invertTree && frameNode.samples > 0;
+
+ const root = new CallView({
+ frame: frameNode,
+ inverted: inverted,
+ // Root nodes are hidden in inverted call trees.
+ hidden: inverted,
+ // Call trees should only auto-expand when not inverted. Passing undefined
+ // will default to the CALL_TREE_AUTO_EXPAND depth.
+ autoExpandDepth: inverted ? 0 : undefined,
+ // Some cells like the time duration and cost percentage don't make sense
+ // for a memory allocations call tree.
+ visibleCells: {
+ selfCount: true,
+ count: true,
+ selfSize: true,
+ size: true,
+ selfCountPercentage: true,
+ countPercentage: true,
+ selfSizePercentage: true,
+ sizePercentage: true,
+ function: true,
+ },
+ });
+
+ // Bind events.
+ root.on("link", this._onLink);
+
+ // Pipe "focus" events to the view, mostly for tests
+ root.on("focus", () => this.emit("focus"));
+
+ // Clear out other call trees.
+ this.container.innerHTML = "";
+ root.attachTo(this.container);
+
+ // Memory allocation samples don't contain cateogry labels.
+ root.toggleCategories(false);
+ },
+
+ toString: () => "[object MemoryCallTreeView]",
+});
+
+EventEmitter.decorate(MemoryCallTreeView);
+
+exports.MemoryCallTreeView = MemoryCallTreeView;
diff --git a/devtools/client/performance/views/details-memory-flamegraph.js b/devtools/client/performance/views/details-memory-flamegraph.js
new file mode 100644
index 0000000000..e8f1f51fd1
--- /dev/null
+++ b/devtools/client/performance/views/details-memory-flamegraph.js
@@ -0,0 +1,138 @@
+/* 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/. */
+/* globals $, PerformanceController, OverviewView */
+"use strict";
+
+const {
+ FlameGraph,
+ FlameGraphUtils,
+} = require("devtools/client/shared/widgets/FlameGraph");
+const { extend } = require("devtools/shared/extend");
+const RecordingUtils = require("devtools/shared/performance/recording-utils");
+const EventEmitter = require("devtools/shared/event-emitter");
+
+const EVENTS = require("devtools/client/performance/events");
+const {
+ DetailsSubview,
+} = require("devtools/client/performance/views/details-abstract-subview");
+const { L10N } = require("devtools/client/performance/modules/global");
+
+/**
+ * FlameGraph view containing a pyramid-like visualization of memory allocation
+ * sites, controlled by DetailsView.
+ */
+const MemoryFlameGraphView = extend(DetailsSubview, {
+ shouldUpdateWhileMouseIsActive: true,
+
+ rerenderPrefs: [
+ "invert-flame-graph",
+ "flatten-tree-recursion",
+ "show-idle-blocks",
+ ],
+
+ /**
+ * Sets up the view with event binding.
+ */
+ async initialize() {
+ DetailsSubview.initialize.call(this);
+
+ this.graph = new FlameGraph($("#memory-flamegraph-view"));
+ this.graph.timelineTickUnits = L10N.getStr("graphs.ms");
+ this.graph.setTheme(PerformanceController.getTheme());
+ await this.graph.ready();
+
+ this._onRangeChangeInGraph = this._onRangeChangeInGraph.bind(this);
+ this._onThemeChanged = this._onThemeChanged.bind(this);
+
+ PerformanceController.on(EVENTS.THEME_CHANGED, this._onThemeChanged);
+ this.graph.on("selecting", this._onRangeChangeInGraph);
+ },
+
+ /**
+ * Unbinds events.
+ */
+ async destroy() {
+ DetailsSubview.destroy.call(this);
+
+ PerformanceController.off(EVENTS.THEME_CHANGED, this._onThemeChanged);
+ this.graph.off("selecting", this._onRangeChangeInGraph);
+
+ await this.graph.destroy();
+ },
+
+ /**
+ * Method for handling all the set up for rendering a new flamegraph.
+ *
+ * @param object interval [optional]
+ * The { startTime, endTime }, in milliseconds.
+ */
+ render: function(interval = {}) {
+ const recording = PerformanceController.getCurrentRecording();
+ const duration = recording.getDuration();
+ const allocations = recording.getAllocations();
+
+ const thread = RecordingUtils.getProfileThreadFromAllocations(allocations);
+ const data = FlameGraphUtils.createFlameGraphDataFromThread(thread, {
+ invertStack: PerformanceController.getOption("invert-flame-graph"),
+ flattenRecursion: PerformanceController.getOption(
+ "flatten-tree-recursion"
+ ),
+ showIdleBlocks:
+ PerformanceController.getOption("show-idle-blocks") &&
+ L10N.getStr("table.idle"),
+ });
+
+ this.graph.setData({
+ data,
+ bounds: {
+ startTime: 0,
+ endTime: duration,
+ },
+ visible: {
+ startTime: interval.startTime || 0,
+ endTime: interval.endTime || duration,
+ },
+ });
+
+ this.emit(EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
+ },
+
+ /**
+ * Fired when a range is selected or cleared in the FlameGraph.
+ */
+ _onRangeChangeInGraph: function() {
+ const interval = this.graph.getViewRange();
+
+ // Squelch rerendering this view when we update the range here
+ // to avoid recursion, as our FlameGraph handles rerendering itself
+ // when originating from within the graph.
+ this.requiresUpdateOnRangeChange = false;
+ OverviewView.setTimeInterval(interval);
+ this.requiresUpdateOnRangeChange = true;
+ },
+
+ /**
+ * Called whenever a pref is changed and this view needs to be rerendered.
+ */
+ _onRerenderPrefChanged: function() {
+ const recording = PerformanceController.getCurrentRecording();
+ const allocations = recording.getAllocations();
+ const thread = RecordingUtils.getProfileThreadFromAllocations(allocations);
+ FlameGraphUtils.removeFromCache(thread);
+ },
+
+ /**
+ * Called when `devtools.theme` changes.
+ */
+ _onThemeChanged: function(theme) {
+ this.graph.setTheme(theme);
+ this.graph.refresh({ force: true });
+ },
+
+ toString: () => "[object MemoryFlameGraphView]",
+});
+
+EventEmitter.decorate(MemoryFlameGraphView);
+
+exports.MemoryFlameGraphView = MemoryFlameGraphView;
diff --git a/devtools/client/performance/views/details-waterfall.js b/devtools/client/performance/views/details-waterfall.js
new file mode 100644
index 0000000000..975aa43874
--- /dev/null
+++ b/devtools/client/performance/views/details-waterfall.js
@@ -0,0 +1,282 @@
+/* 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/. */
+/* globals $, PerformanceController, OverviewView, DetailsView */
+"use strict";
+
+const { extend } = require("devtools/shared/extend");
+const EventEmitter = require("devtools/shared/event-emitter");
+const {
+ setNamedTimeout,
+ clearNamedTimeout,
+} = require("devtools/client/shared/widgets/view-helpers");
+
+const React = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+
+const EVENTS = require("devtools/client/performance/events");
+const WaterfallUtils = require("devtools/client/performance/modules/logic/waterfall-utils");
+const {
+ TickUtils,
+} = require("devtools/client/performance/modules/waterfall-ticks");
+const {
+ MarkerDetails,
+} = require("devtools/client/performance/modules/widgets/marker-details");
+const {
+ DetailsSubview,
+} = require("devtools/client/performance/views/details-abstract-subview");
+
+const Waterfall = React.createFactory(
+ require("devtools/client/performance/components/Waterfall")
+);
+
+const MARKER_DETAILS_WIDTH = 200;
+// Units are in milliseconds.
+const WATERFALL_RESIZE_EVENTS_DRAIN = 100;
+
+/**
+ * Waterfall view containing the timeline markers, controlled by DetailsView.
+ */
+const WaterfallView = extend(DetailsSubview, {
+ // Smallest unit of time between two markers. Larger by 10x^3 than Number.EPSILON.
+ MARKER_EPSILON: 0.000000000001,
+ // px
+ WATERFALL_MARKER_SIDEBAR_WIDTH: 175,
+ // px
+ WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS: 20,
+
+ observedPrefs: ["hidden-markers"],
+
+ rerenderPrefs: ["hidden-markers"],
+
+ // Units are in milliseconds.
+ rangeChangeDebounceTime: 75,
+
+ /**
+ * Sets up the view with event binding.
+ */
+ initialize: function() {
+ DetailsSubview.initialize.call(this);
+
+ this._cache = new WeakMap();
+
+ this._onMarkerSelected = this._onMarkerSelected.bind(this);
+ this._onResize = this._onResize.bind(this);
+ this._onViewSource = this._onViewSource.bind(this);
+ this._onShowAllocations = this._onShowAllocations.bind(this);
+ this._hiddenMarkers = PerformanceController.getPref("hidden-markers");
+
+ this.treeContainer = $("#waterfall-tree");
+ this.detailsContainer = $("#waterfall-details");
+ this.detailsSplitter = $("#waterfall-view > splitter");
+
+ this.details = new MarkerDetails(
+ $("#waterfall-details"),
+ $("#waterfall-view > splitter")
+ );
+ this.details.hidden = true;
+
+ this.details.on("resize", this._onResize);
+ this.details.on("view-source", this._onViewSource);
+ this.details.on("show-allocations", this._onShowAllocations);
+ window.addEventListener("resize", this._onResize);
+
+ // TODO bug 1167093 save the previously set width, and ensure minimum width
+ this.details.width = MARKER_DETAILS_WIDTH;
+ },
+
+ /**
+ * Unbinds events.
+ */
+ destroy: function() {
+ DetailsSubview.destroy.call(this);
+
+ clearNamedTimeout("waterfall-resize");
+
+ this._cache = null;
+
+ this.details.off("resize", this._onResize);
+ this.details.off("view-source", this._onViewSource);
+ this.details.off("show-allocations", this._onShowAllocations);
+ window.removeEventListener("resize", this._onResize);
+
+ ReactDOM.unmountComponentAtNode(this.treeContainer);
+ },
+
+ /**
+ * Method for handling all the set up for rendering a new waterfall.
+ *
+ * @param object interval [optional]
+ * The { startTime, endTime }, in milliseconds.
+ */
+ render: function(interval = {}) {
+ const recording = PerformanceController.getCurrentRecording();
+ if (recording.isRecording()) {
+ return;
+ }
+ const startTime = interval.startTime || 0;
+ const endTime = interval.endTime || recording.getDuration();
+ const markers = recording.getMarkers();
+ const rootMarkerNode = this._prepareWaterfallTree(markers);
+
+ this._populateWaterfallTree(rootMarkerNode, { startTime, endTime });
+ this.emit(EVENTS.UI_WATERFALL_RENDERED);
+ },
+
+ /**
+ * Called when a marker is selected in the waterfall view,
+ * updating the markers detail view.
+ */
+ _onMarkerSelected: function(event, marker) {
+ const recording = PerformanceController.getCurrentRecording();
+ const frames = recording.getFrames();
+ const allocations = recording.getConfiguration().withAllocations;
+
+ if (event === "selected") {
+ this.details.render({ marker, frames, allocations });
+ this.details.hidden = false;
+ }
+ if (event === "unselected") {
+ this.details.empty();
+ }
+ },
+
+ /**
+ * Called when the marker details view is resized.
+ */
+ _onResize: function() {
+ setNamedTimeout("waterfall-resize", WATERFALL_RESIZE_EVENTS_DRAIN, () => {
+ this.render(OverviewView.getTimeInterval());
+ });
+ },
+
+ /**
+ * Called whenever an observed pref is changed.
+ */
+ _onObservedPrefChange: function(prefName) {
+ this._hiddenMarkers = PerformanceController.getPref("hidden-markers");
+
+ // Clear the cache as we'll need to recompute the collapsed
+ // marker model
+ this._cache = new WeakMap();
+ },
+
+ /**
+ * Called when MarkerDetails view emits an event to view source.
+ */
+ _onViewSource: function(data) {
+ PerformanceController.viewSourceInDebugger(
+ data.url,
+ data.line,
+ data.column
+ );
+ },
+
+ /**
+ * Called when MarkerDetails view emits an event to snap to allocations.
+ */
+ _onShowAllocations: function(data) {
+ let { endTime } = data;
+ let startTime = 0;
+ const recording = PerformanceController.getCurrentRecording();
+ const markers = recording.getMarkers();
+
+ let lastGCMarkerFromPreviousCycle = null;
+ let lastGCMarker = null;
+ // Iterate over markers looking for the most recent GC marker
+ // from the cycle before the marker's whose allocations we're interested in.
+ for (const marker of markers) {
+ // We found the marker whose allocations we're tracking; abort
+ if (marker.start === endTime) {
+ break;
+ }
+
+ if (marker.name === "GarbageCollection") {
+ if (lastGCMarker && lastGCMarker.cycle !== marker.cycle) {
+ lastGCMarkerFromPreviousCycle = lastGCMarker;
+ }
+ lastGCMarker = marker;
+ }
+ }
+
+ if (lastGCMarkerFromPreviousCycle) {
+ startTime = lastGCMarkerFromPreviousCycle.end;
+ }
+
+ // Adjust times so we don't include the range of these markers themselves.
+ endTime -= this.MARKER_EPSILON;
+ startTime += startTime !== 0 ? this.MARKER_EPSILON : 0;
+
+ OverviewView.setTimeInterval({ startTime, endTime });
+ DetailsView.selectView("memory-calltree");
+ },
+
+ /**
+ * Called when the recording is stopped and prepares data to
+ * populate the waterfall tree.
+ */
+ _prepareWaterfallTree: function(markers) {
+ const cached = this._cache.get(markers);
+ if (cached) {
+ return cached;
+ }
+
+ const rootMarkerNode = WaterfallUtils.createParentNode({ name: "(root)" });
+
+ WaterfallUtils.collapseMarkersIntoNode({
+ rootNode: rootMarkerNode,
+ markersList: markers,
+ filter: this._hiddenMarkers,
+ });
+
+ this._cache.set(markers, rootMarkerNode);
+ return rootMarkerNode;
+ },
+
+ /**
+ * Calculates the available width for the waterfall.
+ * This should be invoked every time the container node is resized.
+ */
+ _recalculateBounds: function() {
+ this.waterfallWidth =
+ this.treeContainer.clientWidth -
+ this.WATERFALL_MARKER_SIDEBAR_WIDTH -
+ this.WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS;
+ },
+
+ /**
+ * Renders the waterfall tree.
+ */
+ _populateWaterfallTree: function(rootMarkerNode, interval) {
+ this._recalculateBounds();
+
+ const doc = this.treeContainer.ownerDocument;
+ const startTime = interval.startTime | 0;
+ const endTime = interval.endTime | 0;
+ const dataScale = this.waterfallWidth / (endTime - startTime);
+
+ this.canvas = TickUtils.drawWaterfallBackground(
+ doc,
+ dataScale,
+ this.waterfallWidth
+ );
+
+ const treeView = Waterfall({
+ marker: rootMarkerNode,
+ startTime,
+ endTime,
+ dataScale,
+ sidebarWidth: this.WATERFALL_MARKER_SIDEBAR_WIDTH,
+ waterfallWidth: this.waterfallWidth,
+ onFocus: node => this._onMarkerSelected("selected", node),
+ });
+
+ ReactDOM.render(treeView, this.treeContainer);
+ },
+
+ toString: () => "[object WaterfallView]",
+});
+
+EventEmitter.decorate(WaterfallView);
+
+exports.WaterfallView = WaterfallView;
diff --git a/devtools/client/performance/views/details.js b/devtools/client/performance/views/details.js
new file mode 100644
index 0000000000..67c97d6bb5
--- /dev/null
+++ b/devtools/client/performance/views/details.js
@@ -0,0 +1,295 @@
+/* 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/. */
+/* globals $, $$, PerformanceController */
+
+"use strict";
+
+const EVENTS = require("devtools/client/performance/events");
+
+const {
+ WaterfallView,
+} = require("devtools/client/performance/views/details-waterfall");
+const {
+ JsCallTreeView,
+} = require("devtools/client/performance/views/details-js-call-tree");
+const {
+ JsFlameGraphView,
+} = require("devtools/client/performance/views/details-js-flamegraph");
+const {
+ MemoryCallTreeView,
+} = require("devtools/client/performance/views/details-memory-call-tree");
+const {
+ MemoryFlameGraphView,
+} = require("devtools/client/performance/views/details-memory-flamegraph");
+
+const EventEmitter = require("devtools/shared/event-emitter");
+
+/**
+ * Details view containing call trees, flamegraphs and markers waterfall.
+ * Manages subviews and toggles visibility between them.
+ */
+const DetailsView = {
+ /**
+ * Name to (node id, view object, actor requirements, pref killswitch)
+ * mapping of subviews.
+ */
+ components: {
+ waterfall: {
+ id: "waterfall-view",
+ view: WaterfallView,
+ features: ["withMarkers"],
+ },
+ "js-calltree": {
+ id: "js-profile-view",
+ view: JsCallTreeView,
+ },
+ "js-flamegraph": {
+ id: "js-flamegraph-view",
+ view: JsFlameGraphView,
+ },
+ "memory-calltree": {
+ id: "memory-calltree-view",
+ view: MemoryCallTreeView,
+ features: ["withAllocations"],
+ },
+ "memory-flamegraph": {
+ id: "memory-flamegraph-view",
+ view: MemoryFlameGraphView,
+ features: ["withAllocations"],
+ prefs: ["enable-memory-flame"],
+ },
+ },
+
+ /**
+ * Sets up the view with event binding, initializes subviews.
+ */
+ async initialize() {
+ this.el = $("#details-pane");
+ this.toolbar = $("#performance-toolbar-controls-detail-views");
+
+ this._onViewToggle = this._onViewToggle.bind(this);
+ this._onRecordingStoppedOrSelected = this._onRecordingStoppedOrSelected.bind(
+ this
+ );
+ this.setAvailableViews = this.setAvailableViews.bind(this);
+
+ for (const button of $$("toolbarbutton[data-view]", this.toolbar)) {
+ button.addEventListener("command", this._onViewToggle);
+ }
+
+ await this.setAvailableViews();
+
+ PerformanceController.on(
+ EVENTS.RECORDING_STATE_CHANGE,
+ this._onRecordingStoppedOrSelected
+ );
+ PerformanceController.on(
+ EVENTS.RECORDING_SELECTED,
+ this._onRecordingStoppedOrSelected
+ );
+ PerformanceController.on(EVENTS.PREF_CHANGED, this.setAvailableViews);
+ },
+
+ /**
+ * Unbinds events, destroys subviews.
+ */
+ async destroy() {
+ for (const button of $$("toolbarbutton[data-view]", this.toolbar)) {
+ button.removeEventListener("command", this._onViewToggle);
+ }
+
+ for (const component of Object.values(this.components)) {
+ component.initialized && (await component.view.destroy());
+ }
+
+ PerformanceController.off(
+ EVENTS.RECORDING_STATE_CHANGE,
+ this._onRecordingStoppedOrSelected
+ );
+ PerformanceController.off(
+ EVENTS.RECORDING_SELECTED,
+ this._onRecordingStoppedOrSelected
+ );
+ PerformanceController.off(EVENTS.PREF_CHANGED, this.setAvailableViews);
+ },
+
+ /**
+ * Sets the possible views based off of recording features and server actor support
+ * by hiding/showing the buttons that select them and going to default view
+ * if currently selected. Called when a preference changes in
+ * `devtools.performance.ui.`.
+ */
+ async setAvailableViews() {
+ const recording = PerformanceController.getCurrentRecording();
+ const isCompleted = recording && recording.isCompleted();
+ let invalidCurrentView = false;
+
+ for (const [name, { view }] of Object.entries(this.components)) {
+ const isSupported = this._isViewSupported(name);
+
+ $(`toolbarbutton[data-view=${name}]`).hidden = !isSupported;
+
+ // If the view is currently selected and not supported, go back to the
+ // default view.
+ if (!isSupported && this.isViewSelected(view)) {
+ invalidCurrentView = true;
+ }
+ }
+
+ // Two scenarios in which we select the default view.
+ //
+ // 1: If we currently have selected a view that is no longer valid due
+ // to feature support, and this isn't the first view, and the current recording
+ // is completed.
+ //
+ // 2. If we have a finished recording and no panel was selected yet,
+ // use a default now that we have the recording configurations
+ if (
+ (this._initialized && isCompleted && invalidCurrentView) ||
+ (!this._initialized && isCompleted && recording)
+ ) {
+ await this.selectDefaultView();
+ }
+ },
+
+ /**
+ * Takes a view name and determines if the current recording
+ * can support the view.
+ *
+ * @param {string} viewName
+ * @return {boolean}
+ */
+ _isViewSupported: function(viewName) {
+ const { features, prefs } = this.components[viewName];
+ const recording = PerformanceController.getCurrentRecording();
+
+ if (!recording || !recording.isCompleted()) {
+ return false;
+ }
+
+ const prefSupported = prefs?.length
+ ? prefs.every(p => PerformanceController.getPref(p))
+ : true;
+ return PerformanceController.isFeatureSupported(features) && prefSupported;
+ },
+
+ /**
+ * Select one of the DetailView's subviews to be rendered,
+ * hiding the others.
+ *
+ * @param String viewName
+ * Name of the view to be shown.
+ */
+ async selectView(viewName) {
+ const component = this.components[viewName];
+ this.el.selectedPanel = $("#" + component.id);
+
+ await this._whenViewInitialized(component);
+
+ for (const button of $$("toolbarbutton[data-view]", this.toolbar)) {
+ if (button.getAttribute("data-view") === viewName) {
+ button.setAttribute("checked", true);
+ } else {
+ button.removeAttribute("checked");
+ }
+ }
+
+ // Set a flag indicating that a view was explicitly set based on a
+ // recording's features.
+ this._initialized = true;
+
+ this.emit(EVENTS.UI_DETAILS_VIEW_SELECTED, viewName);
+ },
+
+ /**
+ * Selects a default view based off of protocol support
+ * and preferences enabled.
+ */
+ selectDefaultView: function() {
+ // We want the waterfall to be default view in almost all cases, except when
+ // timeline actor isn't supported, or we have markers disabled (which should only
+ // occur temporarily via bug 1156499
+ if (this._isViewSupported("waterfall")) {
+ return this.selectView("waterfall");
+ }
+ // The JS CallTree should always be supported since the profiler
+ // actor is as old as the world.
+ return this.selectView("js-calltree");
+ },
+
+ /**
+ * Checks if the provided view is currently selected.
+ *
+ * @param object viewObject
+ * @return boolean
+ */
+ isViewSelected: function(viewObject) {
+ // If not initialized, and we have no recordings,
+ // no views are selected (even though there's a selected panel)
+ if (!this._initialized) {
+ return false;
+ }
+
+ const selectedPanel = this.el.selectedPanel;
+ const selectedId = selectedPanel.id;
+
+ for (const { id, view } of Object.values(this.components)) {
+ if (id == selectedId && view == viewObject) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ /**
+ * Initializes a subview if it wasn't already set up, and makes sure
+ * it's populated with recording data if there is some available.
+ *
+ * @param object component
+ * A component descriptor from DetailsView.components
+ */
+ async _whenViewInitialized(component) {
+ if (component.initialized) {
+ return;
+ }
+ component.initialized = true;
+ await component.view.initialize();
+
+ // If this view is initialized *after* a recording is shown, it won't display
+ // any data. Make sure it's populated by setting `shouldUpdateWhenShown`.
+ // All detail views require a recording to be complete, so do not
+ // attempt to render if recording is in progress or does not exist.
+ const recording = PerformanceController.getCurrentRecording();
+ if (recording && recording.isCompleted()) {
+ component.view.shouldUpdateWhenShown = true;
+ }
+ },
+
+ /**
+ * Called when recording stops or is selected.
+ */
+ _onRecordingStoppedOrSelected: function(state, recording) {
+ if (typeof state === "string" && state !== "recording-stopped") {
+ return;
+ }
+ this.setAvailableViews();
+ },
+
+ /**
+ * Called when a view button is clicked.
+ */
+ _onViewToggle: function(e) {
+ this.selectView(e.target.getAttribute("data-view"));
+ },
+
+ toString: () => "[object DetailsView]",
+};
+
+/**
+ * Convenient way of emitting events from the view.
+ */
+EventEmitter.decorate(DetailsView);
+
+exports.DetailsView = DetailsView;
diff --git a/devtools/client/performance/views/moz.build b/devtools/client/performance/views/moz.build
new file mode 100644
index 0000000000..d10fbf3dac
--- /dev/null
+++ b/devtools/client/performance/views/moz.build
@@ -0,0 +1,17 @@
+# 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(
+ "details-abstract-subview.js",
+ "details-js-call-tree.js",
+ "details-js-flamegraph.js",
+ "details-memory-call-tree.js",
+ "details-memory-flamegraph.js",
+ "details-waterfall.js",
+ "details.js",
+ "overview.js",
+ "recordings.js",
+ "toolbar.js",
+)
diff --git a/devtools/client/performance/views/overview.js b/devtools/client/performance/views/overview.js
new file mode 100644
index 0000000000..9257a48fff
--- /dev/null
+++ b/devtools/client/performance/views/overview.js
@@ -0,0 +1,457 @@
+/* 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/. */
+/* globals $ PerformanceController */
+"use strict";
+
+// No sense updating the overview more often than receiving data from the
+// backend. Make sure this isn't lower than DEFAULT_TIMELINE_DATA_PULL_TIMEOUT
+// in devtools/server/actors/timeline.js
+
+const EVENTS = require("devtools/client/performance/events");
+const {
+ GraphsController,
+} = require("devtools/client/performance/modules/widgets/graphs");
+
+const EventEmitter = require("devtools/shared/event-emitter");
+
+// The following units are in milliseconds.
+const OVERVIEW_UPDATE_INTERVAL = 200;
+const FRAMERATE_GRAPH_LOW_RES_INTERVAL = 100;
+const FRAMERATE_GRAPH_HIGH_RES_INTERVAL = 16;
+const GRAPH_REQUIREMENTS = {
+ timeline: {
+ features: ["withMarkers"],
+ },
+ framerate: {
+ features: ["withTicks"],
+ },
+ memory: {
+ features: ["withMemory"],
+ },
+};
+
+/**
+ * View handler for the overview panel's time view, displaying
+ * framerate, timeline and memory over time.
+ */
+const OverviewView = {
+ /**
+ * How frequently we attempt to render the graphs. Overridden
+ * in tests.
+ */
+ OVERVIEW_UPDATE_INTERVAL: OVERVIEW_UPDATE_INTERVAL,
+
+ /**
+ * Sets up the view with event binding.
+ */
+ initialize: function() {
+ this.graphs = new GraphsController({
+ root: $("#overview-pane"),
+ getFilter: () => PerformanceController.getPref("hidden-markers"),
+ getTheme: () => PerformanceController.getTheme(),
+ });
+
+ // If no timeline support, shut it all down.
+ if (!PerformanceController.getTraits().features.withMarkers) {
+ this.disable();
+ return;
+ }
+
+ // Store info on multiprocess support.
+ this._multiprocessData = PerformanceController.getMultiprocessStatus();
+
+ this._onRecordingStateChange = this._onRecordingStateChange.bind(this);
+ this._onRecordingSelected = this._onRecordingSelected.bind(this);
+ this._onRecordingTick = this._onRecordingTick.bind(this);
+ this._onGraphSelecting = this._onGraphSelecting.bind(this);
+ this._onGraphRendered = this._onGraphRendered.bind(this);
+ this._onPrefChanged = this._onPrefChanged.bind(this);
+ this._onThemeChanged = this._onThemeChanged.bind(this);
+
+ // Toggle the initial visibility of memory and framerate graph containers
+ // based off of prefs.
+ PerformanceController.on(EVENTS.PREF_CHANGED, this._onPrefChanged);
+ PerformanceController.on(EVENTS.THEME_CHANGED, this._onThemeChanged);
+ PerformanceController.on(
+ EVENTS.RECORDING_STATE_CHANGE,
+ this._onRecordingStateChange
+ );
+ PerformanceController.on(
+ EVENTS.RECORDING_SELECTED,
+ this._onRecordingSelected
+ );
+ this.graphs.on("selecting", this._onGraphSelecting);
+ this.graphs.on("rendered", this._onGraphRendered);
+ },
+
+ /**
+ * Unbinds events.
+ */
+ async destroy() {
+ PerformanceController.off(EVENTS.PREF_CHANGED, this._onPrefChanged);
+ PerformanceController.off(EVENTS.THEME_CHANGED, this._onThemeChanged);
+ PerformanceController.off(
+ EVENTS.RECORDING_STATE_CHANGE,
+ this._onRecordingStateChange
+ );
+ PerformanceController.off(
+ EVENTS.RECORDING_SELECTED,
+ this._onRecordingSelected
+ );
+ this.graphs.off("selecting", this._onGraphSelecting);
+ this.graphs.off("rendered", this._onGraphRendered);
+ await this.graphs.destroy();
+ },
+
+ /**
+ * Returns true if any of the overview graphs have mouse dragging active,
+ * false otherwise.
+ */
+ get isMouseActive() {
+ // Fetch all graphs currently stored in the GraphsController.
+ // These graphs are not necessarily active, but will not have
+ // an active mouse, in that case.
+ return !!this.graphs.getWidgets().some(e => e.isMouseActive);
+ },
+
+ /**
+ * Disabled in the event we're using a Timeline mock, so we'll have no
+ * timeline, ticks or memory data to show, so just block rendering and hide
+ * the panel.
+ */
+ disable: function() {
+ this._disabled = true;
+ this.graphs.disableAll();
+ },
+
+ /**
+ * Returns the disabled status.
+ *
+ * @return boolean
+ */
+ isDisabled: function() {
+ return this._disabled;
+ },
+
+ /**
+ * Sets the time interval selection for all graphs in this overview.
+ *
+ * @param object interval
+ * The { startTime, endTime }, in milliseconds.
+ */
+ setTimeInterval: function(interval, options = {}) {
+ const recording = PerformanceController.getCurrentRecording();
+ if (recording == null) {
+ throw new Error(
+ "A recording should be available in order to set the selection."
+ );
+ }
+ if (this.isDisabled()) {
+ return;
+ }
+ const mapStart = () => 0;
+ const mapEnd = () => recording.getDuration();
+ const selection = { start: interval.startTime, end: interval.endTime };
+ this._stopSelectionChangeEventPropagation = options.stopPropagation;
+ this.graphs.setMappedSelection(selection, { mapStart, mapEnd });
+ this._stopSelectionChangeEventPropagation = false;
+ },
+
+ /**
+ * Gets the time interval selection for all graphs in this overview.
+ *
+ * @return object
+ * The { startTime, endTime }, in milliseconds.
+ */
+ getTimeInterval: function() {
+ const recording = PerformanceController.getCurrentRecording();
+ if (recording == null) {
+ throw new Error(
+ "A recording should be available in order to get the selection."
+ );
+ }
+ if (this.isDisabled()) {
+ return { startTime: 0, endTime: recording.getDuration() };
+ }
+ const mapStart = () => 0;
+ const mapEnd = () => recording.getDuration();
+ const selection = this.graphs.getMappedSelection({ mapStart, mapEnd });
+ // If no selection returned, this means the overview graphs have not been rendered
+ // yet, so act as if we have no selection (the full recording). Also
+ // if the selection range distance is tiny, assume the range was cleared or just
+ // clicked, and we do not have a range.
+ if (!selection || selection.max - selection.min < 1) {
+ return { startTime: 0, endTime: recording.getDuration() };
+ }
+ return { startTime: selection.min, endTime: selection.max };
+ },
+
+ /**
+ * Method for handling all the set up for rendering the overview graphs.
+ *
+ * @param number resolution
+ * The fps graph resolution. @see Graphs.js
+ */
+ async render(resolution) {
+ if (this.isDisabled()) {
+ return;
+ }
+
+ const recording = PerformanceController.getCurrentRecording();
+ await this.graphs.render(recording.getAllData(), resolution);
+
+ // Finished rendering all graphs in this overview.
+ this.emit(EVENTS.UI_OVERVIEW_RENDERED, resolution);
+ },
+
+ /**
+ * Called at most every OVERVIEW_UPDATE_INTERVAL milliseconds
+ * and uses data fetched from the controller to render
+ * data into all the corresponding overview graphs.
+ */
+ async _onRecordingTick() {
+ await this.render(FRAMERATE_GRAPH_LOW_RES_INTERVAL);
+ this._prepareNextTick();
+ },
+
+ /**
+ * Called to refresh the timer to keep firing _onRecordingTick.
+ */
+ _prepareNextTick: function() {
+ // Check here to see if there's still a _timeoutId, incase
+ // `stop` was called before the _prepareNextTick call was executed.
+ if (this.isRendering()) {
+ this._timeoutId = setTimeout(
+ this._onRecordingTick,
+ this.OVERVIEW_UPDATE_INTERVAL
+ );
+ }
+ },
+
+ /**
+ * Called when recording state changes.
+ */
+ _onRecordingStateChange: OverviewViewOnStateChange(async function(
+ state,
+ recording
+ ) {
+ if (state !== "recording-stopped") {
+ return;
+ }
+ // Check to see if the recording that just stopped is the current recording.
+ // If it is, render the high-res graphs. For manual recordings, it will also
+ // be the current recording, but profiles generated by `console.profile` can stop
+ // while having another profile selected -- in this case, OverviewView should keep
+ // rendering the current recording.
+ if (recording !== PerformanceController.getCurrentRecording()) {
+ return;
+ }
+ await this.render(FRAMERATE_GRAPH_HIGH_RES_INTERVAL);
+ await this._checkSelection(recording);
+ }),
+
+ /**
+ * Called when a new recording is selected.
+ */
+ _onRecordingSelected: OverviewViewOnStateChange(async function(recording) {
+ this._setGraphVisibilityFromRecordingFeatures(recording);
+
+ // If this recording is complete, render the high res graph
+ if (recording.isCompleted()) {
+ await this.render(FRAMERATE_GRAPH_HIGH_RES_INTERVAL);
+ }
+ await this._checkSelection(recording);
+ this.graphs.dropSelection();
+ }),
+
+ /**
+ * Start the polling for rendering the overview graph.
+ */
+ _startPolling: function() {
+ this._timeoutId = setTimeout(
+ this._onRecordingTick,
+ this.OVERVIEW_UPDATE_INTERVAL
+ );
+ },
+
+ /**
+ * Stop the polling for rendering the overview graph.
+ */
+ _stopPolling: function() {
+ clearTimeout(this._timeoutId);
+ this._timeoutId = null;
+ },
+
+ /**
+ * Whether or not the overview view is in a state of polling rendering.
+ */
+ isRendering: function() {
+ return !!this._timeoutId;
+ },
+
+ /**
+ * Makes sure the selection is enabled or disabled in all the graphs,
+ * based on whether a recording currently exists and is not in progress.
+ */
+ async _checkSelection(recording) {
+ const isEnabled = recording ? recording.isCompleted() : false;
+ await this.graphs.selectionEnabled(isEnabled);
+ },
+
+ /**
+ * Fired when the graph selection has changed. Called by
+ * mouseup and scroll events.
+ */
+ _onGraphSelecting: function() {
+ if (this._stopSelectionChangeEventPropagation) {
+ return;
+ }
+
+ this.emit(EVENTS.UI_OVERVIEW_RANGE_SELECTED, this.getTimeInterval());
+ },
+
+ _onGraphRendered: function(graphName) {
+ switch (graphName) {
+ case "timeline":
+ this.emit(EVENTS.UI_MARKERS_GRAPH_RENDERED);
+ break;
+ case "memory":
+ this.emit(EVENTS.UI_MEMORY_GRAPH_RENDERED);
+ break;
+ case "framerate":
+ this.emit(EVENTS.UI_FRAMERATE_GRAPH_RENDERED);
+ break;
+ }
+ },
+
+ /**
+ * Called whenever a preference in `devtools.performance.ui.` changes.
+ * Does not care about the enabling of memory/framerate graphs,
+ * because those will set values on a recording model, and
+ * the graphs will render based on the existence.
+ */
+ async _onPrefChanged(prefName, prefValue) {
+ switch (prefName) {
+ case "hidden-markers": {
+ const graph = await this.graphs.isAvailable("timeline");
+ if (graph) {
+ const filter = PerformanceController.getPref("hidden-markers");
+ graph.setFilter(filter);
+ graph.refresh({ force: true });
+ }
+ break;
+ }
+ }
+ },
+
+ _setGraphVisibilityFromRecordingFeatures: function(recording) {
+ for (const [graphName, requirements] of Object.entries(
+ GRAPH_REQUIREMENTS
+ )) {
+ this.graphs.enable(
+ graphName,
+ PerformanceController.isFeatureSupported(requirements.features)
+ );
+ }
+ },
+
+ /**
+ * Fetch the multiprocess status and if e10s is not currently on, disable
+ * realtime rendering.
+ *
+ * @return {boolean}
+ */
+ isRealtimeRenderingEnabled: function() {
+ return this._multiprocessData.enabled;
+ },
+
+ /**
+ * Show the graphs overview panel when a recording is finished
+ * when non-realtime graphs are enabled. Also set the graph visibility
+ * so the performance graphs know which graphs to render.
+ *
+ * @param {RecordingModel} recording
+ */
+ _showGraphsPanel: function(recording) {
+ this._setGraphVisibilityFromRecordingFeatures(recording);
+ $("#overview-pane").classList.remove("hidden");
+ },
+
+ /**
+ * Hide the graphs container completely.
+ */
+ _hideGraphsPanel: function() {
+ $("#overview-pane").classList.add("hidden");
+ },
+
+ /**
+ * Called when `devtools.theme` changes.
+ */
+ _onThemeChanged: function(theme) {
+ this.graphs.setTheme({ theme, redraw: true });
+ },
+
+ toString: () => "[object OverviewView]",
+};
+
+/**
+ * Utility that can wrap a method of OverviewView that
+ * handles a recording state change like when a recording is starting,
+ * stopping, or about to start/stop, and determines whether or not
+ * the polling for rendering the overview graphs needs to start or stop.
+ * Must be called with the OverviewView context.
+ *
+ * @param {function?} fn
+ * @return {function}
+ */
+function OverviewViewOnStateChange(fn) {
+ return function _onRecordingStateChange(recording) {
+ // Normalize arguments for the RECORDING_STATE_CHANGE event,
+ // as it also has a `state` argument.
+ if (typeof recording === "string") {
+ recording = arguments[1];
+ }
+
+ const currentRecording = PerformanceController.getCurrentRecording();
+
+ // All these methods require a recording to exist selected and
+ // from the event name, since there is a delay between starting
+ // a recording and changing the selection.
+ if (!currentRecording || !recording) {
+ // If no recording (this can occur when having a console.profile recording, and
+ // we do not stop it from the backend), and we are still rendering updates,
+ // stop that.
+ if (this.isRendering()) {
+ this._stopPolling();
+ }
+ return;
+ }
+
+ // If realtime rendering is not enabed (e10s not on), then
+ // show the disabled message, or the full graphs if the recording is completed
+ if (!this.isRealtimeRenderingEnabled()) {
+ if (recording.isRecording()) {
+ this._hideGraphsPanel();
+ // Abort, as we do not want to change polling status.
+ return;
+ }
+ this._showGraphsPanel(recording);
+ }
+
+ if (this.isRendering() && !currentRecording.isRecording()) {
+ this._stopPolling();
+ } else if (currentRecording.isRecording() && !this.isRendering()) {
+ this._startPolling();
+ }
+
+ if (fn) {
+ fn.apply(this, arguments);
+ }
+ };
+}
+
+// Decorates the OverviewView as an EventEmitter
+EventEmitter.decorate(OverviewView);
+
+exports.OverviewView = OverviewView;
diff --git a/devtools/client/performance/views/recordings.js b/devtools/client/performance/views/recordings.js
new file mode 100644
index 0000000000..d73b1ac083
--- /dev/null
+++ b/devtools/client/performance/views/recordings.js
@@ -0,0 +1,249 @@
+/* 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/. */
+/* globals $, PerformanceController */
+"use strict";
+
+const EVENTS = require("devtools/client/performance/events");
+const { L10N } = require("devtools/client/performance/modules/global");
+
+const PerformanceUtils = require("devtools/client/performance/modules/utils");
+
+const React = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+const RecordingList = React.createFactory(
+ require("devtools/client/performance/components/RecordingList")
+);
+const RecordingListItem = React.createFactory(
+ require("devtools/client/performance/components/RecordingListItem")
+);
+
+const EventEmitter = require("devtools/shared/event-emitter");
+
+/**
+ * Functions handling the recordings UI.
+ */
+const RecordingsView = {
+ /**
+ * Initialization function, called when the tool is started.
+ */
+ initialize: function() {
+ this._onSelect = this._onSelect.bind(this);
+ this._onRecordingStateChange = this._onRecordingStateChange.bind(this);
+ this._onNewRecording = this._onNewRecording.bind(this);
+ this._onSaveButtonClick = this._onSaveButtonClick.bind(this);
+ this._onRecordingDeleted = this._onRecordingDeleted.bind(this);
+ this._onRecordingExported = this._onRecordingExported.bind(this);
+
+ PerformanceController.on(
+ EVENTS.RECORDING_STATE_CHANGE,
+ this._onRecordingStateChange
+ );
+ PerformanceController.on(EVENTS.RECORDING_ADDED, this._onNewRecording);
+ PerformanceController.on(
+ EVENTS.RECORDING_DELETED,
+ this._onRecordingDeleted
+ );
+ PerformanceController.on(
+ EVENTS.RECORDING_EXPORTED,
+ this._onRecordingExported
+ );
+
+ // DE-XUL: Begin migrating the recording sidebar to React. Temporarily hold state
+ // here.
+ this._listState = {
+ recordings: [],
+ labels: new WeakMap(),
+ selected: null,
+ };
+ this._listMount = PerformanceUtils.createHtmlMount(
+ $("#recording-list-mount")
+ );
+ this._renderList();
+ },
+
+ /**
+ * Get the index of the currently selected recording. Only used by tests.
+ * @return {integer} index
+ */
+ getSelectedIndex() {
+ const { recordings, selected } = this._listState;
+ return recordings.indexOf(selected);
+ },
+
+ /**
+ * Set the currently selected recording via its index. Only used by tests.
+ * @param {integer} index
+ */
+ setSelectedByIndex(index) {
+ this._onSelect(this._listState.recordings[index]);
+ this._renderList();
+ },
+
+ /**
+ * DE-XUL: During the migration, this getter will access the selected recording from
+ * the private _listState object so that tests will continue to pass.
+ */
+ get selected() {
+ return this._listState.selected;
+ },
+
+ /**
+ * DE-XUL: During the migration, this getter will access the number of recordings.
+ */
+ get itemCount() {
+ return this._listState.recordings.length;
+ },
+
+ /**
+ * DE-XUL: Render the recording list using React.
+ */
+ _renderList: function() {
+ const { recordings, labels, selected } = this._listState;
+
+ const recordingList = RecordingList({
+ itemComponent: RecordingListItem,
+ items: recordings.map(recording => ({
+ onSelect: () => this._onSelect(recording),
+ onSave: () => this._onSaveButtonClick(recording),
+ isLoading: !recording.isRecording() && !recording.isCompleted(),
+ isRecording: recording.isRecording(),
+ isSelected: recording === selected,
+ duration: recording.getDuration().toFixed(0),
+ label: labels.get(recording),
+ })),
+ });
+
+ ReactDOM.render(recordingList, this._listMount);
+ },
+
+ /**
+ * Destruction function, called when the tool is closed.
+ */
+ destroy: function() {
+ PerformanceController.off(
+ EVENTS.RECORDING_STATE_CHANGE,
+ this._onRecordingStateChange
+ );
+ PerformanceController.off(EVENTS.RECORDING_ADDED, this._onNewRecording);
+ PerformanceController.off(
+ EVENTS.RECORDING_DELETED,
+ this._onRecordingDeleted
+ );
+ PerformanceController.off(
+ EVENTS.RECORDING_EXPORTED,
+ this._onRecordingExported
+ );
+ },
+
+ /**
+ * Called when a new recording is stored in the UI. This handles
+ * when recordings are lazily loaded (like a console.profile occurring
+ * before the tool is loaded) or imported. In normal manual recording cases,
+ * this will also be fired.
+ */
+ _onNewRecording: function(recording) {
+ this._onRecordingStateChange(null, recording);
+ },
+
+ /**
+ * Signals that a recording has changed state.
+ *
+ * @param string state
+ * Can be "recording-started", "recording-stopped", "recording-stopping"
+ * @param RecordingModel recording
+ * Model of the recording that was started.
+ */
+ _onRecordingStateChange: function(state, recording) {
+ const { recordings, labels } = this._listState;
+
+ if (!recordings.includes(recording)) {
+ recordings.push(recording);
+ labels.set(
+ recording,
+ recording.getLabel() ||
+ L10N.getFormatStr("recordingsList.itemLabel", recordings.length)
+ );
+
+ // If this is a manual recording, immediately select it, or
+ // select a console profile if its the only one
+ if (!recording.isConsole() || !this._listState.selected) {
+ this._onSelect(recording);
+ }
+ }
+
+ // Determine if the recording needs to be selected.
+ const isCompletedManualRecording =
+ !recording.isConsole() && recording.isCompleted();
+ if (recording.isImported() || isCompletedManualRecording) {
+ this._onSelect(recording);
+ }
+
+ this._renderList();
+ },
+
+ /**
+ * Clears out all non-console recordings.
+ */
+ _onRecordingDeleted: function(recording) {
+ const { recordings } = this._listState;
+ const index = recordings.indexOf(recording);
+ if (index === -1) {
+ throw new Error("Attempting to remove a recording that doesn't exist.");
+ }
+ recordings.splice(index, 1);
+ this._renderList();
+ },
+
+ /**
+ * The select listener for this container.
+ */
+ async _onSelect(recording) {
+ this._listState.selected = recording;
+ this.emit(EVENTS.UI_RECORDING_SELECTED, recording);
+ this._renderList();
+ },
+
+ /**
+ * The click listener for the "save" button of each item in this container.
+ */
+ _onSaveButtonClick: function(recording) {
+ const fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ fp.init(
+ window,
+ L10N.getStr("recordingsList.saveDialogTitle"),
+ Ci.nsIFilePicker.modeSave
+ );
+ fp.appendFilter(
+ L10N.getStr("recordingsList.saveDialogJSONFilter"),
+ "*.json"
+ );
+ fp.appendFilter(L10N.getStr("recordingsList.saveDialogAllFilter"), "*.*");
+ fp.defaultString = "profile.json";
+
+ fp.open({
+ done: result => {
+ if (result == Ci.nsIFilePicker.returnCancel) {
+ return;
+ }
+ this.emit(EVENTS.UI_EXPORT_RECORDING, recording, fp.file);
+ },
+ });
+ },
+
+ _onRecordingExported: function(recording, file) {
+ if (recording.isConsole()) {
+ return;
+ }
+ const name = file.leafName.replace(/\..+$/, "");
+ this._listState.labels.set(recording, name);
+ this._renderList();
+ },
+};
+
+/**
+ * Convenient way of emitting events from the RecordingsView.
+ */
+EventEmitter.decorate(RecordingsView);
+
+exports.RecordingsView = window.RecordingsView = RecordingsView;
diff --git a/devtools/client/performance/views/toolbar.js b/devtools/client/performance/views/toolbar.js
new file mode 100644
index 0000000000..cd156e4c4d
--- /dev/null
+++ b/devtools/client/performance/views/toolbar.js
@@ -0,0 +1,188 @@
+/* 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/. */
+/* globals $, $$, PerformanceController */
+"use strict";
+
+const EVENTS = require("devtools/client/performance/events");
+const {
+ TIMELINE_BLUEPRINT,
+} = require("devtools/client/performance/modules/markers");
+const {
+ MarkerBlueprintUtils,
+} = require("devtools/client/performance/modules/marker-blueprint-utils");
+
+const { OptionsView } = require("devtools/client/shared/options-view");
+const EventEmitter = require("devtools/shared/event-emitter");
+
+const BRANCH_NAME = "devtools.performance.ui.";
+
+/**
+ * View handler for toolbar events (mostly option toggling and triggering)
+ */
+const ToolbarView = {
+ /**
+ * Sets up the view with event binding.
+ */
+ async initialize() {
+ this._onFilterPopupShowing = this._onFilterPopupShowing.bind(this);
+ this._onFilterPopupHiding = this._onFilterPopupHiding.bind(this);
+ this._onHiddenMarkersChanged = this._onHiddenMarkersChanged.bind(this);
+ this._onPrefChanged = this._onPrefChanged.bind(this);
+ this._popup = $("#performance-options-menupopup");
+
+ this.optionsView = new OptionsView({
+ branchName: BRANCH_NAME,
+ menupopup: this._popup,
+ });
+
+ // Set the visibility of experimental UI options on load
+ // based off of `devtools.performance.ui.experimental` preference
+ const experimentalEnabled = PerformanceController.getOption("experimental");
+ this._toggleExperimentalUI(experimentalEnabled);
+
+ await this.optionsView.initialize();
+ this.optionsView.on("pref-changed", this._onPrefChanged);
+
+ this._buildMarkersFilterPopup();
+ this._updateHiddenMarkersPopup();
+ $("#performance-filter-menupopup").addEventListener(
+ "popupshowing",
+ this._onFilterPopupShowing
+ );
+ $("#performance-filter-menupopup").addEventListener(
+ "popuphiding",
+ this._onFilterPopupHiding
+ );
+ },
+
+ /**
+ * Unbinds events and cleans up view.
+ */
+ destroy: function() {
+ $("#performance-filter-menupopup").removeEventListener(
+ "popupshowing",
+ this._onFilterPopupShowing
+ );
+ $("#performance-filter-menupopup").removeEventListener(
+ "popuphiding",
+ this._onFilterPopupHiding
+ );
+ this._popup = null;
+
+ this.optionsView.off("pref-changed", this._onPrefChanged);
+ this.optionsView.destroy();
+ },
+
+ /**
+ * Creates the timeline markers filter popup.
+ */
+ _buildMarkersFilterPopup: function() {
+ for (const [markerName, markerDetails] of Object.entries(
+ TIMELINE_BLUEPRINT
+ )) {
+ const menuitem = document.createXULElement("menuitem");
+ menuitem.setAttribute("closemenu", "none");
+ menuitem.setAttribute("type", "checkbox");
+ menuitem.setAttribute("align", "center");
+ menuitem.setAttribute("flex", "1");
+ menuitem.setAttribute(
+ "label",
+ MarkerBlueprintUtils.getMarkerGenericName(markerName)
+ );
+ menuitem.setAttribute("marker-type", markerName);
+ menuitem.className = `marker-color-${markerDetails.colorName}`;
+
+ menuitem.addEventListener("command", this._onHiddenMarkersChanged);
+
+ $("#performance-filter-menupopup").appendChild(menuitem);
+ }
+ },
+
+ /**
+ * Updates the menu items checked state in the timeline markers filter popup.
+ */
+ _updateHiddenMarkersPopup: function() {
+ const menuItems = $$("#performance-filter-menupopup menuitem[marker-type]");
+ const hiddenMarkers = PerformanceController.getPref("hidden-markers");
+
+ for (const menuitem of menuItems) {
+ if (~hiddenMarkers.indexOf(menuitem.getAttribute("marker-type"))) {
+ menuitem.removeAttribute("checked");
+ } else {
+ menuitem.setAttribute("checked", "true");
+ }
+ }
+ },
+
+ /**
+ * Fired when `devtools.performance.ui.experimental` is changed, or
+ * during init. Toggles the visibility of experimental performance tool options
+ * in the UI options.
+ *
+ * Sets or removes "experimental-enabled" on the menu and main elements,
+ * hiding or showing all elements with class "experimental-option".
+ *
+ * TODO re-enable "#option-enable-memory" permanently once stable in bug 1163350
+ * TODO re-enable "#option-show-jit-optimizations" permanently once stable in
+ * bug 1163351
+ *
+ * @param {boolean} isEnabled
+ */
+ _toggleExperimentalUI: function(isEnabled) {
+ if (isEnabled) {
+ $(".theme-body").classList.add("experimental-enabled");
+ this._popup.classList.add("experimental-enabled");
+ } else {
+ $(".theme-body").classList.remove("experimental-enabled");
+ this._popup.classList.remove("experimental-enabled");
+ }
+ },
+
+ /**
+ * Fired when the markers filter popup starts to show.
+ */
+ _onFilterPopupShowing: function() {
+ $("#filter-button").setAttribute("open", "true");
+ },
+
+ /**
+ * Fired when the markers filter popup starts to hide.
+ */
+ _onFilterPopupHiding: function() {
+ $("#filter-button").removeAttribute("open");
+ },
+
+ /**
+ * Fired when a menu item in the markers filter popup is checked or unchecked.
+ */
+ _onHiddenMarkersChanged: function() {
+ const checkedMenuItems = $$(
+ "#performance-filter-menupopup menuitem[marker-type]:not([checked])"
+ );
+ const hiddenMarkers = Array.from(checkedMenuItems, e =>
+ e.getAttribute("marker-type")
+ );
+ PerformanceController.setPref("hidden-markers", hiddenMarkers);
+ },
+
+ /**
+ * Fired when a preference changes in the underlying OptionsView.
+ * Propogated by the PerformanceController.
+ */
+ _onPrefChanged: function(prefName) {
+ const value = PerformanceController.getOption(prefName);
+
+ if (prefName === "experimental") {
+ this._toggleExperimentalUI(value);
+ }
+
+ this.emit(EVENTS.UI_PREF_CHANGED, prefName, value);
+ },
+
+ toString: () => "[object ToolbarView]",
+};
+
+EventEmitter.decorate(ToolbarView);
+
+exports.ToolbarView = window.ToolbarView = ToolbarView;