summaryrefslogtreecommitdiffstats
path: root/devtools/client/performance/test
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/performance/test')
-rw-r--r--devtools/client/performance/test/.eslintrc.js6
-rw-r--r--devtools/client/performance/test/browser.ini124
-rw-r--r--devtools/client/performance/test/browser_aaa-run-first-leaktest.js28
-rw-r--r--devtools/client/performance/test/browser_perf-button-states.js76
-rw-r--r--devtools/client/performance/test/browser_perf-calltree-js-categories.js60
-rw-r--r--devtools/client/performance/test/browser_perf-calltree-js-columns.js66
-rw-r--r--devtools/client/performance/test/browser_perf-calltree-js-events.js58
-rw-r--r--devtools/client/performance/test/browser_perf-calltree-memory-columns.js69
-rw-r--r--devtools/client/performance/test/browser_perf-console-record-01.js43
-rw-r--r--devtools/client/performance/test/browser_perf-console-record-02.js70
-rw-r--r--devtools/client/performance/test/browser_perf-console-record-03.js58
-rw-r--r--devtools/client/performance/test/browser_perf-console-record-04.js58
-rw-r--r--devtools/client/performance/test/browser_perf-console-record-05.js92
-rw-r--r--devtools/client/performance/test/browser_perf-console-record-06.js96
-rw-r--r--devtools/client/performance/test/browser_perf-console-record-07.js170
-rw-r--r--devtools/client/performance/test/browser_perf-console-record-08.js268
-rw-r--r--devtools/client/performance/test/browser_perf-console-record-09.js64
-rw-r--r--devtools/client/performance/test/browser_perf-details-01-toggle.js67
-rw-r--r--devtools/client/performance/test/browser_perf-details-02-utility-fun.js59
-rw-r--r--devtools/client/performance/test/browser_perf-details-03-without-allocations.js127
-rw-r--r--devtools/client/performance/test/browser_perf-details-04-toolbar-buttons.js145
-rw-r--r--devtools/client/performance/test/browser_perf-details-05-preserve-view.js50
-rw-r--r--devtools/client/performance/test/browser_perf-details-06-rerender-on-selection.js79
-rw-r--r--devtools/client/performance/test/browser_perf-details-07-bleed-events.js48
-rw-r--r--devtools/client/performance/test/browser_perf-details-render-00-waterfall.js40
-rw-r--r--devtools/client/performance/test/browser_perf-details-render-01-js-calltree.js40
-rw-r--r--devtools/client/performance/test/browser_perf-details-render-02-js-flamegraph.js40
-rw-r--r--devtools/client/performance/test/browser_perf-details-render-03-memory-calltree.js44
-rw-r--r--devtools/client/performance/test/browser_perf-details-render-04-memory-flamegraph.js45
-rw-r--r--devtools/client/performance/test/browser_perf-docload.js43
-rw-r--r--devtools/client/performance/test/browser_perf-gc-snap.js146
-rw-r--r--devtools/client/performance/test/browser_perf-highlighted.js48
-rw-r--r--devtools/client/performance/test/browser_perf-loading-01.js52
-rw-r--r--devtools/client/performance/test/browser_perf-loading-02.js82
-rw-r--r--devtools/client/performance/test/browser_perf-marker-details.js146
-rw-r--r--devtools/client/performance/test/browser_perf-options-01-toggle-throw.js31
-rw-r--r--devtools/client/performance/test/browser_perf-options-02-toggle-throw-alt.js38
-rw-r--r--devtools/client/performance/test/browser_perf-options-03-toggle-meta.js38
-rw-r--r--devtools/client/performance/test/browser_perf-options-enable-framerate-01.js52
-rw-r--r--devtools/client/performance/test/browser_perf-options-enable-framerate-02.js43
-rw-r--r--devtools/client/performance/test/browser_perf-options-enable-memory-01.js58
-rw-r--r--devtools/client/performance/test/browser_perf-options-enable-memory-02.js49
-rw-r--r--devtools/client/performance/test/browser_perf-options-flatten-tree-recursion-01.js73
-rw-r--r--devtools/client/performance/test/browser_perf-options-flatten-tree-recursion-02.js86
-rw-r--r--devtools/client/performance/test/browser_perf-options-invert-call-tree-01.js43
-rw-r--r--devtools/client/performance/test/browser_perf-options-invert-call-tree-02.js45
-rw-r--r--devtools/client/performance/test/browser_perf-options-invert-flame-graph-01.js43
-rw-r--r--devtools/client/performance/test/browser_perf-options-invert-flame-graph-02.js46
-rw-r--r--devtools/client/performance/test/browser_perf-options-propagate-allocations.js36
-rw-r--r--devtools/client/performance/test/browser_perf-options-propagate-profiler.js32
-rw-r--r--devtools/client/performance/test/browser_perf-options-show-idle-blocks-01.js43
-rw-r--r--devtools/client/performance/test/browser_perf-options-show-idle-blocks-02.js45
-rw-r--r--devtools/client/performance/test/browser_perf-options-show-jit-optimizations.js260
-rw-r--r--devtools/client/performance/test/browser_perf-options-show-platform-data-01.js43
-rw-r--r--devtools/client/performance/test/browser_perf-options-show-platform-data-02.js43
-rw-r--r--devtools/client/performance/test/browser_perf-overview-render-01.js34
-rw-r--r--devtools/client/performance/test/browser_perf-overview-render-02.js91
-rw-r--r--devtools/client/performance/test/browser_perf-overview-render-03.js76
-rw-r--r--devtools/client/performance/test/browser_perf-overview-render-04.js74
-rw-r--r--devtools/client/performance/test/browser_perf-overview-selection-01.js71
-rw-r--r--devtools/client/performance/test/browser_perf-overview-selection-02.js73
-rw-r--r--devtools/client/performance/test/browser_perf-overview-selection-03.js82
-rw-r--r--devtools/client/performance/test/browser_perf-overview-time-interval.js73
-rw-r--r--devtools/client/performance/test/browser_perf-private-browsing.js114
-rw-r--r--devtools/client/performance/test/browser_perf-range-changed-render.js81
-rw-r--r--devtools/client/performance/test/browser_perf-recording-notices-01.js45
-rw-r--r--devtools/client/performance/test/browser_perf-recording-notices-02.js65
-rw-r--r--devtools/client/performance/test/browser_perf-recording-notices-03.js135
-rw-r--r--devtools/client/performance/test/browser_perf-recording-notices-04.js66
-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.js45
-rw-r--r--devtools/client/performance/test/browser_perf-recording-selected-02.js58
-rw-r--r--devtools/client/performance/test/browser_perf-recording-selected-03.js44
-rw-r--r--devtools/client/performance/test/browser_perf-recording-selected-04.js59
-rw-r--r--devtools/client/performance/test/browser_perf-recordings-clear-01.js54
-rw-r--r--devtools/client/performance/test/browser_perf-recordings-clear-02.js69
-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.js56
-rw-r--r--devtools/client/performance/test/browser_perf-recordings-io-04.js178
-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.js142
-rw-r--r--devtools/client/performance/test/browser_perf-refresh.js36
-rw-r--r--devtools/client/performance/test/browser_perf-states.js102
-rw-r--r--devtools/client/performance/test/browser_perf-telemetry-01.js53
-rw-r--r--devtools/client/performance/test/browser_perf-telemetry-02.js48
-rw-r--r--devtools/client/performance/test/browser_perf-telemetry-03.js56
-rw-r--r--devtools/client/performance/test/browser_perf-telemetry-04.js50
-rw-r--r--devtools/client/performance/test/browser_perf-theme-toggle.js78
-rw-r--r--devtools/client/performance/test/browser_perf-tree-abstract-01.js154
-rw-r--r--devtools/client/performance/test/browser_perf-tree-abstract-02.js138
-rw-r--r--devtools/client/performance/test/browser_perf-tree-abstract-03.js151
-rw-r--r--devtools/client/performance/test/browser_perf-tree-abstract-04.js35
-rw-r--r--devtools/client/performance/test/browser_perf-tree-abstract-05.js103
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-01.js65
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-02.js148
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-03.js79
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-04.js78
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-05.js36
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-06.js52
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-07.js40
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-08.js109
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-09.js59
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-10.js160
-rw-r--r--devtools/client/performance/test/browser_perf-tree-view-11.js154
-rw-r--r--devtools/client/performance/test/browser_perf-ui-recording.js39
-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.js41
-rw-r--r--devtools/client/performance/test/browser_timeline-waterfall-generic.js105
-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.js97
-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.js93
-rw-r--r--devtools/client/performance/test/helpers/actions.js155
-rw-r--r--devtools/client/performance/test/helpers/dom-utils.js30
-rw-r--r--devtools/client/performance/test/helpers/event-utils.js114
-rw-r--r--devtools/client/performance/test/helpers/input-utils.js75
-rw-r--r--devtools/client/performance/test/helpers/moz.build20
-rw-r--r--devtools/client/performance/test/helpers/panel-utils.js106
-rw-r--r--devtools/client/performance/test/helpers/prefs.js72
-rw-r--r--devtools/client/performance/test/helpers/profiler-mm-utils.js117
-rw-r--r--devtools/client/performance/test/helpers/recording-utils.js54
-rw-r--r--devtools/client/performance/test/helpers/synth-utils.js99
-rw-r--r--devtools/client/performance/test/helpers/tab-utils.js85
-rw-r--r--devtools/client/performance/test/helpers/urls.js6
-rw-r--r--devtools/client/performance/test/helpers/wait-utils.js61
-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/unit/.eslintrc.js6
-rw-r--r--devtools/client/performance/test/unit/head.js46
-rw-r--r--devtools/client/performance/test/unit/test_frame-utils-01.js133
-rw-r--r--devtools/client/performance/test/unit/test_frame-utils-02.js59
-rw-r--r--devtools/client/performance/test/unit/test_jit-graph-data.js209
-rw-r--r--devtools/client/performance/test/unit/test_jit-model-01.js120
-rw-r--r--devtools/client/performance/test/unit/test_jit-model-02.js149
-rw-r--r--devtools/client/performance/test/unit/test_marker-blueprint.js29
-rw-r--r--devtools/client/performance/test/unit/test_marker-utils.js115
-rw-r--r--devtools/client/performance/test/unit/test_perf-utils-allocations-to-samples.js96
-rw-r--r--devtools/client/performance/test/unit/test_profiler-categories.js38
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-01.js160
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-02.js62
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-03.js95
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-04.js91
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-05.js82
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-06.js176
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-07.js101
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-08.js99
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-09.js84
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-10.js153
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-11.js90
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-12.js94
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-13.js86
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-allocations-01.js95
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-allocations-02.js105
-rw-r--r--devtools/client/performance/test/unit/test_waterfall-utils-collapse-01.js71
-rw-r--r--devtools/client/performance/test/unit/test_waterfall-utils-collapse-02.js82
-rw-r--r--devtools/client/performance/test/unit/test_waterfall-utils-collapse-03.js64
-rw-r--r--devtools/client/performance/test/unit/test_waterfall-utils-collapse-04.js103
-rw-r--r--devtools/client/performance/test/unit/test_waterfall-utils-collapse-05.js164
-rw-r--r--devtools/client/performance/test/unit/xpcshell.ini36
166 files changed, 12777 insertions, 0 deletions
diff --git a/devtools/client/performance/test/.eslintrc.js b/devtools/client/performance/test/.eslintrc.js
new file mode 100644
index 000000000..8d15a76d9
--- /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 000000000..1d1954177
--- /dev/null
+++ b/devtools/client/performance/test/browser.ini
@@ -0,0 +1,124 @@
+[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
+
+[browser_aaa-run-first-leaktest.js]
+[browser_perf-button-states.js]
+[browser_perf-calltree-js-categories.js]
+[browser_perf-calltree-js-columns.js]
+[browser_perf-calltree-js-events.js]
+[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-gc-snap.js] TODO bug 1256350
+[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-highlighted.js]
+[browser_perf-loading-01.js]
+[browser_perf-loading-02.js]
+# [browser_perf-marker-details.js] 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] 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] 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]
+[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] TODO bug 1256350
+# [browser_perf-recordings-io-02.js] TODO bug 1256350
+# [browser_perf-recordings-io-03.js] TODO bug 1256350
+# [browser_perf-recordings-io-04.js] TODO bug 1256350
+# [browser_perf-recordings-io-05.js] TODO bug 1256350
+# [browser_perf-recordings-io-06.js] 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] TODO bug 1256350
+[browser_perf-tree-abstract-01.js]
+[browser_perf-tree-abstract-02.js]
+[browser_perf-tree-abstract-03.js]
+[browser_perf-tree-abstract-04.js]
+[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] TODO bug 1256350
+[browser_perf-ui-recording.js]
+# [browser_timeline-filters-01.js] TODO bug 1256350
+# [browser_timeline-filters-02.js] TODO bug 1256350
+[browser_timeline-waterfall-background.js]
+[browser_timeline-waterfall-generic.js]
+# [browser_timeline-waterfall-rerender.js] TODO bug 1256350
+# [browser_timeline-waterfall-sidebar.js] TODO bug 1256350
+# [browser_timeline-waterfall-workers.js] 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 000000000..d3ecef42e
--- /dev/null
+++ b/devtools/client/performance/test/browser_aaa-run-first-leaktest.js
@@ -0,0 +1,28 @@
+/* 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(function* () {
+ let { target, toolbox, panel } = yield 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.");
+
+ ok(panel.panelWin.gTarget, "Should have a target reference on the panel window.");
+ ok(panel.panelWin.gToolbox, "Should have a toolbox reference on the panel window.");
+ ok(panel.panelWin.gFront, "Should have a front reference on the panel window.");
+
+ yield 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 000000000..7f7ca1b2a
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-button-states.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 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { $, $$, EVENTS, PerformanceController, PerformanceView } = panel.panelWin;
+
+ let recordButton = $("#main-record-button");
+
+ checkRecordButtonsStates(false, false);
+
+ let uiStartClick = once(PerformanceView, EVENTS.UI_START_RECORDING);
+ let recordingStarted = once(PerformanceController, EVENTS.RECORDING_STATE_CHANGE, {
+ expectedArgs: { "1": "recording-started" }
+ });
+ let backendStartReady = once(PerformanceController,
+ EVENTS.BACKEND_READY_AFTER_RECORDING_START);
+ let uiStateRecording = once(PerformanceView, EVENTS.UI_STATE_CHANGED, {
+ expectedArgs: { "1": "recording" }
+ });
+
+ click(recordButton);
+ yield uiStartClick;
+
+ checkRecordButtonsStates(true, true);
+
+ yield recordingStarted;
+
+ checkRecordButtonsStates(true, false);
+
+ yield backendStartReady;
+ yield uiStateRecording;
+
+ let uiStopClick = once(PerformanceView, EVENTS.UI_STOP_RECORDING);
+ let recordingStopped = once(PerformanceController, EVENTS.RECORDING_STATE_CHANGE, {
+ expectedArgs: { "1": "recording-stopped" }
+ });
+ let backendStopReady = once(PerformanceController,
+ EVENTS.BACKEND_READY_AFTER_RECORDING_STOP);
+ let uiStateRecorded = once(PerformanceView, EVENTS.UI_STATE_CHANGED, {
+ expectedArgs: { "1": "recorded" }
+ });
+
+ click(recordButton);
+ yield uiStopClick;
+ yield recordingStopped;
+
+ checkRecordButtonsStates(false, false);
+
+ yield backendStopReady;
+ yield uiStateRecorded;
+
+ yield teardownToolboxAndRemoveTab(panel);
+
+ function checkRecordButtonsStates(checked, locked) {
+ for (let 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 000000000..c0710932f
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-calltree-js-categories.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 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, $, $$, DetailsView, JsCallTreeView } = panel.panelWin;
+
+ // Enable platform data to show the categories in the tree.
+ Services.prefs.setBoolPref(UI_SHOW_PLATFORM_DATA_PREF, true);
+
+ yield startRecording(panel);
+ // To show the `Gecko` category in the tree.
+ yield busyWait(100);
+ yield stopRecording(panel);
+
+ let rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ yield DetailsView.selectView("js-calltree");
+ yield 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.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
+
+function geckoCategoryPresent($$) {
+ for (let 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 000000000..5c8b6e2f3
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-calltree-js-columns.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 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { 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);
+
+ yield startRecording(panel);
+ // To show the `busyWait` function in the tree.
+ yield busyWait(100);
+ yield stopRecording(panel);
+
+ let rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ yield DetailsView.selectView("js-calltree");
+ yield 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
+ });
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
+
+function testCells($, $$, visibleCells) {
+ for (let 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 000000000..c93c7f069
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-calltree-js-events.js
@@ -0,0 +1,58 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, $, DetailsView, OverviewView, JsCallTreeView } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ yield DetailsView.selectView("js-calltree");
+ yield rendered;
+
+ // Mock the profile used so we can get a deterministic tree created.
+ let profile = synthesizeProfile();
+ let threadNode = new ThreadNode(profile.threads[0], OverviewView.getTimeInterval());
+ JsCallTreeView._populateCallTree(threadNode);
+ JsCallTreeView.emit(EVENTS.UI_JS_CALL_TREE_RENDERED);
+
+ let 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;
+ let onFocus = () => count++;
+ JsCallTreeView.on("focus", onFocus);
+
+ 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.");
+
+ yield 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 000000000..9eb8a8de9
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-calltree-memory-columns.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 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, $, $$, DetailsView, MemoryCallTreeView } = panel.panelWin;
+
+ // Enable allocations to test.
+ Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(MemoryCallTreeView, EVENTS.UI_MEMORY_CALL_TREE_RENDERED);
+ yield DetailsView.selectView("memory-calltree");
+ yield 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
+ });
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
+
+function testCells($, $$, visibleCells) {
+ for (let 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 000000000..9353c2f9a
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-console-record-01.js
@@ -0,0 +1,43 @@
+/* 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(function* () {
+ let { target, console } = yield initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ yield console.profile("rust");
+ yield console.profileEnd("rust");
+
+ let { panel } = yield initPerformanceInTab({ tab: target.tab });
+ let { PerformanceController, WaterfallView } = panel.panelWin;
+
+ yield waitUntil(() => PerformanceController.getRecordings().length == 1);
+ yield waitUntil(() => WaterfallView.wasRenderedAtLeastOnce);
+
+ 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(), "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.");
+
+ yield 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 000000000..36d0a54d1
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-console-record-02.js
@@ -0,0 +1,70 @@
+/* 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(function* () {
+ let { target, console } = yield initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ yield console.profile("rust");
+ yield console.profile("rust2");
+
+ let { panel } = yield initPerformanceInTab({ tab: target.tab });
+ let { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
+
+ yield waitUntil(() => PerformanceController.getRecordings().length == 2);
+
+ let 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.
+ yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
+ expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
+ });
+
+ let stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true
+ });
+ yield console.profileEnd("rust");
+ yield stopped;
+
+ stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when a finished recording is selected
+ skipWaitingForOverview: true,
+ skipWaitingForSubview: true,
+ });
+ yield console.profileEnd("rust2");
+ yield stopped;
+
+ yield 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 000000000..a12aab5f2
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-console-record-03.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 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(function* () {
+ let { target, console } = yield initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ yield console.profile("rust");
+ yield console.profileEnd("rust");
+ yield console.profile("rust2");
+
+ let { panel } = yield initPerformanceInTab({ tab: target.tab });
+ let { PerformanceController, WaterfallView } = panel.panelWin;
+
+ yield waitUntil(() => PerformanceController.getRecordings().length == 2);
+ yield waitUntil(() => WaterfallView.wasRenderedAtLeastOnce);
+
+ let 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.");
+
+ let stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true,
+ // only emitted when a finished recording is selected
+ skipWaitingForOverview: true,
+ skipWaitingForSubview: true,
+ });
+ yield console.profileEnd("rust2");
+ yield stopped;
+
+ yield 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 000000000..6465bc746
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-console-record-04.js
@@ -0,0 +1,58 @@
+/* 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(function* () {
+ let { target, console } = yield initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { panel } = yield initPerformanceInTab({ tab: target.tab });
+ let { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
+
+ let started = waitForRecordingStartedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true
+ });
+ yield console.profile("rust");
+ yield 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.");
+ 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.
+ yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
+ expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
+ });
+
+ let stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true
+ });
+ yield console.profileEnd("rust");
+ yield stopped;
+
+ yield 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 000000000..373fd5b0f
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-console-record-05.js
@@ -0,0 +1,92 @@
+/* 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(function* () {
+ let { target, console } = yield initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { panel } = yield initPerformanceInTab({ tab: target.tab });
+ let { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
+
+ let started = waitForRecordingStartedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true
+ });
+ yield console.profile("rust");
+ yield 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.
+ yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
+ expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
+ });
+
+ let stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true
+ });
+ yield console.profileEnd("rust");
+ yield 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,
+ });
+ yield console.profile("rust");
+ yield 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,
+ });
+ yield console.profileEnd("rust");
+ yield stopped;
+
+ yield 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 000000000..f1057c261
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-console-record-06.js
@@ -0,0 +1,96 @@
+/* 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(function* () {
+ let { target, console } = yield initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { panel } = yield initPerformanceInTab({ tab: target.tab });
+ let { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
+
+ let started = waitForRecordingStartedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true
+ });
+ yield console.profile("rust");
+ yield 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.
+ yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
+ expectedArgs: { "1": 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,
+ });
+ yield console.profile("golang");
+ yield 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.
+ yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
+ expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
+ });
+
+ let stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true
+ });
+ yield console.profileEnd("rust");
+ yield 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,
+ });
+ yield console.profileEnd("golang");
+ yield 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.");
+
+ yield 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 000000000..af8dc5144
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-console-record-07.js
@@ -0,0 +1,170 @@
+/* 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(function* () {
+ let { target, console } = yield initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { panel } = yield initPerformanceInTab({ tab: target.tab });
+ let { PerformanceController } = panel.panelWin;
+
+ let started = waitForRecordingStartedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true
+ });
+ yield console.profile();
+ yield 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,
+ });
+ yield console.profile("1");
+ yield 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,
+ });
+ yield console.profile("2");
+ yield 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,
+ });
+ yield console.profileEnd();
+ yield 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");
+ yield 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,
+ });
+ yield console.profileEnd();
+ yield 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
+ });
+ yield console.profileEnd();
+ yield 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();
+ yield idleWait(1000);
+
+ ok(true, "Calling console.profileEnd() with no argument and no pending recordings " +
+ "does not throw.");
+
+ yield 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 000000000..2ad81c413
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-console-record-08.js
@@ -0,0 +1,268 @@
+/* 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(function* () {
+ // This test seems to take a very long time to finish on Linux VMs.
+ requestLongerTimeout(4);
+
+ let { target, console } = yield initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { panel } = yield initPerformanceInTab({ tab: target.tab });
+ let { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
+
+ info("Recording 1 - Starting console.profile()...");
+ let started = waitForRecordingStartedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true
+ });
+ yield console.profile("rust");
+ yield started;
+ testRecordings(PerformanceController, [
+ CONSOLE + SELECTED + RECORDING
+ ]);
+
+ info("Recording 2 - Starting manual recording...");
+ yield 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,
+ });
+ yield console.profile("3");
+ yield 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,
+ });
+ yield console.profile("4");
+ yield 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,
+ });
+ yield console.profileEnd();
+ yield 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);
+ yield 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.");
+
+ yield 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);
+ yield 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.
+ yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
+ expectedArgs: { "1": 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,
+ });
+ yield console.profileEnd();
+ yield 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.
+ yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
+ expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
+ });
+
+ info("Recording 5 - Start one more manual recording.");
+ yield 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.
+ yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
+ expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
+ });
+
+ info("Recording 5 - Stop manual recording.");
+ yield 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,
+ });
+ yield console.profileEnd();
+ yield stopped;
+ testRecordings(PerformanceController, [
+ CONSOLE,
+ MANUAL,
+ CONSOLE,
+ CONSOLE,
+ MANUAL + SELECTED
+ ]);
+ ok(!OverviewView.isRendering(),
+ "Stop rendering overview when a completed recording is selected.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
+
+function testRecordings(controller, expectedBitFlags) {
+ let recordings = controller.getRecordings();
+ let 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 000000000..06c14faa5
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-console-record-09.js
@@ -0,0 +1,64 @@
+/* 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(function* () {
+ let { target, console } = yield initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { panel } = yield initPerformanceInTab({ tab: target.tab });
+ let { PerformanceController } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ info("Starting console.profile()...");
+ let 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,
+ });
+ yield console.profile();
+ yield started;
+
+ yield 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()...");
+ yield console.profileEnd();
+ yield idleWait(1000);
+
+ ok(true,
+ "Stopping an in-progress console profile after clearing recordings does not throw.");
+
+ yield PerformanceController.clearRecordings();
+ recordings = PerformanceController.getRecordings();
+ is(recordings.length, 0, "No recordings found");
+ is(PerformanceController.getCurrentRecording(), null,
+ "There should be no current recording.");
+
+ yield 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 000000000..8cc7f9e0c
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-01-toggle.js
@@ -0,0 +1,67 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, $, DetailsView } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield 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] = yield 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] = yield 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] = yield viewChanged;
+ is(viewName, "waterfall", "UI_DETAILS_VIEW_SELECTED fired with view name");
+ checkViews(DetailsView, $, "waterfall");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
+
+function checkViews(DetailsView, $, currentView) {
+ for (let viewName in DetailsView.components) {
+ let 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 000000000..5914742dd
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-02-utility-fun.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 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let {
+ EVENTS,
+ DetailsView,
+ WaterfallView,
+ JsCallTreeView,
+ JsFlameGraphView
+ } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield 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);
+ yield DetailsView.selectView("js-calltree");
+ yield 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);
+ yield DetailsView.selectView("js-flamegraph");
+ yield 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);
+ yield DetailsView.selectView("waterfall");
+ yield selected;
+
+ ok(DetailsView.isViewSelected(WaterfallView),
+ "The waterfall view is now selected in the details view.");
+
+ yield 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 000000000..c69c1de9f
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-03-without-allocations.js
@@ -0,0 +1,127 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let {
+ EVENTS,
+ $,
+ DetailsView,
+ WaterfallView,
+ MemoryCallTreeView,
+ MemoryFlameGraphView
+ } = panel.panelWin;
+
+ let flameBtn = $("toolbarbutton[data-view='memory-flamegraph']");
+ let callBtn = $("toolbarbutton[data-view='memory-calltree']");
+
+ // Disable allocations to prevent recording them.
+ Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, false);
+
+ yield startRecording(panel);
+ yield 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.
+ yield startRecording(panel);
+ yield 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");
+ yield selected;
+ yield 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");
+ yield selected;
+ yield 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);
+ yield selected;
+ yield 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);
+ yield 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");
+ yield selected;
+ yield 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");
+ yield selected;
+ yield rendered;
+
+ ok(DetailsView.isViewSelected(MemoryFlameGraphView), "The memory flamegraph view can " +
+ "be selected again after going back to the view with memory data.");
+
+ yield 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 000000000..9dec9fe7c
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-04-toolbar-buttons.js
@@ -0,0 +1,145 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let {
+ EVENTS,
+ $,
+ PerformanceController,
+ WaterfallView
+ } = panel.panelWin;
+
+ let waterfallBtn = $("toolbarbutton[data-view='waterfall']");
+ let jsFlameBtn = $("toolbarbutton[data-view='js-flamegraph']");
+ let jsCallBtn = $("toolbarbutton[data-view='js-calltree']");
+ let memFlameBtn = $("toolbarbutton[data-view='memory-flamegraph']");
+ let 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.");
+
+ yield 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.");
+
+ yield 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.");
+
+ yield 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);
+ yield selected;
+ yield 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);
+ yield 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);
+ yield stopRecording(panel);
+ yield 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.");
+
+ yield 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 000000000..00c71db7e
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-05-preserve-view.js
@@ -0,0 +1,50 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, PerformanceController, DetailsView, JsCallTreeView } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let selected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
+ let rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ yield DetailsView.selectView("js-calltree");
+ yield selected;
+ yield rendered;
+
+ ok(DetailsView.isViewSelected(JsCallTreeView),
+ "The js calltree view is now selected in the details view.");
+
+ let cleared = once(PerformanceController, EVENTS.RECORDING_SELECTED,
+ { expectedArgs: { "1": null } });
+ yield PerformanceController.clearRecordings();
+ yield cleared;
+
+ yield startRecording(panel);
+ yield 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.");
+
+ yield 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 000000000..abe2dfc75
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-06-rerender-on-selection.js
@@ -0,0 +1,79 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let {
+ EVENTS,
+ OverviewView,
+ DetailsView,
+ WaterfallView,
+ JsCallTreeView,
+ JsFlameGraphView
+ } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let waterfallRendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ OverviewView.setTimeInterval({ startTime: 10, endTime: 20 });
+ yield waterfallRendered;
+
+ // Select the call tree to make sure it's initialized and ready to receive
+ // redrawing requests once reselected.
+ let callTreeRendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ yield DetailsView.selectView("js-calltree");
+ yield callTreeRendered;
+
+ // Switch to the flamegraph and perform a scroll over the visualization.
+ // The waterfall and call tree should get rerendered when reselected.
+ let flamegraphRendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ yield DetailsView.selectView("js-flamegraph");
+ yield flamegraphRendered;
+
+ let overviewRangeSelected = once(OverviewView, EVENTS.UI_OVERVIEW_RANGE_SELECTED);
+ let waterfallRerendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ let 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
+ });
+
+ yield overviewRangeSelected;
+ ok(true, "Overview range was changed.");
+
+ yield DetailsView.selectView("waterfall");
+ yield waterfallRerendered;
+ ok(true, "Waterfall rerendered by flame graph changing interval.");
+
+ yield DetailsView.selectView("js-calltree");
+ yield callTreeRerendered;
+ ok(true, "CallTree rerendered by flame graph changing interval.");
+
+ yield 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 000000000..f299aadad
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-07-bleed-events.js
@@ -0,0 +1,48 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, DetailsView, JsCallTreeView } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield 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.
+ let callTreeRendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ yield DetailsView.selectView("js-calltree");
+ yield callTreeRendered;
+
+ let waterfallSelected = once(DetailsView, EVENTS.UI_DETAILS_VIEW_SELECTED);
+ yield DetailsView.selectView("waterfall");
+ yield waterfallSelected;
+
+ once(JsCallTreeView, EVENTS.UI_WATERFALL_RENDERED).then(() =>
+ ok(false, "JsCallTreeView should not receive UI_WATERFALL_RENDERED event."));
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let callTreeRerendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ yield DetailsView.selectView("js-calltree");
+ yield callTreeRerendered;
+
+ ok(true, "Test passed.");
+ yield 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 000000000..5f65fa00d
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-render-00-waterfall.js
@@ -0,0 +1,40 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { DetailsView, WaterfallView } = panel.panelWin;
+
+ yield startRecording(panel);
+ // Already waits for EVENTS.UI_WATERFALL_RENDERED.
+ yield 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.");
+
+ yield startRecording(panel);
+ // Already waits for EVENTS.UI_WATERFALL_RENDERED.
+ yield 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.");
+
+ yield 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 000000000..bc191f2fc
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-render-01-js-calltree.js
@@ -0,0 +1,40 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, DetailsView, JsCallTreeView } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ yield DetailsView.selectView("js-calltree");
+ yield rendered;
+
+ ok(true, "JsCallTreeView rendered after recording is stopped.");
+
+ yield startRecording(panel);
+ yield stopRecording(panel, {
+ expectedViewClass: "JsCallTreeView",
+ expectedViewEvent: "UI_JS_CALL_TREE_RENDERED"
+ });
+
+ ok(true, "JsCallTreeView rendered again after recording completed a second time.");
+
+ yield 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 000000000..e5e74fc7c
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-render-02-js-flamegraph.js
@@ -0,0 +1,40 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, DetailsView, JsFlameGraphView } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ yield DetailsView.selectView("js-flamegraph");
+ yield rendered;
+
+ ok(true, "JsFlameGraphView rendered after recording is stopped.");
+
+ yield startRecording(panel);
+ yield stopRecording(panel, {
+ expectedViewClass: "JsFlameGraphView",
+ expectedViewEvent: "UI_JS_FLAMEGRAPH_RENDERED"
+ });
+
+ ok(true, "JsFlameGraphView rendered again after recording completed a second time.");
+
+ yield 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 000000000..758dea8c6
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-render-03-memory-calltree.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 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, DetailsView, MemoryCallTreeView } = panel.panelWin;
+
+ // Enable allocations to test.
+ Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(MemoryCallTreeView, EVENTS.UI_MEMORY_CALL_TREE_RENDERED);
+ yield DetailsView.selectView("memory-calltree");
+ yield rendered;
+
+ ok(true, "MemoryCallTreeView rendered after recording is stopped.");
+
+ yield startRecording(panel);
+ yield stopRecording(panel, {
+ expectedViewClass: "MemoryCallTreeView",
+ expectedViewEvent: "UI_MEMORY_CALL_TREE_RENDERED"
+ });
+
+ ok(true, "MemoryCallTreeView rendered again after recording completed a second time.");
+
+ yield 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 000000000..119f090e5
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-details-render-04-memory-flamegraph.js
@@ -0,0 +1,45 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, DetailsView, MemoryFlameGraphView } = panel.panelWin;
+
+ // Enable allocations to test.
+ Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
+ yield DetailsView.selectView("memory-flamegraph");
+ yield rendered;
+
+ ok(true, "MemoryFlameGraphView rendered after recording is stopped.");
+
+ yield startRecording(panel);
+ yield stopRecording(panel, {
+ expectedViewClass: "MemoryFlameGraphView",
+ expectedViewEvent: "UI_MEMORY_FLAMEGRAPH_RENDERED"
+ });
+
+ ok(true,
+ "MemoryFlameGraphView rendered again after recording completed a second time.");
+
+ yield 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 000000000..b92a8cfbd
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-docload.js
@@ -0,0 +1,43 @@
+/* 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(function* () {
+ let { panel, target } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { PerformanceController } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield reload(target);
+
+ yield waitUntil(() => {
+ // Wait until we get the necessary markers.
+ let 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;
+ });
+
+ yield stopRecording(panel);
+ yield 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 000000000..57589825e
--- /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
+ */
+function* spawnTest() {
+ let { panel } = yield initPerformance(ALLOCS_URL);
+ let { $, $$, EVENTS, PerformanceController, OverviewView, DetailsView, WaterfallView, MemoryCallTreeView } = panel.panelWin;
+ let EPSILON = 0.00001;
+
+ Services.prefs.setBoolPref(ALLOCATIONS_PREF, true);
+
+ yield startRecording(panel);
+ yield idleWait(1000);
+ yield stopRecording(panel);
+
+ injectGCMarkers(PerformanceController, WaterfallView);
+
+ // Select everything
+ let rendered = WaterfallView.once(EVENTS.UI_WATERFALL_RENDERED);
+ OverviewView.setTimeInterval({ startTime: 0, endTime: Number.MAX_VALUE });
+ yield 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}`);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, targetBar);
+ let showAllocsButton;
+ // On slower machines this can not be found immediately?
+ yield 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);
+ EventUtils.sendMouseEvent({ type: "click" }, showAllocsButton);
+ yield 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 });
+ yield DetailsView.selectView("waterfall");
+ yield 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}`);
+ EventUtils.sendMouseEvent({ type: "mousedown" }, targetBar);
+ // On slower machines this can not be found immediately?
+ yield 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);
+ EventUtils.sendMouseEvent({ type: "click" }, showAllocsButton);
+ yield 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 });
+ yield rendered;
+
+ Services.prefs.setBoolPref(ALLOCATIONS_PREF, false);
+ yield startRecording(panel);
+ rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ yield stopRecording(panel);
+ yield rendered;
+
+ injectGCMarkers(PerformanceController, WaterfallView);
+
+ // Select everything
+ rendered = WaterfallView.once(EVENTS.UI_WATERFALL_RENDERED);
+ OverviewView.setTimeInterval({ startTime: 0, endTime: Number.MAX_VALUE });
+ yield rendered;
+
+ ok(true, "WaterfallView rendered after recording is stopped.");
+
+ bars = $$(".waterfall-marker-bar");
+ gcMarkers = PerformanceController.getCurrentRecording().getMarkers();
+
+ EventUtils.sendMouseEvent({ type: "mousedown" }, bars[0]);
+ showAllocsButton = $("#waterfall-details .custom-button[type='show-allocations']");
+ ok(!showAllocsButton, "No GC buttons when allocations are disabled");
+
+
+ yield 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 000000000..72ad90547
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-highlighted.js
@@ -0,0 +1,48 @@
+/* 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(function* () {
+ let { target, toolbox, console } = yield initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let tab = toolbox.doc.getElementById("toolbox-tab-performance");
+
+ yield console.profile("rust");
+ yield waitUntil(() => tab.hasAttribute("highlighted"));
+
+ ok(tab.hasAttribute("highlighted"), "Performance tab is highlighted during recording " +
+ "from console.profile when unloaded.");
+
+ yield console.profileEnd("rust");
+ yield waitUntil(() => !tab.hasAttribute("highlighted"));
+
+ ok(!tab.hasAttribute("highlighted"),
+ "Performance tab is no longer highlighted when console.profile recording finishes.");
+
+ let { panel } = yield initPerformanceInTab({ tab: target.tab });
+
+ yield startRecording(panel);
+
+ ok(tab.hasAttribute("highlighted"),
+ "Performance tab is highlighted during recording while in performance tool.");
+
+ yield stopRecording(panel);
+
+ ok(!tab.hasAttribute("highlighted"),
+ "Performance tab is no longer highlighted when recording finishes.");
+
+ yield 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 000000000..d732efcae
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-loading-01.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 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, L10N, PerformanceController } = panel.panelWin;
+
+ yield startRecording(panel);
+
+ is(getDurationLabelText(panel, 0),
+ L10N.getStr("recordingsList.recordingLabel"),
+ "The duration node should show the 'recording' message while recording");
+
+ let recordingStopping = once(PerformanceController, EVENTS.RECORDING_STATE_CHANGE, {
+ expectedArgs: { "1": "recording-stopping" }
+ });
+ let recordingStopped = once(PerformanceController, EVENTS.RECORDING_STATE_CHANGE, {
+ expectedArgs: { "1": "recording-stopped" }
+ });
+ let everythingStopped = stopRecording(panel);
+
+ yield recordingStopping;
+ is(getDurationLabelText(panel, 0),
+ L10N.getStr("recordingsList.loadingLabel"),
+ "The duration node should show the 'loading' message while stopping");
+
+ yield 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");
+
+ yield everythingStopped;
+ yield 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 000000000..c860cb7a9
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-loading-02.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 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, $, PerformanceController } = panel.panelWin;
+ let detailsContainer = $("#details-pane-container");
+ let recordingNotice = $("#recording-notice");
+ let loadingNotice = $("#loading-notice");
+ let detailsPane = $("#details-pane");
+
+ yield startRecording(panel);
+
+ is(detailsContainer.selectedPanel, recordingNotice,
+ "The recording-notice is shown while recording.");
+
+ let recordingStopping = once(PerformanceController, EVENTS.RECORDING_STATE_CHANGE, {
+ expectedArgs: { "1": "recording-stopping" }
+ });
+ let recordingStopped = once(PerformanceController, EVENTS.RECORDING_STATE_CHANGE, {
+ expectedArgs: { "1": "recording-stopped" }
+ });
+ let everythingStopped = stopRecording(panel);
+
+ yield recordingStopping;
+ is(detailsContainer.selectedPanel, loadingNotice,
+ "The loading-notice is shown while the record is stopping.");
+
+ yield recordingStopped;
+ is(detailsContainer.selectedPanel, detailsPane,
+ "The details panel is shown after the record has stopped.");
+
+ yield everythingStopped;
+ yield startRecording(panel);
+
+ info("While the 2nd record is still going, switch to the first one.");
+ let recordingSelected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+ setSelectedRecording(panel, 0);
+ yield recordingSelected;
+
+ recordingStopping = once(PerformanceController, EVENTS.RECORDING_STATE_CHANGE, {
+ expectedArgs: { "1": "recording-stopping" }
+ });
+ recordingStopped = once(PerformanceController, EVENTS.RECORDING_STATE_CHANGE, {
+ expectedArgs: { "1": "recording-stopped" }
+ });
+ everythingStopped = stopRecording(panel);
+
+ yield 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.");
+
+ yield 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.");
+
+ yield everythingStopped;
+ yield 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 000000000..8607f269d
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-marker-details.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 if the Marker Details view renders all properties expected
+ * for each marker.
+ */
+
+function* spawnTest() {
+ let { target, panel } = yield 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"
+ ];
+
+ yield startRecording(panel);
+ ok(true, "Recording has started.");
+
+ yield 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));
+ });
+
+ yield 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.indexOf(bar.getAttribute("type")) !== -1);
+ let markers = PerformanceController.getCurrentRecording().getMarkers()
+ .filter(m => MARKER_TYPES.indexOf(m.name) !== -1);
+
+ info(`Got ${bars.length} bars and ${markers.length} markers.`);
+ info("Markers types from datasrc: " + Array.map(markers, e => e.name));
+ info("Markers names from sidebar: " + Array.map(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.restyleHint) {
+ shouldHaveLabel($, "Restyle Hint:", marker.restyleHint.replace(/eRestyle_/g, ""), 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];
+ EventUtils.sendMouseEvent({ type: "mousedown" }, bar);
+
+ if (tests[m.name]) {
+ if (testsDone.indexOf(m.name) === -1) {
+ 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;
+ }
+ }
+
+ yield 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 000000000..4ecfb4152
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-01-toggle-throw.js
@@ -0,0 +1,31 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { DetailsView, JsCallTreeView } = panel.panelWin;
+
+ yield 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.");
+ }
+
+ yield 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 000000000..cdea1556a
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-02-toggle-throw-alt.js
@@ -0,0 +1,38 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { DetailsView, JsCallTreeView } = panel.panelWin;
+
+ yield DetailsView.selectView("js-calltree");
+ yield 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.");
+ }
+
+ yield stopRecording(panel, {
+ expectedViewClass: "JsCallTreeView",
+ expectedViewEvent: "UI_JS_CALL_TREE_RENDERED"
+ });
+
+ yield 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 000000000..384133fff
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-03-toggle-meta.js
@@ -0,0 +1,38 @@
+/* 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(function* () {
+ Services.prefs.setBoolPref(UI_EXPERIMENTAL_PREF, false);
+
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { $ } = panel.panelWin;
+ let $body = $(".theme-body");
+ let $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.");
+
+ yield 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 000000000..ad75db6cf
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-enable-framerate-01.js
@@ -0,0 +1,52 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { $, PerformanceController } = panel.panelWin;
+
+ // Disable framerate to test.
+ Services.prefs.setBoolPref(UI_ENABLE_FRAMERATE_PREF, false);
+
+ yield startRecording(panel);
+ yield 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.");
+
+ yield startRecording(panel);
+ yield 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.");
+
+ yield 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 000000000..b7f870bba
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-enable-framerate-02.js
@@ -0,0 +1,43 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { PerformanceController } = panel.panelWin;
+
+ // Test starting without framerate, and stopping with it.
+ Services.prefs.setBoolPref(UI_ENABLE_FRAMERATE_PREF, false);
+ yield startRecording(panel);
+
+ Services.prefs.setBoolPref(UI_ENABLE_FRAMERATE_PREF, true);
+ yield stopRecording(panel);
+
+ is(PerformanceController.getCurrentRecording().getConfiguration().withTicks, false,
+ "The recording finished without tracking framerate.");
+
+ // Test starting with framerate, and stopping without it.
+ yield startRecording(panel);
+
+ Services.prefs.setBoolPref(UI_ENABLE_FRAMERATE_PREF, false);
+ yield stopRecording(panel);
+
+ is(PerformanceController.getCurrentRecording().getConfiguration().withTicks, true,
+ "The recording finished with tracking framerate.");
+
+ yield 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 000000000..9785d54d6
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-enable-memory-01.js
@@ -0,0 +1,58 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { $, PerformanceController } = panel.panelWin;
+
+ // Disable memory to test.
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, false);
+
+ yield startRecording(panel);
+ yield 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");
+
+ yield startRecording(panel);
+ yield 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.");
+
+ yield 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 000000000..b9c577687
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-enable-memory-02.js
@@ -0,0 +1,49 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { PerformanceController } = panel.panelWin;
+
+ // Test starting without memory, and stopping with it.
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, false);
+ yield startRecording(panel);
+
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, true);
+ yield 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.
+ yield startRecording(panel);
+
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, false);
+ yield 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.");
+
+ yield 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 000000000..9fccd6199
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-flatten-tree-recursion-01.js
@@ -0,0 +1,73 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let {
+ EVENTS,
+ PerformanceController,
+ DetailsView,
+ JsFlameGraphView,
+ FlameGraphUtils
+ } = panel.panelWin;
+
+ Services.prefs.setBoolPref(UI_FLATTEN_RECURSION_PREF, true);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ yield DetailsView.selectView("js-flamegraph");
+ yield rendered;
+
+ let thread1 = PerformanceController.getCurrentRecording().getProfile().threads[0];
+ let 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);
+ yield rendered;
+ ok(true, "JsFlameGraphView rerendered when toggling flatten-tree-recursion.");
+
+ let thread2 = PerformanceController.getCurrentRecording().getProfile().threads[0];
+ let 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);
+ yield rendered;
+ ok(true, "JsFlameGraphView rerendered when toggling back flatten-tree-recursion.");
+
+ let thread3 = PerformanceController.getCurrentRecording().getProfile().threads[0];
+ let 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).");
+
+ yield 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 000000000..509dd0f66
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-flatten-tree-recursion-02.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 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let {
+ EVENTS,
+ PerformanceController,
+ DetailsView,
+ MemoryFlameGraphView,
+ RecordingUtils,
+ FlameGraphUtils
+ } = panel.panelWin;
+
+ // Enable memory to test
+ Services.prefs.setBoolPref(UI_ENABLE_ALLOCATIONS_PREF, true);
+ Services.prefs.setBoolPref(UI_FLATTEN_RECURSION_PREF, true);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
+ yield DetailsView.selectView("memory-flamegraph");
+ yield rendered;
+
+ let allocations1 = PerformanceController.getCurrentRecording().getAllocations();
+ let thread1 = RecordingUtils.getProfileThreadFromAllocations(allocations1);
+ let 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);
+ yield rendered;
+ ok(true, "MemoryFlameGraphView rerendered when toggling flatten-tree-recursion.");
+
+ let allocations2 = PerformanceController.getCurrentRecording().getAllocations();
+ let thread2 = RecordingUtils.getProfileThreadFromAllocations(allocations2);
+ let 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);
+ yield rendered;
+ ok(true, "MemoryFlameGraphView rerendered when toggling back flatten-tree-recursion.");
+
+ let allocations3 = PerformanceController.getCurrentRecording().getAllocations();
+ let thread3 = RecordingUtils.getProfileThreadFromAllocations(allocations3);
+ let 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).");
+
+ yield 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 000000000..cd84e8754
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-invert-call-tree-01.js
@@ -0,0 +1,43 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, DetailsView, JsCallTreeView } = panel.panelWin;
+
+ Services.prefs.setBoolPref(UI_INVERT_CALL_TREE_PREF, true);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ yield DetailsView.selectView("js-calltree");
+ yield rendered;
+
+ rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ Services.prefs.setBoolPref(UI_INVERT_CALL_TREE_PREF, false);
+ yield 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);
+ yield rendered;
+ ok(true, "JsCallTreeView rerendered when toggling back invert-call-tree.");
+
+ yield 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 000000000..ae0c8ede8
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-invert-call-tree-02.js
@@ -0,0 +1,45 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { 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);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(MemoryCallTreeView, EVENTS.UI_MEMORY_CALL_TREE_RENDERED);
+ yield DetailsView.selectView("memory-calltree");
+ yield rendered;
+
+ rendered = once(MemoryCallTreeView, EVENTS.UI_MEMORY_CALL_TREE_RENDERED);
+ Services.prefs.setBoolPref(UI_INVERT_CALL_TREE_PREF, false);
+ yield 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);
+ yield rendered;
+ ok(true, "MemoryCallTreeView rerendered when toggling back invert-call-tree.");
+
+ yield 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 000000000..ee009bacf
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-invert-flame-graph-01.js
@@ -0,0 +1,43 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, DetailsView, JsFlameGraphView } = panel.panelWin;
+
+ Services.prefs.setBoolPref(UI_INVERT_FLAME_PREF, true);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ yield DetailsView.selectView("js-flamegraph");
+ yield rendered;
+
+ rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_INVERT_FLAME_PREF, false);
+ yield 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);
+ yield rendered;
+ ok(true, "JsFlameGraphView rerendered when toggling back invert-call-tree.");
+
+ yield 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 000000000..0a9322547
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-invert-flame-graph-02.js
@@ -0,0 +1,46 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { 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);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
+ yield DetailsView.selectView("memory-flamegraph");
+ yield rendered;
+
+ rendered = once(MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_INVERT_FLAME_PREF, false);
+ yield 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);
+ yield rendered;
+ ok(true, "MemoryFlameGraphView rerendered when toggling back invert-call-tree.");
+
+ yield 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 000000000..509452be4
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-propagate-allocations.js
@@ -0,0 +1,36 @@
+/* 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(function* () {
+ let { panel, toolbox } = yield 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);
+
+ yield startRecording(panel);
+ let { probability, maxLogLength } = yield toolbox.performance.getConfiguration();
+ yield 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.");
+
+ yield 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 000000000..d59233051
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-propagate-profiler.js
@@ -0,0 +1,32 @@
+/* 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(function* () {
+ let { panel, toolbox } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ Services.prefs.setIntPref(PROFILER_BUFFER_SIZE_PREF, 1000);
+ Services.prefs.setIntPref(PROFILER_SAMPLE_RATE_PREF, 2);
+
+ yield startRecording(panel);
+ let { entries, interval } = yield toolbox.performance.getConfiguration();
+ yield stopRecording(panel);
+
+ is(entries, 1000, "profiler entries option is set on profiler");
+ is(interval, 0.5, "profiler interval option is set on profiler");
+
+ yield 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 000000000..8c59ede42
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-show-idle-blocks-01.js
@@ -0,0 +1,43 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, DetailsView, JsFlameGraphView } = panel.panelWin;
+
+ Services.prefs.setBoolPref(UI_SHOW_IDLE_BLOCKS_PREF, true);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ yield DetailsView.selectView("js-flamegraph");
+ yield rendered;
+
+ rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_SHOW_IDLE_BLOCKS_PREF, false);
+ yield 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);
+ yield rendered;
+ ok(true, "JsFlameGraphView rerendered when toggling back show-idle-blocks.");
+
+ yield 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 000000000..3e0146ac7
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-show-idle-blocks-02.js
@@ -0,0 +1,45 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { 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);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
+ yield DetailsView.selectView("memory-flamegraph");
+ yield rendered;
+
+ rendered = once(MemoryFlameGraphView, EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_SHOW_IDLE_BLOCKS_PREF, false);
+ yield 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);
+ yield rendered;
+ ok(true, "MemoryFlameGraphView rerendered when toggling back show-idle-blocks.");
+
+ yield 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 000000000..fd0bbc663
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-show-jit-optimizations.js
@@ -0,0 +1,260 @@
+/* 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);
+
+function* spawnTest() {
+ let { panel } = yield 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
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ yield DetailsView.selectView("js-calltree");
+
+ yield 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.
+ yield checkFrame(1);
+
+ // gRawSite2 and gRawSite3 are both optimizations on B, so they'll have
+ // indices in descending order of # of samples.
+ yield checkFrame(2, true);
+
+ // Leaf node (C) with no optimizations should not display any opts.
+ yield 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");
+ mousedown(window, $$(".call-tree-item")[2]);
+ yield 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);
+ yield 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);
+ yield rendered;
+
+ rendered = once(JsCallTreeView, "focus");
+ mousedown(window, $$(".call-tree-item")[2]);
+ yield 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);
+ yield 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");
+ mousedown(window, $$(".call-tree-item")[2]);
+ yield rendered;
+ isHidden = $("#jit-optimizations-view").classList.contains("hidden");
+ ok(isHidden, "opts view hidden when jit pref off and selecting a frame with opts");
+
+ yield teardown(panel);
+ finish();
+
+ 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());
+ yield rendered;
+ }
+
+ function* checkFrame(frameIndex, hasOpts) {
+ info(`Checking frame ${frameIndex}`);
+ // Click the frame
+ let rendered = once(JsCallTreeView, "focus");
+ mousedown(window, $$(".call-tree-item")[frameIndex]);
+ yield 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 000000000..20e69bd9a
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-show-platform-data-01.js
@@ -0,0 +1,43 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, DetailsView, JsCallTreeView } = panel.panelWin;
+
+ Services.prefs.setBoolPref(UI_SHOW_PLATFORM_DATA_PREF, true);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ yield DetailsView.selectView("js-calltree");
+ yield rendered;
+
+ rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ Services.prefs.setBoolPref(UI_SHOW_PLATFORM_DATA_PREF, false);
+ yield 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);
+ yield rendered;
+ ok(true, "JsCallTreeView rerendered when toggling back show-idle-blocks.");
+
+ yield 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 000000000..df199e797
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-options-show-platform-data-02.js
@@ -0,0 +1,43 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, DetailsView, JsFlameGraphView } = panel.panelWin;
+
+ Services.prefs.setBoolPref(UI_SHOW_PLATFORM_DATA_PREF, true);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ yield DetailsView.selectView("js-flamegraph");
+ yield rendered;
+
+ rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ Services.prefs.setBoolPref(UI_SHOW_PLATFORM_DATA_PREF, false);
+ yield 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);
+ yield rendered;
+ ok(true, "JsFlameGraphView rerendered when toggling back show-idle-blocks.");
+
+ yield 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 000000000..a34ba21ea
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-overview-render-01.js
@@ -0,0 +1,34 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, OverviewView } = panel.panelWin;
+
+ yield startRecording(panel);
+
+ // Ensure overview keeps rendering.
+ yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
+ expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
+ });
+
+ ok(true, "Overview was rendered while recording.");
+
+ yield stopRecording(panel);
+ yield 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 000000000..a7cb7026e
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-overview-render-02.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 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, OverviewView } = panel.panelWin;
+
+ // Enable memory to test.
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, true);
+
+ yield startRecording(panel);
+
+ let framerate = OverviewView.graphs.get("framerate");
+ let markers = OverviewView.graphs.get("timeline");
+ let 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.
+ yield times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, 3, {
+ expectedArgs: { "1": 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.");
+
+ yield 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.");
+
+ yield 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 000000000..e46ce2f91
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-overview-render-03.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 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { PerformanceController, OverviewView } = panel.panelWin;
+
+ // Enable memory to test.
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, true);
+
+ let doChecks = () => {
+ let markers = OverviewView.graphs.get("timeline");
+ let framerate = OverviewView.graphs.get("framerate");
+ let 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.");
+ };
+
+ yield startRecording(panel);
+ doChecks();
+
+ yield waitUntil(() => PerformanceController.getCurrentRecording().getMarkers().length);
+ yield waitUntil(() => PerformanceController.getCurrentRecording().getMemory().length);
+ yield waitUntil(() => PerformanceController.getCurrentRecording().getTicks().length);
+ doChecks();
+
+ yield stopRecording(panel);
+ doChecks();
+
+ yield 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 000000000..22c856851
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-overview-render-04.js
@@ -0,0 +1,74 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { $, 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++);
+
+ yield startRecording(panel, { skipWaitingForOverview: true });
+
+ is(isVisible($("#overview-pane")), false, "Overview graphs hidden.");
+ is(updated, 0, "Overview graphs have not been updated");
+
+ yield waitUntil(() => PerformanceController.getCurrentRecording().getMarkers().length);
+ yield waitUntil(() => PerformanceController.getCurrentRecording().getMemory().length);
+ yield waitUntil(() => PerformanceController.getCurrentRecording().getTicks().length);
+ is(isVisible($("#overview-pane")), false, "Overview graphs still hidden.");
+ is(updated, 0, "Overview graphs have still not been updated");
+
+ yield stopRecording(panel);
+
+ is(isVisible($("#overview-pane")), true, "Overview graphs no longer hidden.");
+ is(updated, 1, "Overview graphs rendered upon completion.");
+
+ yield 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.");
+
+ yield stopRecording(panel);
+
+ is(isVisible($("#overview-pane")), true,
+ "overview graphs no longer hidden when recording finishes");
+ is(updated, 2, "Overview graphs rendered again upon completion.");
+
+ yield 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 000000000..b8a8d730b
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-overview-selection-01.js
@@ -0,0 +1,71 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let duration = PerformanceController.getCurrentRecording().getDuration();
+ let 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 }] = yield 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 }] = yield 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\`.`);
+
+ let mapStart = () => 0;
+ let mapEnd = () => duration;
+ let 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 }] = yield 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`.");
+
+ yield 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 000000000..71b410094
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-overview-selection-02.js
@@ -0,0 +1,73 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { OverviewView } = panel.panelWin;
+
+ // Enable memory to test.
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, true);
+
+ yield startRecording(panel);
+
+ let markersOverview = OverviewView.graphs.get("timeline");
+ let memoryGraph = OverviewView.graphs.get("memory");
+ let 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).");
+
+ yield 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).");
+
+ yield 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).");
+
+ yield 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).");
+
+ yield 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 000000000..8f06901e8
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-overview-selection-03.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 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, OverviewView } = panel.panelWin;
+
+ // Enable memory to test.
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, true);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let markersOverview = OverviewView.graphs.get("timeline");
+ let memoryGraph = OverviewView.graphs.get("memory");
+ let framerateGraph = OverviewView.graphs.get("framerate");
+ let 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 });
+ yield rangeSelected;
+
+ is(markersOverview.getSelection().toSource(), framerateGraph.getSelection().toSource(),
+ "The markers overview has a correct selection.");
+ is(memoryGraph.getSelection().toSource(), framerateGraph.getSelection().toSource(),
+ "The memory overview has a correct selection.");
+ is(framerateGraph.getSelection().toSource(), "({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 });
+ yield rangeSelected;
+
+ is(markersOverview.getSelection().toSource(), framerateGraph.getSelection().toSource(),
+ "The markers overview has a correct selection.");
+ is(memoryGraph.getSelection().toSource(), framerateGraph.getSelection().toSource(),
+ "The memory overview has a correct selection.");
+ is(framerateGraph.getSelection().toSource(), "({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 });
+ yield rangeSelected;
+
+ is(markersOverview.getSelection().toSource(), framerateGraph.getSelection().toSource(),
+ "The markers overview has a correct selection.");
+ is(memoryGraph.getSelection().toSource(), framerateGraph.getSelection().toSource(),
+ "The memory overview has a correct selection.");
+ is(framerateGraph.getSelection().toSource(), "({start:0, end:" + (width / 10) + "})",
+ "The framerate graph has a correct selection.");
+
+ yield 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 000000000..b66e3ef86
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-overview-time-interval.js
@@ -0,0 +1,73 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { 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.");
+ }
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ // Get/set the time interval and wait for the event propagation.
+
+ let rangeSelected = once(OverviewView, EVENTS.UI_OVERVIEW_RANGE_SELECTED);
+ OverviewView.setTimeInterval({ startTime: 10, endTime: 20 });
+ yield rangeSelected;
+
+ let 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);
+
+ let 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.");
+
+ yield 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 000000000..dd2383fd4
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-private-browsing.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 frontend is disabled when in private browsing mode.
+ */
+
+const { SIMPLE_URL } = require("devtools/client/performance/test/helpers/urls");
+const { addWindow } = require("devtools/client/performance/test/helpers/tab-utils");
+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(function* () {
+ yield testNormalWindow();
+ yield testPrivateWindow();
+ yield testRecordingFailingInWindow(0);
+ yield testRecordingFailingInWindow(1);
+ yield teardownPerfInWindow(1, { shouldCloseWindow: true, dontWaitForTabClose: true });
+ yield testRecordingSucceedingInWindow(0);
+ yield teardownPerfInWindow(0, { shouldCloseWindow: false });
+
+ gPanelWinTuples = null;
+});
+
+function* createPanelInNewWindow(options) {
+ let win = yield addWindow(options);
+ return yield createPanelInWindow(options, win);
+}
+
+function* createPanelInWindow(options, win = window) {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: win
+ }, options);
+
+ gPanelWinTuples.push({ panel, win });
+ return { panel, win };
+}
+
+function* testNormalWindow() {
+ let { panel } = yield createPanelInWindow({
+ private: false
+ });
+
+ let { PerformanceView } = panel.panelWin;
+
+ is(PerformanceView.getState(), "empty",
+ "The initial state of the performance panel view is correct (1).");
+}
+
+function* testPrivateWindow() {
+ let { panel } = yield 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
+ });
+
+ let { PerformanceView } = panel.panelWin;
+
+ is(PerformanceView.getState(), "unavailable",
+ "The initial state of the performance panel view is correct (2).");
+}
+
+function* testRecordingFailingInWindow(index) {
+ let { panel } = gPanelWinTuples[index];
+ let { EVENTS, PerformanceController } = panel.panelWin;
+
+ let onRecordingStarted = () => {
+ ok(false, "Recording should not start while a private window is present.");
+ };
+
+ PerformanceController.on(EVENTS.RECORDING_STATE_CHANGE, onRecordingStarted);
+
+ let whenFailed = once(PerformanceController,
+ EVENTS.BACKEND_FAILED_AFTER_RECORDING_START);
+ PerformanceController.startRecording();
+ yield whenFailed;
+ ok(true, "Recording has failed.");
+
+ PerformanceController.off(EVENTS.RECORDING_STATE_CHANGE, onRecordingStarted);
+}
+
+function* testRecordingSucceedingInWindow(index) {
+ let { panel } = gPanelWinTuples[index];
+ let { EVENTS, PerformanceController } = panel.panelWin;
+
+ let onRecordingFailed = () => {
+ ok(false, "Recording should start while now private windows are present.");
+ };
+
+ PerformanceController.on(EVENTS.BACKEND_FAILED_AFTER_RECORDING_START,
+ onRecordingFailed);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+ ok(true, "Recording has succeeded.");
+
+ PerformanceController.off(EVENTS.BACKEND_FAILED_AFTER_RECORDING_START,
+ onRecordingFailed);
+}
+
+function* teardownPerfInWindow(index, options) {
+ let { panel, win } = gPanelWinTuples[index];
+ yield teardownToolboxAndRemoveTab(panel, options);
+
+ 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 000000000..b3b9c6a92
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-range-changed-render.js
@@ -0,0 +1,81 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let {
+ EVENTS,
+ OverviewView,
+ DetailsView,
+ WaterfallView,
+ JsCallTreeView,
+ JsFlameGraphView
+ } = panel.panelWin;
+
+ let updatedWaterfall = 0;
+ let updatedCallTree = 0;
+ let updatedFlameGraph = 0;
+ let updateWaterfall = () => updatedWaterfall++;
+ let updateCallTree = () => updatedCallTree++;
+ let 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);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ OverviewView.emit(EVENTS.UI_OVERVIEW_RANGE_SELECTED, { startTime: 0, endTime: 10 });
+ yield rendered;
+ ok(true, "Waterfall rerenders when a range in the overview graph is selected.");
+
+ rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ yield DetailsView.selectView("js-calltree");
+ yield rendered;
+ ok(true, "Call tree rerenders after its corresponding pane is shown.");
+
+ rendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+ yield DetailsView.selectView("js-flamegraph");
+ yield 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);
+ yield rendered;
+ ok(true, "Flamegraph rerenders when a range in the overview graph is removed.");
+
+ rendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ yield DetailsView.selectView("js-calltree");
+ yield rendered;
+ ok(true, "Call tree rerenders after its corresponding pane is shown.");
+
+ rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ yield DetailsView.selectView("waterfall");
+ yield rendered;
+ ok(true, "Waterfall rerenders after its corresponding pane is shown.");
+
+ is(updatedWaterfall, 3, "WaterfallView rerendered 3 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);
+
+ yield 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 000000000..697691a55
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recording-notices-01.js
@@ -0,0 +1,45 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { $, PerformanceView } = panel.panelWin;
+
+ let MAIN_CONTAINER = $("#performance-view");
+ let EMPTY = $("#empty-notice");
+ let CONTENT = $("#performance-view-content");
+ let DETAILS_CONTAINER = $("#details-pane-container");
+ let RECORDING = $("#recording-notice");
+ let DETAILS = $("#details-pane");
+
+ is(PerformanceView.getState(), "empty", "Correct default state.");
+ is(MAIN_CONTAINER.selectedPanel, EMPTY, "Showing empty panel on load.");
+
+ yield 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.");
+
+ yield 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.");
+
+ yield 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 000000000..b7905b3d7
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recording-notices-02.js
@@ -0,0 +1,65 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let {
+ EVENTS,
+ $,
+ PerformanceController,
+ PerformanceView,
+ } = panel.panelWin;
+
+ let MAIN_CONTAINER = $("#performance-view");
+ let CONTENT = $("#performance-view-content");
+ let DETAILS_CONTAINER = $("#details-pane-container");
+ let RECORDING = $("#recording-notice");
+ let DETAILS = $("#details-pane");
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ yield 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);
+ yield 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);
+ yield 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.");
+
+ yield stopRecording(panel);
+
+ yield 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 000000000..eeb439677
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recording-notices-03.js
@@ -0,0 +1,135 @@
+/* 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 { pmmLoadFrameScripts, pmmStopProfiler, pmmClearFrameScripts } = 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(function* () {
+ // Make sure the profiler module is stopped so we can set a new buffer limit.
+ pmmLoadFrameScripts(gBrowser);
+ yield pmmStopProfiler();
+
+ // Keep the profiler's buffer large, but still get to 1% relatively quick.
+ Services.prefs.setIntPref(PROFILER_BUFFER_SIZE_PREF, 1000000);
+
+ let { target, console } = yield initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { panel } = yield initPerformanceInTab({ tab: target.tab });
+ let {
+ gFront,
+ EVENTS,
+ $,
+ PerformanceController,
+ PerformanceView,
+ } = panel.panelWin;
+
+ // Set a fast profiler-status update interval.
+ yield gFront.setProfilerStatusInterval(10);
+
+ let DETAILS_CONTAINER = $("#details-pane-container");
+ let NORMAL_BUFFER_STATUS_MESSAGE = $("#recording-notice .buffer-status-message");
+ let CONSOLE_BUFFER_STATUS_MESSAGE =
+ $("#console-recording-notice .buffer-status-message");
+ let gPercent;
+
+ // Start a manual recording.
+ yield startRecording(panel);
+
+ yield waitUntil(function* () {
+ [, gPercent] = yield 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());
+ either(DETAILS_CONTAINER.getAttribute("buffer-status"), "in-progress", "full",
+ "Container has [buffer-status=in-progress] or [buffer-status=full].");
+ ok(NORMAL_BUFFER_STATUS_MESSAGE.value.indexOf(gPercent + "%") !== -1,
+ "Buffer status text has correct percentage.");
+
+ // Start a console profile.
+ yield console.profile("rust");
+
+ yield waitUntil(function* () {
+ [, gPercent] = yield once(PerformanceView,
+ EVENTS.UI_RECORDING_PROFILER_STATUS_RENDERED,
+ { spreadArgs: true });
+ return gPercent > Math.floor(bufferUsage * 100);
+ });
+
+ ok(true, "Buffer percentage increased in display (2).");
+
+ bufferUsage = PerformanceController.getBufferUsageForRecording(
+ PerformanceController.getCurrentRecording());
+ either(DETAILS_CONTAINER.getAttribute("buffer-status"), "in-progress", "full",
+ "Container has [buffer-status=in-progress] or [buffer-status=full].");
+ ok(NORMAL_BUFFER_STATUS_MESSAGE.value.indexOf(gPercent + "%") !== -1,
+ "Buffer status text has correct percentage.");
+
+ // Select the console recording.
+ let selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+ setSelectedRecording(panel, 1);
+ yield selected;
+
+ yield waitUntil(function* () {
+ [, gPercent] = yield once(PerformanceView,
+ EVENTS.UI_RECORDING_PROFILER_STATUS_RENDERED,
+ { spreadArgs: true });
+ return gPercent > 0;
+ });
+
+ ok(true, "Percentage updated for newly selected recording.");
+
+ either(DETAILS_CONTAINER.getAttribute("buffer-status"), "in-progress", "full",
+ "Container has [buffer-status=in-progress] or [buffer-status=full].");
+ ok(CONSOLE_BUFFER_STATUS_MESSAGE.value.indexOf(gPercent + "%") !== -1,
+ "Buffer status text has correct percentage for console recording.");
+
+ // Stop the console profile, then select the original manual recording.
+ yield console.profileEnd("rust");
+
+ selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+ setSelectedRecording(panel, 0);
+ yield selected;
+
+ yield waitUntil(function* () {
+ [, gPercent] = yield once(PerformanceView,
+ EVENTS.UI_RECORDING_PROFILER_STATUS_RENDERED,
+ { spreadArgs: true });
+ return gPercent > Math.floor(bufferUsage * 100);
+ });
+
+ ok(true, "Buffer percentage increased in display (3).");
+
+ either(DETAILS_CONTAINER.getAttribute("buffer-status"), "in-progress", "full",
+ "Container has [buffer-status=in-progress] or [buffer-status=full].");
+ ok(NORMAL_BUFFER_STATUS_MESSAGE.value.indexOf(gPercent + "%") !== -1,
+ "Buffer status text has correct percentage.");
+
+ // Stop the manual recording.
+ yield stopRecording(panel);
+
+ yield teardownToolboxAndRemoveTab(panel);
+
+ pmmClearFrameScripts();
+});
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 000000000..067cda9dc
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recording-notices-04.js
@@ -0,0 +1,66 @@
+/* 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 { pmmLoadFrameScripts, pmmStopProfiler, pmmClearFrameScripts } = 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(function* () {
+ // Make sure the profiler module is stopped so we can set a new buffer limit.
+ pmmLoadFrameScripts(gBrowser);
+ yield pmmStopProfiler();
+
+ // Keep the profiler's buffer small, to get to 100% really quickly.
+ Services.prefs.setIntPref(PROFILER_BUFFER_SIZE_PREF, 10000);
+
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { gFront, EVENTS, $, PerformanceController, PerformanceView } = panel.panelWin;
+
+ // Set a fast profiler-status update interval
+ yield gFront.setProfilerStatusInterval(10);
+
+ let DETAILS_CONTAINER = $("#details-pane-container");
+ let NORMAL_BUFFER_STATUS_MESSAGE = $("#recording-notice .buffer-status-message");
+ let gPercent;
+
+ // Start a manual recording.
+ yield startRecording(panel);
+
+ yield waitUntil(function* () {
+ [, gPercent] = yield once(PerformanceView,
+ EVENTS.UI_RECORDING_PROFILER_STATUS_RENDERED,
+ { spreadArgs: true });
+ return gPercent == 100;
+ });
+
+ ok(true, "Buffer percentage increased in display.");
+
+ let bufferUsage = PerformanceController.getBufferUsageForRecording(
+ PerformanceController.getCurrentRecording());
+ ok(bufferUsage, 1, "Buffer is full for this recording.");
+ ok(DETAILS_CONTAINER.getAttribute("buffer-status"), "full",
+ "Container has [buffer-status=full].");
+ ok(NORMAL_BUFFER_STATUS_MESSAGE.value.indexOf(gPercent + "%") !== -1,
+ "Buffer status text has correct percentage.");
+
+ // Stop the manual recording.
+ yield stopRecording(panel);
+
+ yield teardownToolboxAndRemoveTab(panel);
+
+ pmmClearFrameScripts();
+});
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 000000000..b6267470d
--- /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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { gFront, $, PerformanceController } = panel.panelWin;
+
+ // Set a fast profiler-status update interval
+ yield gFront.setProfilerStatusInterval(10);
+
+ let supported = false;
+ let enabled = false;
+
+ PerformanceController.getMultiprocessStatus = () => {
+ return { supported, enabled };
+ };
+
+ PerformanceController._setMultiprocessAttributes();
+ ok($("#performance-view").getAttribute("e10s"), "unsupported",
+ "When e10s is disabled and no option to turn on, container has [e10s=unsupported].");
+
+ supported = true;
+ enabled = false;
+ PerformanceController._setMultiprocessAttributes();
+ ok($("#performance-view").getAttribute("e10s"), "disabled",
+ "When e10s is disabled and but is supported, container has [e10s=disabled].");
+
+ supported = false;
+ enabled = true;
+ PerformanceController._setMultiprocessAttributes();
+ ok($("#performance-view").getAttribute("e10s"), "",
+ "When e10s is enabled, but not supported, this probably means we no longer have " +
+ "E10S_TESTING_ONLY, and we have no e10s attribute.");
+
+ supported = true;
+ enabled = true;
+ PerformanceController._setMultiprocessAttributes();
+ ok($("#performance-view").getAttribute("e10s"), "",
+ "When e10s is enabled and supported, there should be no e10s attribute.");
+
+ yield 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 000000000..15cb66ec9
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recording-selected-01.js
@@ -0,0 +1,45 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, PerformanceController } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ is(getRecordingsCount(panel), 2,
+ "There should be two recordings visible.");
+ is(getSelectedRecordingIndex(panel), 1,
+ "The second recording item should be selected.");
+
+ let selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+ setSelectedRecording(panel, 0);
+ yield selected;
+
+ is(getRecordingsCount(panel), 2,
+ "There should still be two recordings visible.");
+ is(getSelectedRecordingIndex(panel), 0,
+ "The first recording item should be selected.");
+
+ yield 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 000000000..0bffa3a73
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recording-selected-02.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 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(function* () {
+ // This test seems to take a very long time to finish on Linux VMs.
+ requestLongerTimeout(4);
+
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, PerformanceController } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ yield 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);
+ yield 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);
+ yield selected;
+
+ is(getRecordingsCount(panel), 2,
+ "There should still be two recordings visible.");
+ is(getSelectedRecordingIndex(panel), 1,
+ "The second recording item should be selected again.");
+
+ yield stopRecording(panel);
+
+ yield 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 000000000..7febfbb2b
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recording-selected-03.js
@@ -0,0 +1,44 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { $, EVENTS, PerformanceController } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ yield startRecording(panel);
+
+ info("Selecting recording #0 and waiting for it to be displayed.");
+
+ let selected = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+ setSelectedRecording(panel, 0);
+ yield 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.");
+
+ yield stopRecording(panel);
+
+ yield 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 000000000..014ef5bdd
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recording-selected-04.js
@@ -0,0 +1,59 @@
+/* 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 } = 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { 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);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ // Ã…llow 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.
+ yield DetailsView.selectView("js-calltree");
+ yield DetailsView.selectView("js-flamegraph");
+ yield DetailsView.selectView("memory-calltree");
+ yield DetailsView.selectView("memory-flamegraph");
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let rerender = waitForAllWidgetsRendered(panel);
+ setSelectedRecording(panel, 0);
+ yield rerender;
+
+ ok(true, "All widgets were rendered when selecting the first recording.");
+
+ rerender = waitForAllWidgetsRendered(panel);
+ setSelectedRecording(panel, 1);
+ yield rerender;
+
+ ok(true, "All widgets were rendered when selecting the second recording.");
+
+ yield 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 000000000..87579896a
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recordings-clear-01.js
@@ -0,0 +1,54 @@
+/* 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(function* () {
+ let { panel } = yield initPanelInNewTab({
+ tool: "performance",
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { PerformanceController, PerformanceView } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield 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.");
+
+ yield startRecording(panel);
+ yield 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.");
+
+ yield 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.");
+
+ yield 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 000000000..d8196dbd1
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recordings-clear-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 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(function* () {
+ let { panel } = yield initPanelInNewTab({
+ tool: "performance",
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, PerformanceController, PerformanceView } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield 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.");
+
+ yield 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.");
+
+ let recordingDeleted = times(PerformanceController, EVENTS.RECORDING_DELETED, 2);
+ let recordingStopped = once(PerformanceController, EVENTS.RECORDING_STATE_CHANGE, {
+ expectedArgs: { "1": "recording-stopped" }
+ });
+
+ PerformanceController.clearRecordings();
+
+ yield recordingDeleted;
+ yield 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.
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ is(getRecordingsCount(panel), 1,
+ "The recordings list should have one recording.");
+
+ yield 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 000000000..90a014421
--- /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 = Task.async(function* () {
+ var { target, panel, toolbox } = yield 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;
+
+ yield startRecording(panel);
+ yield 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.
+ yield DetailsView.selectView("js-calltree");
+ yield DetailsView.selectView("js-flamegraph");
+ yield DetailsView.selectView("memory-calltree");
+ yield 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);
+ yield PerformanceController.exportRecording("", PerformanceController.getCurrentRecording(), file);
+
+ yield 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);
+
+ yield imported;
+ ok(true, "The recording data appears to have been successfully imported.");
+
+ yield 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).");
+
+ yield 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 000000000..48e7fb63c
--- /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 = Task.async(function* () {
+ let { target, panel, toolbox } = yield 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 {
+ yield 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.");
+ }
+
+ yield 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 000000000..e9fafd392
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recordings-io-03.js
@@ -0,0 +1,56 @@
+/* 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 } = Cu.import("resource://gre/modules/FileUtils.jsm", {});
+var { NetUtil } = Cu.import("resource://gre/modules/NetUtil.jsm", {});
+
+var test = Task.async(function* () {
+ let { target, panel, toolbox } = yield 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));
+ yield asyncCopy({ bogus: "data" }, file);
+
+ try {
+ yield 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.");
+ }
+
+ yield 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 deferred = Promise.defer();
+
+ let string = JSON.stringify(data);
+ let inputStream = getUnicodeConverter().convertToInputStream(string);
+ let outputStream = FileUtils.openSafeFileOutputStream(file);
+
+ NetUtil.asyncCopy(inputStream, outputStream, status => {
+ if (!Components.isSuccessCode(status)) {
+ deferred.reject(new Error("Could not save data to file."));
+ }
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+}
+/* 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 000000000..2da84f438
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recordings-io-04.js
@@ -0,0 +1,178 @@
+/* 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 = Task.async(function* () {
+ let { target, panel, toolbox } = yield 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));
+ yield 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);
+ yield PerformanceController.importRecording("", file);
+
+ yield imported;
+ ok(true, "The original profiler data appears to have been successfully imported.");
+
+ yield calltreeRendered;
+ yield 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.`);
+ }
+ }
+
+ yield 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 deferred = Promise.defer();
+
+ let string = JSON.stringify(data);
+ let inputStream = getUnicodeConverter().convertToInputStream(string);
+ let outputStream = FileUtils.openSafeFileOutputStream(file);
+
+ NetUtil.asyncCopy(inputStream, outputStream, status => {
+ if (!Components.isSuccessCode(status)) {
+ deferred.reject(new Error("Could not save data to file."));
+ }
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+}
+/* 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 000000000..e836da917
--- /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 = Task.async(function* () {
+ var { target, panel, toolbox } = yield initPerformance(SIMPLE_URL);
+ var { EVENTS, PerformanceController, WaterfallView } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield 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);
+ yield PerformanceController.exportRecording("", PerformanceController.getCurrentRecording(), file);
+
+ yield exported;
+ ok(true, "The recording data appears to have been successfully saved.");
+
+ // Clear and re-import.
+
+ yield PerformanceController.clearRecordings();
+
+ let rendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ let imported = once(PerformanceController, EVENTS.RECORDING_IMPORTED);
+ yield PerformanceController.importRecording("", file);
+ yield imported;
+ yield rendered;
+
+ ok(true, "No error was thrown.");
+
+ yield 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 000000000..18734ce52
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-recordings-io-06.js
@@ -0,0 +1,142 @@
+/* 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 = Task.async(function* () {
+ let { target, panel, toolbox } = yield 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));
+ yield asyncCopy(profilerData, file);
+
+ // Import recording.
+
+ let calltreeRendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ let imported = once(PerformanceController, EVENTS.RECORDING_IMPORTED);
+ yield PerformanceController.importRecording("", file);
+
+ yield imported;
+ ok(true, "The profiler data appears to have been successfully imported.");
+
+ yield calltreeRendered;
+ ok(true, "The imported data was re-rendered.");
+
+ yield 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 deferred = Promise.defer();
+
+ let string = JSON.stringify(data);
+ let inputStream = getUnicodeConverter().convertToInputStream(string);
+ let outputStream = FileUtils.openSafeFileOutputStream(file);
+
+ NetUtil.asyncCopy(inputStream, outputStream, status => {
+ if (!Components.isSuccessCode(status)) {
+ deferred.reject(new Error("Could not save data to file."));
+ }
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+}
+/* 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 000000000..825e2153f
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-refresh.js
@@ -0,0 +1,36 @@
+/* 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(function* () {
+ let { panel, target } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { PerformanceController } = panel.panelWin;
+
+ yield startRecording(panel);
+ yield reload(target);
+
+ let recording = PerformanceController.getCurrentRecording();
+ let markersLength = recording.getAllData().markers.length;
+
+ ok(recording.isRecording(),
+ "RecordingModel should still be recording after reload");
+
+ yield waitUntil(() => recording.getMarkers().length > markersLength);
+ ok("Markers continue after reload.");
+
+ yield stopRecording(panel);
+ yield 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 000000000..c01fb3121
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-states.js
@@ -0,0 +1,102 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { 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.");
+
+ yield 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.");
+
+ yield 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.");
+
+ yield 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.");
+
+ yield 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.");
+
+ yield 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 000000000..2c37e6c5a
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-telemetry-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 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { PerformanceController } = panel.panelWin;
+
+ let telemetry = PerformanceController._telemetry;
+ let logs = telemetry.getLogs();
+ let DURATION = "DEVTOOLS_PERFTOOLS_RECORDING_DURATION_MS";
+ let COUNT = "DEVTOOLS_PERFTOOLS_RECORDING_COUNT";
+ let CONSOLE_COUNT = "DEVTOOLS_PERFTOOLS_CONSOLE_RECORDING_COUNT";
+ let FEATURES = "DEVTOOLS_PERFTOOLS_RECORDING_FEATURES_USED";
+
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, false);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ Services.prefs.setBoolPref(UI_ENABLE_MEMORY_PREF, true);
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ is(logs[DURATION].length, 2, `There are two entries for ${DURATION}.`);
+ ok(logs[DURATION].every(d => typeof d === "number"),
+ `Every ${DURATION} entry is a number.`);
+ is(logs[COUNT].length, 2, `There are two entries for ${COUNT}.`);
+ is(logs[CONSOLE_COUNT], void 0, `There are no entries for ${CONSOLE_COUNT}.`);
+ is(logs[FEATURES].length, 8,
+ `There are two recordings worth of entries for ${FEATURES}.`);
+ ok(logs[FEATURES].find(r => r[0] === "withMemory" && r[1] === true),
+ "One feature entry for memory enabled.");
+ ok(logs[FEATURES].find(r => r[0] === "withMemory" && r[1] === false),
+ "One feature entry for memory disabled.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
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 000000000..6fe268e3a
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-telemetry-02.js
@@ -0,0 +1,48 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { EVENTS, PerformanceController } = panel.panelWin;
+
+ let telemetry = PerformanceController._telemetry;
+ let logs = telemetry.getLogs();
+ let EXPORTED = "DEVTOOLS_PERFTOOLS_RECORDING_EXPORT_FLAG";
+ let IMPORTED = "DEVTOOLS_PERFTOOLS_RECORDING_IMPORT_FLAG";
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let file = FileUtils.getFile("TmpD", ["tmpprofile.json"]);
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
+
+ let exported = once(PerformanceController, EVENTS.RECORDING_EXPORTED);
+ yield PerformanceController.exportRecording("",
+ PerformanceController.getCurrentRecording(), file);
+ yield exported;
+
+ ok(logs[EXPORTED], `A telemetry entry for ${EXPORTED} exists after exporting.`);
+
+ let imported = once(PerformanceController, EVENTS.RECORDING_IMPORTED);
+ yield PerformanceController.importRecording(null, file);
+ yield imported;
+
+ ok(logs[IMPORTED], `A telemetry entry for ${IMPORTED} exists after importing.`);
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
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 000000000..a10f314d2
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-telemetry-03.js
@@ -0,0 +1,56 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let {
+ EVENTS,
+ PerformanceController,
+ DetailsView,
+ JsCallTreeView,
+ JsFlameGraphView
+ } = panel.panelWin;
+
+ let telemetry = PerformanceController._telemetry;
+ let logs = telemetry.getLogs();
+ let VIEWS = "DEVTOOLS_PERFTOOLS_SELECTED_VIEW_MS";
+
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ let calltreeRendered = once(JsCallTreeView, EVENTS.UI_JS_CALL_TREE_RENDERED);
+ let flamegraphRendered = once(JsFlameGraphView, EVENTS.UI_JS_FLAMEGRAPH_RENDERED);
+
+ // Go through some views to check later.
+ yield DetailsView.selectView("js-calltree");
+ yield calltreeRendered;
+
+ yield DetailsView.selectView("js-flamegraph");
+ yield flamegraphRendered;
+
+ yield teardownToolboxAndRemoveTab(panel);
+
+ // Check views after destruction to ensure `js-flamegraph` gets called
+ // with a time during destruction.
+ ok(logs[VIEWS].find(r => r[0] === "waterfall" && typeof r[1] === "number"),
+ `${VIEWS} for waterfall view and time.`);
+ ok(logs[VIEWS].find(r => r[0] === "js-calltree" && typeof r[1] === "number"),
+ `${VIEWS} for js-calltree view and time.`);
+ ok(logs[VIEWS].find(r => r[0] === "js-flamegraph" && typeof r[1] === "number"),
+ `${VIEWS} for js-flamegraph view and time.`);
+});
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 000000000..362b54714
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-telemetry-04.js
@@ -0,0 +1,50 @@
+/* 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(function* () {
+ let { target, console } = yield initConsoleInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { panel } = yield initPerformanceInTab({ tab: target.tab });
+ let { PerformanceController } = panel.panelWin;
+
+ let telemetry = PerformanceController._telemetry;
+ let logs = telemetry.getLogs();
+ let DURATION = "DEVTOOLS_PERFTOOLS_RECORDING_DURATION_MS";
+ let CONSOLE_COUNT = "DEVTOOLS_PERFTOOLS_CONSOLE_RECORDING_COUNT";
+ let FEATURES = "DEVTOOLS_PERFTOOLS_RECORDING_FEATURES_USED";
+
+ let started = waitForRecordingStartedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true
+ });
+ yield console.profile("rust");
+ yield started;
+
+ let stopped = waitForRecordingStoppedEvents(panel, {
+ // only emitted for manual recordings
+ skipWaitingForBackendReady: true
+ });
+ yield console.profileEnd("rust");
+ yield stopped;
+
+ is(logs[DURATION].length, 1, `There is one entry for ${DURATION}.`);
+ ok(logs[DURATION].every(d => typeof d === "number"),
+ `Every ${DURATION} entry is a number.`);
+ is(logs[CONSOLE_COUNT].length, 1, `There is one entry for ${CONSOLE_COUNT}.`);
+ is(logs[FEATURES].length, 4,
+ `There is one recording worth of entries for ${FEATURES}.`);
+
+ yield teardownToolboxAndRemoveTab(panel);
+});
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 000000000..f8dbe9767
--- /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);
+
+function* spawnTest() {
+ let { panel } = yield initPerformance(SIMPLE_URL);
+ let { EVENTS, $, OverviewView, document: doc } = panel.panelWin;
+
+ yield startRecording(panel);
+ let markers = OverviewView.graphs.get("timeline");
+ is(markers.backgroundColor, DARK_BG,
+ "correct theme on load for markers.");
+ yield stopRecording(panel);
+
+ let refreshed = once(markers, "refresh");
+ setTheme("light");
+ yield 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");
+ yield refreshed;
+
+ info("Testing with memory overview");
+
+ Services.prefs.setBoolPref(MEMORY_PREF, true);
+
+ yield startRecording(panel);
+ let memory = OverviewView.graphs.get("memory");
+ is(memory.backgroundColor, DARK_BG,
+ "correct theme on load for memory.");
+ yield stopRecording(panel);
+
+ refreshed = Promise.all([
+ once(markers, "refresh"),
+ once(memory, "refresh"),
+ ]);
+ setTheme("light");
+ yield 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");
+ yield refreshed;
+
+ yield 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 000000000..9b56a1b8c
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-abstract-01.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 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(function* () {
+ let { MyCustomTreeItem, myDataSrc } = synthesizeCustomTreeClass();
+
+ let container = document.createElement("vbox");
+ yield appendAndWaitForPaint(gBrowser.selectedBrowser.parentNode, container);
+
+ // Populate the tree and test the root item...
+
+ let 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");
+ mousedown(treeRoot.target.querySelector(".arrow"));
+
+ let [, eventItem] = yield receivedExpandEvent;
+ is(eventItem, treeRoot,
+ "The 'expand' event target is correct (1).");
+
+ yield receivedFocusEvent;
+ is(document.commandDispatcher.focusedElement, treeRoot.target,
+ "The root node is now focused.");
+
+ let fooItem = treeRoot.getChild(0);
+ let 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 });
+ mousedown(fooItem.target);
+
+ [, eventItem] = yield 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");
+ dblclick(barItem.target);
+
+ [, eventItem] = yield receivedExpandEvent;
+ is(eventItem, barItem,
+ "The 'expand' event target is correct (3).");
+
+ yield receivedFocusEvent;
+ is(document.commandDispatcher.focusedElement, barItem.target,
+ "The 'foo' node is now focused.");
+
+ // A child item got expanded, test the descendants...
+
+ let 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 000000000..62db4cfd6
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-abstract-02.js
@@ -0,0 +1,138 @@
+/* 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(function* () {
+ let { MyCustomTreeItem, myDataSrc } = synthesizeCustomTreeClass();
+
+ let container = document.createElement("vbox");
+ yield appendAndWaitForPaint(gBrowser.selectedBrowser.parentNode, container);
+
+ // Populate the tree and test the root item...
+
+ let 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.");
+
+ let fooItem = treeRoot.getChild(0);
+ let 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.");
+
+ let 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 000000000..4e427fcd3
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-abstract-03.js
@@ -0,0 +1,151 @@
+/* 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(function* () {
+ let { MyCustomTreeItem, myDataSrc } = synthesizeCustomTreeClass();
+
+ let container = document.createElement("vbox");
+ yield appendAndWaitForPaint(gBrowser.selectedBrowser.parentNode, container);
+
+ // Populate the tree by pressing RIGHT...
+
+ let 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.");
+
+ let fooItem = treeRoot.getChild(0);
+ let 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.");
+
+ let 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 000000000..614235ab8
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-abstract-04.js
@@ -0,0 +1,35 @@
+/* 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(function* () {
+ let { MyCustomTreeItem, myDataSrc } = synthesizeCustomTreeClass();
+
+ let container = document.createElement("vbox");
+ yield appendAndWaitForPaint(gBrowser.selectedBrowser.parentNode, container);
+
+ // Populate the tree and test the root item...
+
+ let treeRoot = new MyCustomTreeItem(myDataSrc, { parent: null });
+ treeRoot.attachTo(container);
+
+ let originalTreeRootExpandedState = treeRoot.expanded;
+ info("Double clicking on the root item arrow and waiting for focus event.");
+
+ let receivedFocusEvent = once(treeRoot, "focus");
+ dblclick(treeRoot.target.querySelector(".arrow"));
+ yield 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 000000000..88138b6f3
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-abstract-05.js
@@ -0,0 +1,103 @@
+/* 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(function* () {
+ let { MyCustomTreeItem } = synthesizeCustomTreeClass();
+
+ let container = document.createElement("vbox");
+ container.style.height = "100%";
+ container.style.overflow = "scroll";
+ yield appendAndWaitForPaint(gBrowser.selectedBrowser.parentNode, container);
+
+ let myDataSrc = {
+ label: "root",
+ children: []
+ };
+
+ for (let i = 0; i < 1000; i++) {
+ myDataSrc.children.push({
+ label: "child-" + i,
+ children: []
+ });
+ }
+
+ let 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
+
+ let 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 000000000..a2699d3d0
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-view-01.js
@@ -0,0 +1,65 @@
+/* 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 () {
+ let profile = synthesizeProfile();
+ let 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;
+
+ let treeRoot = new CallView({ frame: threadNode });
+ let container = document.createElement("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 000000000..bb325ba90
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-view-02.js
@@ -0,0 +1,148 @@
+/* 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.
+ let profile = synthesizeProfile();
+
+ profile.threads[0].stringTable[1] =
+ profile.threads[0].stringTable[1].replace("A (", `${MANGLED_FN} (`);
+
+ let 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;
+
+ let treeRoot = new CallView({ frame: threadNode });
+ let container = document.createElement("vbox");
+ treeRoot.autoExpandDepth = 0;
+ treeRoot.attachTo(container);
+
+ let $$ = node => container.querySelectorAll(node);
+ let $fun = (node, ancestor) => (ancestor || container).querySelector(
+ ".call-tree-cell[type=function] > " + node);
+ let $$fun = (node, ancestor) => (ancestor || container).querySelectorAll(
+ ".call-tree-cell[type=function] > " + node);
+ let $$dur = i => container.querySelectorAll(".call-tree-cell[type=duration]")[i];
+ let $$per = i => container.querySelectorAll(".call-tree-cell[type=percentage]")[i];
+ let $$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], null,
+ "The root's function cell displays no url.");
+ is($$fun(".call-tree-line")[0], null,
+ "The root's function cell displays no line.");
+ is($$fun(".call-tree-host")[0], null,
+ "The root's function cell displays no host.");
+ is($$fun(".call-tree-category")[0], null,
+ "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(), "Gecko",
+ "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.");
+
+ let 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.");
+
+ 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",
+ "The .A.B node's function cell displays the correct name.");
+ is($fun(".call-tree-url", $$(".call-tree-item")[2]).textContent.trim(), "baz",
+ "The .A.B node's function cell displays the correct url.");
+ ok($fun(".call-tree-url", $$(".call-tree-item")[2]).getAttribute("tooltiptext").includes("http://foo/bar/baz"),
+ "The .A.B node's function cell displays the correct url tooltiptext.");
+ is($fun(".call-tree-line", $$(".call-tree-item")[2]).textContent.trim(), ":34",
+ "The .A.B node's function cell displays the correct line.");
+ is($fun(".call-tree-host", $$(".call-tree-item")[2]).textContent.trim(), "foo",
+ "The .A.B node's function cell displays the correct host.");
+ is($fun(".call-tree-category", $$(".call-tree-item")[2]).textContent.trim(), "Styles",
+ "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-url", $$(".call-tree-item")[3]).textContent.trim(), "baz",
+ "The .A.E node's function cell displays the correct url.");
+ ok($fun(".call-tree-url", $$(".call-tree-item")[3]).getAttribute("tooltiptext").includes("http://foo/bar/baz"),
+ "The .A.E node's function cell displays the correct url tooltiptext.");
+ is($fun(".call-tree-line", $$(".call-tree-item")[3]).textContent.trim(), ":90",
+ "The .A.E node's function cell displays the correct line.");
+ is($fun(".call-tree-host", $$(".call-tree-item")[3]).textContent.trim(), "foo",
+ "The .A.E node's function cell displays the correct host.");
+ 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 000000000..44ab50f32
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-view-03.js
@@ -0,0 +1,79 @@
+/* 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 () {
+ let profile = synthesizeProfile();
+ let 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;
+
+ let treeRoot = new CallView({ frame: threadNode });
+ let container = document.createElement("vbox");
+ treeRoot.attachTo(container);
+
+ let $$fun = i => container.querySelectorAll(".call-tree-cell[type=function]")[i];
+ let $$nam = i => container.querySelectorAll(
+ ".call-tree-cell[type=function] > .call-tree-name")[i];
+ let $$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.filter(container.childNodes, 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",
+ "The .A.B node's function cell displays the correct name.");
+ is($$nam(3).textContent.trim(), "D",
+ "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 000000000..b2bc3dae5
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-view-04.js
@@ -0,0 +1,78 @@
+/* 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 () {
+ let profile = synthesizeProfile();
+ let 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;
+
+ let treeRoot = new CallView({ frame: threadNode });
+ let container = document.createElement("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.");
+
+ let A = treeRoot.getChild();
+ let B = A.getChild();
+ let D = B.getChild();
+
+ 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 (http://foo/bar/baz:78:9)",
+ "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.");
+
+ let functionCell = D.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 000000000..045ed4ce2
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-view-05.js
@@ -0,0 +1,36 @@
+/* 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 () {
+ let profile = synthesizeProfile();
+ let 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;
+
+ let treeRoot = new CallView({ frame: threadNode });
+ let container = document.createElement("vbox");
+ treeRoot.attachTo(container);
+
+ let 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 000000000..305195ddc
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-view-06.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
+ * 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(function* () {
+ let profile = synthesizeProfile();
+ let 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;
+
+ let treeRoot = new CallView({ frame: threadNode });
+ let container = document.createElement("vbox");
+ treeRoot.attachTo(container);
+
+ let A = treeRoot.getChild();
+ let B = A.getChild();
+ let D = B.getChild();
+
+ let linkEvent = null;
+ let handler = (_, e) => {
+ linkEvent = e;
+ };
+
+ treeRoot.on("link", handler);
+
+ // Fire right click.
+ rightMousedown(D.target.querySelector(".call-tree-url"));
+
+ // Ensure link was not called for right click.
+ yield idleWait(100);
+ ok(!linkEvent, "The `link` event not fired for right click.");
+
+ // Fire left click.
+ mousedown(D.target.querySelector(".call-tree-url"));
+
+ // Ensure link was called for left click.
+ yield waitUntil(() => linkEvent);
+ is(linkEvent, D, "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 000000000..cc2cdd612
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-view-07.js
@@ -0,0 +1,40 @@
+/* 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 () {
+ let profile = synthesizeProfile();
+ let 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;
+
+ let treeRoot = new CallView({ frame: threadNode });
+ let container = document.createElement("vbox");
+ container.id = "call-tree-container";
+ treeRoot.attachTo(container);
+
+ let A = treeRoot.getChild();
+ let B = A.getChild();
+ let 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 000000000..7b65ea45c
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-view-08.js
@@ -0,0 +1,109 @@
+/* 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_MASK } = require("devtools/client/performance/modules/categories");
+const RecordingUtils = require("devtools/shared/performance/recording-utils");
+
+add_task(function () {
+ let 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;
+
+ let treeRoot = new CallView({ frame: threadNode, autoExpandDepth: 10 });
+ let container = document.createElement("vbox");
+ treeRoot.attachTo(container);
+
+ /*
+ * (root)
+ * - A
+ * - B
+ * - C
+ * - D
+ * - (GC)
+ * - E
+ * - F
+ * - (JS)
+ * - (JS)
+ */
+
+ let A = treeRoot.getChild(0);
+ let JS = treeRoot.getChild(1);
+ let GC = A.getChild(1);
+ let 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_MASK("js") },
+ ]
+ }, {
+ time: 1 + 1 + 2 + 3,
+ frames: [
+ { location: "(root)" },
+ { location: "platform_JS2", category: CATEGORY_MASK("js") },
+ ]
+ }, {
+ time: 1 + 1 + 2 + 3 + 5,
+ frames: [
+ { location: "(root)" },
+ { location: "http://content/A" },
+ { location: "platform_GC", category: CATEGORY_MASK("gc", 1) },
+ ]
+ }]
+ }]
+});
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 000000000..c7f11549e
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-view-09.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 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 () {
+ let threadNode = new ThreadNode(gProfile.threads[0], { startTime: 0, endTime: 20,
+ invertTree: true });
+ let treeRoot = new CallView({ frame: threadNode, inverted: true });
+ let container = document.createElement("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 000000000..342b47b92
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-view-10.js
@@ -0,0 +1,160 @@
+/* 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 () {
+ let threadNode = new ThreadNode(gProfile.threads[0], { startTime: 0, endTime: 50,
+ invertTree: true });
+ let treeRoot = new CallView({ frame: threadNode, inverted: true });
+ let container = document.createElement("vbox");
+ treeRoot.attachTo(container);
+
+ // Add 1 to each index to skip the hidden root node
+ let $$nam = i => container.querySelectorAll(
+ ".call-tree-cell[type=function] > .call-tree-name")[i + 1];
+ let $$per = i => container.querySelectorAll(
+ ".call-tree-cell[type=percentage]")[i + 1];
+ let $$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 000000000..a316098e3
--- /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");
+
+function* spawnTest() {
+ let { panel } = yield 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
+ yield startRecording(panel);
+ yield stopRecording(panel);
+
+ yield DetailsView.selectView("js-calltree");
+
+ yield 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;
+ }
+ }
+
+ yield teardown(panel);
+ finish();
+
+ 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());
+ yield 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 000000000..b585f763b
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-ui-recording.js
@@ -0,0 +1,39 @@
+/* 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 { pmmLoadFrameScripts, pmmIsProfilerActive, pmmClearFrameScripts } = 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ pmmLoadFrameScripts(gBrowser);
+
+ ok(!(yield pmmIsProfilerActive()),
+ "The built-in profiler module should not have been automatically started.");
+
+ yield startRecording(panel);
+
+ ok((yield pmmIsProfilerActive()),
+ "The built-in profiler module should now be active.");
+
+ yield stopRecording(panel);
+
+ ok((yield pmmIsProfilerActive()),
+ "The built-in profiler module should still be active.");
+
+ yield teardownToolboxAndRemoveTab(panel);
+
+ pmmClearFrameScripts();
+});
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 000000000..4a8d48585
--- /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;
+
+function* spawnTest() {
+ let { panel } = yield 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;
+
+ yield startRecording(panel);
+ ok(true, "Recording has started.");
+
+ yield 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");
+ });
+
+ yield 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;
+
+ yield 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);
+ yield 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);
+ yield 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);
+ yield 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);
+ yield 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);
+ yield 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");
+
+ yield 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 000000000..f9ab00711
--- /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";
+
+function* spawnTest() {
+ let { panel } = yield initPerformance(URL);
+ let { $, $$, EVENTS, PerformanceController, OverviewView, WaterfallView } = panel.panelWin;
+
+ yield startRecording(panel);
+ ok(true, "Recording has started.");
+
+ yield 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);
+ yield stopRecording(panel);
+
+ $("#filter-button").click();
+ let filterJS = $("menuitem[marker-type=Javascript]");
+
+ yield 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);
+ yield 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");
+
+ yield 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 000000000..85d5bd28c
--- /dev/null
+++ b/devtools/client/performance/test/browser_timeline-waterfall-background.js
@@ -0,0 +1,41 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { WaterfallView } = panel.panelWin;
+
+ yield startRecording(panel);
+ ok(true, "Recording has started.");
+
+ // Ensure overview is rendering and some markers were received.
+ yield waitForOverviewRenderedWithMarkers(panel);
+
+ yield 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.");
+
+ yield 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 000000000..bcb87d80c
--- /dev/null
+++ b/devtools/client/performance/test/browser_timeline-waterfall-generic.js
@@ -0,0 +1,105 @@
+/* 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(function* () {
+ let { panel } = yield initPerformanceInNewTab({
+ url: SIMPLE_URL,
+ win: window
+ });
+
+ let { $, $$, EVENTS, WaterfallView } = panel.panelWin;
+
+ yield startRecording(panel);
+ ok(true, "Recording has started.");
+
+ // Ensure overview is rendering and some markers were received.
+ yield waitForOverviewRenderedWithMarkers(panel);
+
+ yield 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.
+
+ let detailsView = WaterfallView.details;
+ // Make sure the bounds are up to date.
+ WaterfallView._recalculateBounds();
+
+ let parentWidthBefore = $("#waterfall-view").getBoundingClientRect().width;
+ let sidebarWidthBefore = $(".waterfall-sidebar").getBoundingClientRect().width;
+ let 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).");
+
+ let waterfallRerendered = once(WaterfallView, EVENTS.UI_WATERFALL_RENDERED);
+ $$(".waterfall-tree-item")[0].click();
+ yield waterfallRerendered;
+
+ let parentWidthAfter = $("#waterfall-view").getBoundingClientRect().width;
+ let sidebarWidthAfter = $(".waterfall-sidebar").getBoundingClientRect().width;
+ let 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).");
+
+ yield 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 000000000..8bf842560
--- /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.
+ */
+
+function* spawnTest() {
+ let { target, panel } = yield initPerformance(SIMPLE_URL);
+ let { $, $$, EVENTS, PerformanceController, OverviewView, WaterfallView } = panel.panelWin;
+
+ const MIN_MARKERS_COUNT = 50;
+ const MAX_MARKERS_SELECT = 20;
+
+ yield startRecording(panel);
+ ok(true, "Recording has started.");
+
+ let updated = 0;
+ OverviewView.on(EVENTS.UI_OVERVIEW_RENDERED, () => updated++);
+
+ ok((yield waitUntil(() => updated > 0)),
+ "The overview graphs were updated a bunch of times.");
+ ok((yield waitUntil(() => PerformanceController.getCurrentRecording().getMarkers().length > MIN_MARKERS_COUNT)),
+ "There are some markers available.");
+
+ yield 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 });
+ yield 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.indexOf($$(".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);
+ EventUtils.sendMouseEvent({ type: "mouseup" }, WaterfallView.detailsSplitter);
+ yield 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.indexOf($$(".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.");
+
+ yield 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 000000000..1c2c1ccae
--- /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.
+ */
+
+function* spawnTest() {
+ let { target, panel } = yield 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 };
+ };
+
+ yield startRecording(panel);
+ ok(true, "Recording has started.");
+
+ yield 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");
+ });
+
+ yield 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.map(markers, e => e.name));
+ info("Markers names from sidebar: " + Array.map(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];
+ 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.indexOf(toMs(mkr.start)) !== -1, "Tooltip has start time.");
+ ok(tooltip.indexOf(toMs(mkr.end)) !== -1, "Tooltip has end time.");
+ }
+
+ yield 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 000000000..5430b8fdc
--- /dev/null
+++ b/devtools/client/performance/test/browser_timeline-waterfall-workers.js
@@ -0,0 +1,97 @@
+/* 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.
+ */
+
+function* spawnTest() {
+ let { panel } = yield initPerformance(WORKER_URL);
+ let { $$, $, PerformanceController } = panel.panelWin;
+
+ loadFrameScripts();
+
+ yield startRecording(panel);
+ ok(true, "Recording has started.");
+
+ evalInDebuggee("performWork()");
+
+ yield 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;
+ });
+
+ yield stopRecording(panel);
+ ok(true, "Recording has ended.");
+
+ for (let node of $$(".waterfall-marker-name[value=Worker")) {
+ testWorkerMarkerUI(node.parentNode.parentNode);
+ }
+
+ yield 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.");
+}
+
+/**
+ * Takes a string `script` and evaluates it directly in the content
+ * in potentially a different process.
+ */
+function evalInDebuggee(script) {
+ let { generateUUID } = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+ let deferred = Promise.defer();
+
+ if (!mm) {
+ throw new Error("`loadFrameScripts()` must be called when using MessageManager.");
+ }
+
+ let id = generateUUID().toString();
+ mm.sendAsyncMessage("devtools:test:eval", { script: script, id: id });
+ mm.addMessageListener("devtools:test:eval:response", handler);
+
+ function handler({ data }) {
+ if (id !== data.id) {
+ return;
+ }
+
+ mm.removeMessageListener("devtools:test:eval:response", handler);
+ deferred.resolve(data.value);
+ }
+
+ return deferred.promise;
+}
+/* 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 000000000..83f927e43
--- /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 000000000..f5ce72de2
--- /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 000000000..93ae5c8e1
--- /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 */
+ let 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 000000000..5cda6eaa6
--- /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 000000000..fd1962157
--- /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");
+ }, false);
+
+ 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 000000000..0aa48d5a1
--- /dev/null
+++ b/devtools/client/performance/test/head.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const { require, loader } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+
+/* 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 = (node, win = window) => {
+ EventUtils.sendMouseEvent({ type: "click" }, node, win);
+};
+
+// Shortcut for simulating a double click on an element.
+const dblclick = (node, win = window) => {
+ EventUtils.sendMouseEvent({ type: "dblclick" }, node, win);
+};
+
+// Shortcut for simulating a mousedown on an element.
+const mousedown = (node, win = window) => {
+ EventUtils.sendMouseEvent({ type: "mousedown" }, node, win);
+};
+
+// Shortcut for simulating a mousedown using the right mouse button on an element.
+const rightMousedown = (node, win = window) => {
+ 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 flags = require("devtools/shared/flags");
+ const PrefUtils = require("devtools/client/performance/test/helpers/prefs");
+
+ flags.testing = true;
+
+ // Make sure all the prefs are reverted to their defaults once tests finish.
+ let 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);
+
+ registerCleanupFunction(() => {
+ info("finish() was called, cleaning up...");
+ flags.testing = false;
+
+ 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.
+ let nsIProfilerModule = Cc["@mozilla.org/tools/profiler;1"]
+ .getService(Ci.nsIProfiler);
+ nsIProfilerModule.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 000000000..e6c70e565
--- /dev/null
+++ b/devtools/client/performance/test/helpers/actions.js
@@ -0,0 +1,155 @@
+/* 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 = {}) {
+ let 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 = {}) {
+ let 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$/;
+
+ let EVENTS = panel.panelWin.EVENTS;
+ let controller = panel.panelWin.PerformanceController;
+ let view = panel.panelWin.PerformanceView;
+ let 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: { "1": "recording-started" }
+ }),
+ options.skipWaitingForViewState
+ ? null
+ : once(view, EVENTS.UI_STATE_CHANGED, {
+ expectedArgs: { "1": options.expectedViewState }
+ }),
+ options.skipWaitingForOverview
+ ? null
+ : once(overview, EVENTS.UI_OVERVIEW_RENDERED, {
+ expectedArgs: { "1": 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";
+
+ let EVENTS = panel.panelWin.EVENTS;
+ let controller = panel.panelWin.PerformanceController;
+ let view = panel.panelWin.PerformanceView;
+ let overview = panel.panelWin.OverviewView;
+ let 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: { "1": "recording-stopping" }
+ }),
+ options.skipWaitingForRecordingStop
+ ? null
+ : once(controller, EVENTS.RECORDING_STATE_CHANGE, {
+ expectedArgs: { "1": "recording-stopped" }
+ }),
+ options.skipWaitingForViewState
+ ? null
+ : once(view, EVENTS.UI_STATE_CHANGED, {
+ expectedArgs: { "1": options.expectedViewState }
+ }),
+ options.skipWaitingForOverview
+ ? null
+ : once(overview, EVENTS.UI_OVERVIEW_RENDERED, {
+ expectedArgs: { "1": 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) => {
+ let { panelWin } = panel;
+ let { 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) => {
+ let { EVENTS, OverviewView, PerformanceController } = panel.panelWin;
+
+ return Promise.all([
+ times(OverviewView, EVENTS.UI_OVERVIEW_RENDERED, minTimes, {
+ expectedArgs: { "1": Constants.FRAMERATE_GRAPH_LOW_RES_INTERVAL }
+ }),
+ waitUntil(() =>
+ PerformanceController.getCurrentRecording().getMarkers().length >= minMarkers
+ ),
+ ]);
+};
+
+/**
+ * Reloads the given tab target.
+ */
+exports.reload = (target) => {
+ target.activeTab.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 000000000..559b2b8d8
--- /dev/null
+++ b/devtools/client/performance/test/helpers/dom-utils.js
@@ -0,0 +1,30 @@
+/* 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) {
+ let isE10s = Services.appinfo.browserTabsRemoteAutostart;
+ if (isE10s) {
+ let win = parent.ownerDocument.defaultView;
+ let 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 000000000..aa184accc
--- /dev/null
+++ b/devtools/client/performance/test/helpers/event-utils.js
@@ -0,0 +1,114 @@
+/* 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 = {}) {
+ let 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}.`));
+ }
+
+ let 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;
+ }
+
+ let [add, remove] = API;
+
+ target[add](eventName, function onEvent(...args) {
+ if ("expectedArgs" in options) {
+ for (let index of Object.keys(options.expectedArgs)) {
+ if (
+ // Expected argument matches this regexp.
+ (options.expectedArgs[index] instanceof RegExp &&
+ !options.expectedArgs[index].exec(args[index])) ||
+ // Expected argument is not a regexp and equal to the received arg.
+ (!(options.expectedArgs[index] instanceof RegExp) &&
+ options.expectedArgs[index] != args[index])
+ ) {
+ dump(`Ignoring event '${eventName}' with unexpected argument at index ` +
+ `${index}: ${args[index]}\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 `once`, but for observer notifications.
+ */
+exports.observeOnce = function (notificationName, options = {}) {
+ return exports.observeTimes(notificationName, 1, options);
+};
+
+/**
+ * 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, false);
+ });
+};
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 000000000..180091d07
--- /dev/null
+++ b/devtools/client/performance/test/helpers/input-utils.js
@@ -0,0 +1,75 @@
+/* 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) => {
+ let ev = node.ownerDocument.createEvent("XULCommandEvent");
+ ev.initCommandEvent("command", true, true, node.ownerDocument.defaultView, 0, false,
+ false, false, false, null);
+ 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 000000000..b858530d6
--- /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 000000000..468a86607
--- /dev/null
+++ b/devtools/client/performance/test/helpers/panel-utils.js
@@ -0,0 +1,106 @@
+/* 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 = function* ({ tool, url, win }, options = {}) {
+ let tab = yield addTab({ url, win }, options);
+ return (yield exports.initPanelInTab({ tool, tab }));
+};
+
+/**
+ * Initializes a toolbox panel in the specified tab.
+ */
+exports.initPanelInTab = function* ({ tool, tab }) {
+ dump(`Initializing a ${tool} panel.\n`);
+
+ let target = TargetFactory.forTab(tab);
+ yield target.makeRemote();
+
+ // 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.
+ let toolbox = yield gDevTools.showToolbox(target, tool);
+ yield toolbox.initPerformance();
+
+ let panel = toolbox.getCurrentPanel();
+ return { target, toolbox, panel };
+};
+
+/**
+ * Initializes a performance panel in a new tab.
+ */
+exports.initPerformanceInNewTab = function* ({ url, win }, options = {}) {
+ let tab = yield addTab({ url, win }, options);
+ return (yield exports.initPerformanceInTab({ tab }));
+};
+
+/**
+ * Initializes a performance panel in the specified tab.
+ */
+exports.initPerformanceInTab = function* ({ tab }) {
+ return (yield 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 = function* ({ url, win }, options = {}) {
+ let tab = yield addTab({ url, win }, options);
+ return (yield exports.initConsoleInTab({ tab }));
+};
+
+/**
+ * Initializes a webconsole panel in the specified tab.
+ * Returns a console property that allows calls to `profile` and `profileEnd`.
+ */
+exports.initConsoleInTab = function* ({ tab }) {
+ let { target, toolbox, panel } = yield exports.initPanelInTab({
+ tool: "webconsole",
+ tab: tab
+ });
+
+ let consoleMethod = function* (method, label, event) {
+ let recordingEventReceived = once(toolbox.performance, event);
+ if (label === undefined) {
+ yield panel.hud.jsterm.execute(`console.${method}()`);
+ } else {
+ yield panel.hud.jsterm.execute(`console.${method}("${label}")`);
+ }
+ yield recordingEventReceived;
+ };
+
+ let profile = function* (label) {
+ return yield consoleMethod("profile", label, "recording-started");
+ };
+
+ let profileEnd = function* (label) {
+ return yield consoleMethod("profileEnd", label, "recording-stopped");
+ };
+
+ return { target, toolbox, panel, console: { profile, profileEnd } };
+};
+
+/**
+ * Tears down a toolbox panel and removes an associated tab.
+ */
+exports.teardownToolboxAndRemoveTab = function* (panel, options) {
+ dump("Destroying panel.\n");
+
+ let tab = panel.target.tab;
+ yield panel.toolbox.destroy();
+ yield removeTab(tab, options);
+};
diff --git a/devtools/client/performance/test/helpers/prefs.js b/devtools/client/performance/test/helpers/prefs.js
new file mode 100644
index 000000000..4d17afe12
--- /dev/null
+++ b/devtools/client/performance/test/helpers/prefs.js
@@ -0,0 +1,72 @@
+/* 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.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-khz";
+
+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, false);
+ return () => Services.prefs.removeObserver(branch, onObserve);
+};
+
+/**
+ * Reverts all known preferences to their default values.
+ */
+exports.rollbackPrefsToDefault = function () {
+ for (let 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 000000000..bffebf818
--- /dev/null
+++ b/devtools/client/performance/test/helpers/profiler-mm-utils.js
@@ -0,0 +1,117 @@
+/* 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.
+ */
+
+const { Cc, Ci } = require("chrome");
+const { Task } = require("devtools/shared/task");
+
+const FRAME_SCRIPT_UTILS_URL = "chrome://devtools/content/shared/frame-script-utils.js";
+
+let gMM = null;
+
+/**
+ * Loads the relevant frame scripts into the provided browser's message manager.
+ */
+exports.pmmLoadFrameScripts = (gBrowser) => {
+ gMM = gBrowser.selectedBrowser.messageManager;
+ gMM.loadFrameScript(FRAME_SCRIPT_UTILS_URL, false);
+};
+
+/**
+ * Clears the cached message manager.
+ */
+exports.pmmClearFrameScripts = () => {
+ gMM = null;
+};
+
+/**
+ * Sends a message to the message listener, attaching an id to the payload data.
+ * Resolves a returned promise when the response is received from the message
+ * listener, with the same id as part of the response payload data.
+ */
+exports.pmmUniqueMessage = function (message, payload) {
+ if (!gMM) {
+ throw new Error("`pmmLoadFrameScripts()` must be called when using MessageManager.");
+ }
+
+ let { generateUUID } = Cc["@mozilla.org/uuid-generator;1"]
+ .getService(Ci.nsIUUIDGenerator);
+ payload.id = generateUUID().toString();
+
+ return new Promise(resolve => {
+ gMM.addMessageListener(message + ":response", function onHandler({ data }) {
+ if (payload.id == data.id) {
+ gMM.removeMessageListener(message + ":response", onHandler);
+ resolve(data.data);
+ }
+ });
+ gMM.sendAsyncMessage(message, payload);
+ });
+};
+
+/**
+ * Checks if the nsProfiler module is active.
+ */
+exports.pmmIsProfilerActive = () => {
+ return exports.pmmSendProfilerCommand("IsActive");
+};
+
+/**
+ * Starts the nsProfiler module.
+ */
+exports.pmmStartProfiler = Task.async(function* ({ entries, interval, features }) {
+ let isActive = (yield exports.pmmSendProfilerCommand("IsActive")).isActive;
+ if (!isActive) {
+ return exports.pmmSendProfilerCommand("StartProfiler", [entries, interval, features,
+ features.length]);
+ }
+ return null;
+});
+/**
+ * Stops the nsProfiler module.
+ */
+exports.pmmStopProfiler = Task.async(function* () {
+ let isActive = (yield exports.pmmSendProfilerCommand("IsActive")).isActive;
+ if (isActive) {
+ return exports.pmmSendProfilerCommand("StopProfiler");
+ }
+ return null;
+});
+
+/**
+ * Calls a method on the nsProfiler module.
+ */
+exports.pmmSendProfilerCommand = (method, args = []) => {
+ return exports.pmmUniqueMessage("devtools:test:profiler", { method, args });
+};
+
+/**
+ * Evaluates a script in content, returning a promise resolved with the
+ * returned result.
+ */
+exports.pmmEvalInDebuggee = (script) => {
+ return exports.pmmUniqueMessage("devtools:test:eval", { script });
+};
+
+/**
+ * Evaluates a console method in content.
+ */
+exports.pmmConsoleMethod = function (method, ...args) {
+ // Terrible ugly hack -- this gets stringified when it uses the
+ // message manager, so an undefined arg in `console.profileEnd()`
+ // turns into a stringified "null", which is terrible. This method
+ // is only used for test helpers, so swap out the argument if its undefined
+ // with an empty string. Differences between empty string and undefined are
+ // tested on the front itself.
+ if (args[0] == null) {
+ args[0] = "";
+ }
+ return exports.pmmUniqueMessage("devtools:test:console", { method, args });
+};
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 000000000..e51e2d5dd
--- /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 000000000..d4631a3f1
--- /dev/null
+++ b/devtools/client/performance/test/helpers/synth-utils.js
@@ -0,0 +1,99 @@
+/* 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_MASK } = 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_MASK("other"), location: "(root)" },
+ { category: CATEGORY_MASK("other"), location: "A (http://foo/bar/baz:12)" },
+ { category: CATEGORY_MASK("css"), location: "B (http://foo/bar/baz:34)" },
+ { category: CATEGORY_MASK("js"), location: "C (http://foo/bar/baz:56)" }
+ ]
+ }, {
+ time: 1 + 1,
+ frames: [
+ { category: CATEGORY_MASK("other"), location: "(root)" },
+ { category: CATEGORY_MASK("other"), location: "A (http://foo/bar/baz:12)" },
+ { category: CATEGORY_MASK("css"), location: "B (http://foo/bar/baz:34)" },
+ { category: CATEGORY_MASK("gc", 1), location: "D (http://foo/bar/baz:78:9)" }
+ ]
+ }, {
+ time: 1 + 1 + 2,
+ frames: [
+ { category: CATEGORY_MASK("other"), location: "(root)" },
+ { category: CATEGORY_MASK("other"), location: "A (http://foo/bar/baz:12)" },
+ { category: CATEGORY_MASK("css"), location: "B (http://foo/bar/baz:34)" },
+ { category: CATEGORY_MASK("gc", 1), location: "D (http://foo/bar/baz:78:9)" }
+ ]
+ }, {
+ time: 1 + 1 + 2 + 3,
+ frames: [
+ { category: CATEGORY_MASK("other"), location: "(root)" },
+ { category: CATEGORY_MASK("other"), location: "A (http://foo/bar/baz:12)" },
+ { category: CATEGORY_MASK("gc", 2), location: "E (http://foo/bar/baz:90)" },
+ { category: CATEGORY_MASK("network"), location: "F (http://foo/bar/baz:99)" }
+ ]
+ }]
+ }]
+ });
+};
+
+/**
+ * Generates a simple implementation for a tree class.
+ */
+exports.synthesizeCustomTreeClass = () => {
+ const { Cu } = require("chrome");
+ const { AbstractTreeItem } = Cu.import("resource://devtools/client/shared/widgets/AbstractTreeItem.jsm", {});
+ const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
+
+ function MyCustomTreeItem(dataSrc, properties) {
+ AbstractTreeItem.call(this, properties);
+ this.itemDataSrc = dataSrc;
+ }
+
+ MyCustomTreeItem.prototype = Heritage.extend(AbstractTreeItem.prototype, {
+ _displaySelf: function (document, arrowNode) {
+ let node = document.createElement("hbox");
+ node.style.marginInlineStart = (this.level * 10) + "px";
+ node.appendChild(arrowNode);
+ node.appendChild(document.createTextNode(this.itemDataSrc.label));
+ return node;
+ },
+
+ _populateSelf: function (children) {
+ for (let 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 000000000..3247faabf
--- /dev/null
+++ b/devtools/client/performance/test/helpers/tab-utils.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* globals dump */
+
+const Services = require("Services");
+const tabs = require("sdk/tabs");
+const tabUtils = require("sdk/tabs/utils");
+const { viewFor } = require("sdk/view/core");
+const { waitForDelayedStartupFinished } = require("devtools/client/performance/test/helpers/wait-utils");
+const { gDevTools } = require("devtools/client/framework/devtools");
+
+/**
+ * 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 = {}) {
+ let id = getRandomInt(0, Number.MAX_SAFE_INTEGER - 1);
+ url += `#${id}`;
+
+ dump(`Adding tab with url: ${url}.\n`);
+
+ return new Promise(resolve => {
+ let tab;
+
+ tabs.on("ready", function onOpen(model) {
+ if (tab != viewFor(model)) {
+ return;
+ }
+ dump(`Tab added and finished loading: ${model.url}.\n`);
+ tabs.off("ready", onOpen);
+ resolve(tab);
+ });
+
+ win.focus();
+ tab = tabUtils.openTab(win, url);
+
+ if (options.dontWaitForTabReady) {
+ resolve(tab);
+ }
+ });
+};
+
+/**
+ * Removes a browser tab from the specified window and waits for it to close.
+ */
+exports.removeTab = function (tab, options = {}) {
+ dump(`Removing tab: ${tabUtils.getURI(tab)}.\n`);
+
+ return new Promise(resolve => {
+ tabs.on("close", function onClose(model) {
+ if (tab != viewFor(model)) {
+ return;
+ }
+ dump(`Tab removed and finished closing: ${model.url}.\n`);
+ tabs.off("close", onClose);
+ resolve(tab);
+ });
+
+ tabUtils.closeTab(tab);
+
+ if (options.dontWaitForTabClose) {
+ resolve(tab);
+ }
+ });
+};
+
+/**
+ * Adds a browser window with the provided options.
+ */
+exports.addWindow = function* (options) {
+ let { OpenBrowserWindow } = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+ let win = OpenBrowserWindow(options);
+ yield waitForDelayedStartupFinished(win);
+ return win;
+};
diff --git a/devtools/client/performance/test/helpers/urls.js b/devtools/client/performance/test/helpers/urls.js
new file mode 100644
index 000000000..3bf1180b3
--- /dev/null
+++ b/devtools/client/performance/test/helpers/urls.js
@@ -0,0 +1,6 @@
+/* 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`;
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 000000000..be654b7d8
--- /dev/null
+++ b/devtools/client/performance/test/helpers/wait-utils.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/* globals dump */
+
+const { CC } = require("chrome");
+const { Task } = require("devtools/shared/task");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const { once, observeOnce } = 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`);
+ let 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 = function* (predicate, interval = 100, tries = 100) {
+ for (let i = 1; i <= tries; i++) {
+ if (yield Task.spawn(predicate)) {
+ dump(`Predicate returned true after ${i} tries.\n`);
+ return;
+ }
+ yield 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");
+};
+
+/**
+ * Waits for the `browser-delayed-startup-finished` observer notification
+ * to be fired on the specified window.
+ */
+exports.waitForDelayedStartupFinished = function (window) {
+ return observeOnce("browser-delayed-startup-finished", { expectedSubject: window });
+};
diff --git a/devtools/client/performance/test/js_simpleWorker.js b/devtools/client/performance/test/js_simpleWorker.js
new file mode 100644
index 000000000..5d254dfb3
--- /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();
+}, false);
diff --git a/devtools/client/performance/test/moz.build b/devtools/client/performance/test/moz.build
new file mode 100644
index 000000000..6bdf1a018
--- /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/unit/.eslintrc.js b/devtools/client/performance/test/unit/.eslintrc.js
new file mode 100644
index 000000000..aec096a0f
--- /dev/null
+++ b/devtools/client/performance/test/unit/.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/unit/head.js b/devtools/client/performance/test/unit/head.js
new file mode 100644
index 000000000..84128a7e8
--- /dev/null
+++ b/devtools/client/performance/test/unit/head.js
@@ -0,0 +1,46 @@
+/* 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 { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var Services = require("Services");
+var { console } = require("resource://gre/modules/Console.jsm");
+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 (let 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)" }
+ ]
+ });
+
+ let uniqueStacks = new RecordingUtils.UniqueStacks();
+ return RecordingUtils.deflateThread({
+ samples: samples,
+ markers: []
+ }, uniqueStacks);
+}
diff --git a/devtools/client/performance/test/unit/test_frame-utils-01.js b/devtools/client/performance/test/unit/test_frame-utils-01.js
new file mode 100644
index 000000000..a85ec9282
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_frame-utils-01.js
@@ -0,0 +1,133 @@
+/* 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);
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ const { computeIsContentAndCategory, parseLocation } = require("devtools/client/performance/modules/logic/frame-utils");
+ let isContent = (frame) => {
+ computeIsContentAndCategory(frame);
+ return frame.isContent;
+ };
+
+ for (let frame of CONTENT_LOCATIONS) {
+ ok(isContent.apply(null, frameify(frame)),
+ `${frame[0]} should be considered a content frame.`);
+ }
+
+ for (let 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++) {
+ let 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++) {
+ let 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/unit/test_frame-utils-02.js b/devtools/client/performance/test/unit/test_frame-utils-02.js
new file mode 100644
index 000000000..ef0d275bd
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_frame-utils-02.js
@@ -0,0 +1,59 @@
+/* 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.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ let FrameUtils = require("devtools/client/performance/modules/logic/frame-utils");
+
+ let 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/unit/test_jit-graph-data.js b/devtools/client/performance/test/unit/test_jit-graph-data.js
new file mode 100644
index 000000000..b298f4bcc
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_jit-graph-data.js
@@ -0,0 +1,209 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Unit test for `createTierGraphDataFromFrameNode` function.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+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() {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+ let { createTierGraphDataFromFrameNode } = require("devtools/client/performance/modules/logic/jit");
+
+ // Select the second half of the set of samples
+ let startTime = (SAMPLE_COUNT / 2 * TIME_PER_SAMPLE) - TIME_OFFSET;
+ let endTime = (SAMPLE_COUNT * TIME_PER_SAMPLE) - TIME_OFFSET;
+ let invertTree = true;
+
+ let 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");
+
+ let frame = getFrameNodePath(root, "X");
+ let data = createTierGraphDataFromFrameNode(frame, root.sampleTimes,
+ (endTime - startTime) / RESOLUTION);
+
+ let 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.
+ let 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) {
+ let 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 () {
+ let samples = [];
+
+ for (let i = 0; i < SAMPLE_COUNT;) {
+ let 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;
+
+ let 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/unit/test_jit-model-01.js b/devtools/client/performance/test/unit/test_jit-model-01.js
new file mode 100644
index 000000000..da50f293c
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_jit-model-01.js
@@ -0,0 +1,120 @@
+/* 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.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ let { JITOptimizations } = require("devtools/client/performance/modules/logic/jit");
+
+ let rawSites = [];
+ rawSites.push(gRawSite2);
+ rawSites.push(gRawSite2);
+ rawSites.push(gRawSite1);
+ rawSites.push(gRawSite1);
+ rawSites.push(gRawSite2);
+ rawSites.push(gRawSite3);
+
+ let jit = new JITOptimizations(rawSites, gStringTable.stringTable);
+ let sites = jit.optimizationSites;
+
+ let [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/unit/test_jit-model-02.js b/devtools/client/performance/test/unit/test_jit-model-02.js
new file mode 100644
index 000000000..19373e399
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_jit-model-02.js
@@ -0,0 +1,149 @@
+/* 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.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ let {
+ JITOptimizations, hasSuccessfulOutcome, isSuccessfulOutcome, SUCCESSFUL_OUTCOMES
+ } = require("devtools/client/performance/modules/logic/jit");
+
+ let rawSites = [];
+ rawSites.push(gRawSite2);
+ rawSites.push(gRawSite2);
+ rawSites.push(gRawSite1);
+ rawSites.push(gRawSite1);
+ rawSites.push(gRawSite2);
+ rawSites.push(gRawSite3);
+
+ let jit = new JITOptimizations(rawSites, gStringTable.stringTable);
+ let sites = jit.optimizationSites;
+
+ let [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/unit/test_marker-blueprint.js b/devtools/client/performance/test/unit/test_marker-blueprint.js
new file mode 100644
index 000000000..b3db47c0f
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_marker-blueprint.js
@@ -0,0 +1,29 @@
+/* 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.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ let { 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 (let 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/unit/test_marker-utils.js b/devtools/client/performance/test/unit/test_marker-utils.js
new file mode 100644
index 000000000..6fc06efbe
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_marker-utils.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests the marker utils methods.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ let { TIMELINE_BLUEPRINT } = require("devtools/client/performance/modules/markers");
+ let { PREFS } = require("devtools/client/performance/modules/global");
+ let { 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: Ci.nsIDOMEvent.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/unit/test_perf-utils-allocations-to-samples.js b/devtools/client/performance/test/unit/test_perf-utils-allocations-to-samples.js
new file mode 100644
index 000000000..2b114ab82
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_perf-utils-allocations-to-samples.js
@@ -0,0 +1,96 @@
+/* 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.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ const { getProfileThreadFromAllocations } = require("devtools/shared/performance/recording-utils");
+ let output = getProfileThreadFromAllocations(TEST_DATA);
+ equal(output.toSource(), EXPECTED_OUTPUT.toSource(), "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
+ }
+ ]
+};
+
+/* eslint-disable no-inline-comments */
+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"
+ ],
+};
+/* eslint-enable no-inline-comments */
diff --git a/devtools/client/performance/test/unit/test_profiler-categories.js b/devtools/client/performance/test/unit/test_profiler-categories.js
new file mode 100644
index 000000000..7ba288167
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_profiler-categories.js
@@ -0,0 +1,38 @@
+/* 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.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ let { CATEGORIES, CATEGORY_MAPPINGS } = require("devtools/client/performance/modules/categories");
+ let { L10N } = require("devtools/client/performance/modules/global");
+ let 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.");
+
+ ok(Object.keys(CATEGORY_MAPPINGS).every(e => (Number(e) >= 9000 && Number(e) <= 9999) ||
+ Number.isInteger(Math.log2(e))),
+ "All bitmask mappings keys are powers of 2, or between 9000-9999 for special " +
+ "categories.");
+
+ ok(Object.keys(CATEGORY_MAPPINGS).every(e => CATEGORIES.indexOf(CATEGORY_MAPPINGS[e])
+ !== -1),
+ "All bitmask mappings point to a category.");
+});
diff --git a/devtools/client/performance/test/unit/test_tree-model-01.js b/devtools/client/performance/test/unit/test_tree-model-01.js
new file mode 100644
index 000000000..cac397795
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-01.js
@@ -0,0 +1,160 @@
+/* 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.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ const { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+
+ // Create a root node from a given samples array.
+
+ let threadNode = new ThreadNode(gThread, { startTime: 0, endTime: 20 });
+ let 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/unit/test_tree-model-02.js b/devtools/client/performance/test/unit/test_tree-model-02.js
new file mode 100644
index 000000000..2cbff11be
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-02.js
@@ -0,0 +1,62 @@
+/* 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.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+
+ // Create a root node from a given samples array.
+
+ let thread = new ThreadNode(gThread, { startTime: 0, endTime: 10 });
+ let 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/unit/test_tree-model-03.js b/devtools/client/performance/test/unit/test_tree-model-03.js
new file mode 100644
index 000000000..dad90710a
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-03.js
@@ -0,0 +1,95 @@
+/* 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.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ let { 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.
+ let startTime = 5;
+ let endTime = 18;
+ let thread = new ThreadNode(gThread, { startTime, endTime });
+ let 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/unit/test_tree-model-04.js b/devtools/client/performance/test/unit/test_tree-model-04.js
new file mode 100644
index 000000000..6bf69111e
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-04.js
@@ -0,0 +1,91 @@
+/* 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.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+
+ // Create a root node from a given samples array, filtering by time.
+
+ let startTime = 5;
+ let endTime = 18;
+ let thread = new ThreadNode(gThread, { startTime, endTime, contentOnly: true });
+ let 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/unit/test_tree-model-05.js b/devtools/client/performance/test/unit/test_tree-model-05.js
new file mode 100644
index 000000000..3b9470798
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-05.js
@@ -0,0 +1,82 @@
+/* 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" }
+ ]
+}]);
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+
+ let 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");
+
+ let 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");
+
+ let 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/unit/test_tree-model-06.js b/devtools/client/performance/test/unit/test_tree-model-06.js
new file mode 100644
index 000000000..7a678852c
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-06.js
@@ -0,0 +1,176 @@
+/* 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.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+ let root = getFrameNodePath(new ThreadNode(gThread, { startTime: 0,
+ endTime: 30 }), "(root)");
+
+ let A = getFrameNodePath(root, "A");
+ let B = getFrameNodePath(A, "B");
+ let C = getFrameNodePath(B, "C");
+ let Aopts = A.getOptimizations();
+ let Bopts = B.getOptimizations();
+ let 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.
+ let optSitesObserved = new Set();
+ for (let 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;
+
+ let 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/unit/test_tree-model-07.js b/devtools/client/performance/test/unit/test_tree-model-07.js
new file mode 100644
index 000000000..2ea08c5ca
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-07.js
@@ -0,0 +1,101 @@
+/* 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_MASK } = require("devtools/client/performance/modules/categories");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+ let url = (n) => `http://content/${n}`;
+
+ // Create a root node from a given samples array.
+
+ let 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, "64"), "root has platform generalized child");
+ equal(getFrameNodePath(root, "64").calls.length, 0,
+ "platform generalized child is a leaf.");
+
+ ok(getFrameNodePath(root, `${url("A")} > 128`),
+ "A has platform generalized child of another type");
+ equal(getFrameNodePath(root, `${url("A")} > 128`).calls.length, 0,
+ "second generalized type is a leaf.");
+
+ ok(getFrameNodePath(root, `${url("A")} > ${url("E")} > ${url("F")} > 64`),
+ "a second leaf of the first generalized type exists deep in the tree.");
+ ok(getFrameNodePath(root, `${url("A")} > 128`),
+ "A has platform generalized child of another type");
+
+ equal(getFrameNodePath(root, "64").category,
+ getFrameNodePath(root, `${url("A")} > ${url("E")} > ${url("F")} > 64`).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_MASK("css") },
+ { location: "http://content/D" }
+ ]
+}, {
+ time: 5 + 6 + 7,
+ frames: [
+ { location: "(root)" },
+ { location: "http://content/A" },
+ { location: "contentY", category: CATEGORY_MASK("css") },
+ { location: "http://content/E" },
+ { location: "http://content/F" },
+ { location: "contentY", category: CATEGORY_MASK("js") },
+ ]
+}, {
+ time: 5 + 20,
+ frames: [
+ { location: "(root)" },
+ { location: "contentX", category: CATEGORY_MASK("js") },
+ ]
+}, {
+ time: 5 + 25,
+ frames: [
+ { location: "(root)" },
+ { location: "http://content/A" },
+ { location: "contentZ", category: CATEGORY_MASK("gc", 1) },
+ ]
+}]);
diff --git a/devtools/client/performance/test/unit/test_tree-model-08.js b/devtools/client/performance/test/unit/test_tree-model-08.js
new file mode 100644
index 000000000..59f7e0d34
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-08.js
@@ -0,0 +1,99 @@
+/* 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.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ let FrameUtils = require("devtools/client/performance/modules/logic/frame-utils");
+ let { FrameNode } = require("devtools/client/performance/modules/logic/tree-model");
+ let { CATEGORY_MASK } = require("devtools/client/performance/modules/categories");
+ let compute = frame => {
+ FrameUtils.computeIsContentAndCategory(frame);
+ return frame;
+ };
+
+ let 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_MASK("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),
+ ];
+
+ let fields = ["nodeType", "functionName", "fileName", "host", "url", "line", "column",
+ "categoryData.abbrev", "isContent", "port"];
+ let 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++) {
+ let info = frames[i].getInfo();
+ let expect = expected[i];
+
+ for (let j = 0; j < fields.length; j++) {
+ let field = fields[j];
+ let 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/unit/test_tree-model-09.js b/devtools/client/performance/test/unit/test_tree-model-09.js
new file mode 100644
index 000000000..1bf267227
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-09.js
@@ -0,0 +1,84 @@
+/* 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.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+ let url = (n) => `http://content/${n}`;
+
+ // Create a root node from a given samples array.
+
+ let 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, "9000"),
+ "root has platform generalized child from Chrome JS");
+ equal(getFrameNodePath(root, "9000").calls.length, 0,
+ "platform generalized child is a leaf.");
+
+ ok(getFrameNodePath(root, `${url("A")} > ${url("E")} > ${url("F")} > 9000`),
+ "a second leaf of the generalized Chrome JS exists.");
+
+ equal(getFrameNodePath(root, "9000").category,
+ getFrameNodePath(root, `${url("A")} > ${url("E")} > ${url("F")} > 9000`).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/unit/test_tree-model-10.js b/devtools/client/performance/test/unit/test_tree-model-10.js
new file mode 100644
index 000000000..9553c7052
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-10.js
@@ -0,0 +1,153 @@
+/* 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
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+ let 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) {
+ let [total, self, name, children] = def;
+ let node = getFrameNodePath(parent, name);
+ let 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/unit/test_tree-model-11.js b/devtools/client/performance/test/unit/test_tree-model-11.js
new file mode 100644
index 000000000..c665dfe32
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-11.js
@@ -0,0 +1,90 @@
+/* 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.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+ let 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) {
+ let [total, self, name, children] = def;
+ let node = getFrameNodePath(parent, name);
+ let 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/unit/test_tree-model-12.js b/devtools/client/performance/test/unit/test_tree-model-12.js
new file mode 100644
index 000000000..fde96e349
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-12.js
@@ -0,0 +1,94 @@
+/* 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.
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+ let thread = new ThreadNode(gThread, { startTime: 0, endTime: 50 });
+ let 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/unit/test_tree-model-13.js b/devtools/client/performance/test/unit/test_tree-model-13.js
new file mode 100644
index 000000000..a1aa666f1
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-13.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Like test_tree-model-12, but inverted.
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+ let 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/unit/test_tree-model-allocations-01.js b/devtools/client/performance/test/unit/test_tree-model-allocations-01.js
new file mode 100644
index 000000000..331a625f9
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-allocations-01.js
@@ -0,0 +1,95 @@
+/* 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.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+ const { getProfileThreadFromAllocations } = require("devtools/shared/performance/recording-utils");
+ let allocationData = getProfileThreadFromAllocations(TEST_DATA);
+ let 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;
+ let fields = [
+ "selfSize", "selfSizePercentage", "selfCount", "selfCountPercentage",
+ "totalSize", "totalSizePercentage", "totalCount", "totalCountPercentage"
+ ];
+ return function (def) {
+ let children;
+ if (Array.isArray(def[def.length - 1])) {
+ children = def.pop();
+ }
+ let name = def.pop();
+ let expected = def;
+
+ let node = getFrameNodePath(parent, name);
+ let 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/unit/test_tree-model-allocations-02.js b/devtools/client/performance/test/unit/test_tree-model-allocations-02.js
new file mode 100644
index 000000000..cfc5c4048
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-allocations-02.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 tree model calculates correct costs/percentages for
+ * allocation frame nodes. Inverted version of test_tree-model-allocations-01.js
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+ const { getProfileThreadFromAllocations } = require("devtools/shared/performance/recording-utils");
+ let allocationData = getProfileThreadFromAllocations(TEST_DATA);
+ let 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;
+ let fields = [
+ "selfSize", "selfSizePercentage", "selfCount", "selfCountPercentage",
+ "totalSize", "totalSizePercentage", "totalCount", "totalCountPercentage"
+ ];
+
+ return function (def) {
+ let children;
+
+ if (Array.isArray(def[def.length - 1])) {
+ children = def.pop();
+ }
+
+ let name = def.pop();
+ let expected = def;
+
+ let node = getFrameNodePath(parent, name);
+ let 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/unit/test_waterfall-utils-collapse-01.js b/devtools/client/performance/test/unit/test_waterfall-utils-collapse-01.js
new file mode 100644
index 000000000..e329622db
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_waterfall-utils-collapse-01.js
@@ -0,0 +1,71 @@
+/* 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.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ const WaterfallUtils = require("devtools/client/performance/modules/logic/waterfall-utils");
+
+ let rootMarkerNode = WaterfallUtils.createParentNode({ name: "(root)" });
+
+ WaterfallUtils.collapseMarkersIntoNode({
+ rootNode: rootMarkerNode,
+ markersList: gTestMarkers
+ });
+
+ function compare(marker, expected) {
+ for (let 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/unit/test_waterfall-utils-collapse-02.js b/devtools/client/performance/test/unit/test_waterfall-utils-collapse-02.js
new file mode 100644
index 000000000..1cc33f45a
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_waterfall-utils-collapse-02.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 for console.time/console.timeEnd
+ * markers, as they should ignore any sort of collapsing.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ const WaterfallUtils = require("devtools/client/performance/modules/logic/waterfall-utils");
+
+ let rootMarkerNode = WaterfallUtils.createParentNode({ name: "(root)" });
+
+ WaterfallUtils.collapseMarkersIntoNode({
+ rootNode: rootMarkerNode,
+ markersList: gTestMarkers
+ });
+
+ function compare(marker, expected) {
+ for (let 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/unit/test_waterfall-utils-collapse-03.js b/devtools/client/performance/test/unit/test_waterfall-utils-collapse-03.js
new file mode 100644
index 000000000..00b6d2db0
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_waterfall-utils-collapse-03.js
@@ -0,0 +1,64 @@
+/* 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.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ const WaterfallUtils = require("devtools/client/performance/modules/logic/waterfall-utils");
+
+ let rootMarkerNode = WaterfallUtils.createParentNode({ name: "(root)" });
+
+ WaterfallUtils.collapseMarkersIntoNode({
+ rootNode: rootMarkerNode,
+ markersList: gTestMarkers
+ });
+
+ function compare(marker, expected) {
+ for (let 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/unit/test_waterfall-utils-collapse-04.js b/devtools/client/performance/test/unit/test_waterfall-utils-collapse-04.js
new file mode 100644
index 000000000..916a3b1d4
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_waterfall-utils-collapse-04.js
@@ -0,0 +1,103 @@
+/* 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.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ const WaterfallUtils = require("devtools/client/performance/modules/logic/waterfall-utils");
+
+ [
+ [["DOMEvent"], gExpectedOutputNoDOMEvent],
+ [["Javascript"], gExpectedOutputNoJS],
+ [["DOMEvent", "Javascript"], gExpectedOutputNoDOMEventOrJS],
+ ].forEach(([filter, expected]) => {
+ let rootMarkerNode = WaterfallUtils.createParentNode({ name: "(root)" });
+
+ WaterfallUtils.collapseMarkersIntoNode({
+ rootNode: rootMarkerNode,
+ markersList: gTestMarkers,
+ filter
+ });
+
+ compare(rootMarkerNode, expected);
+ });
+
+ function compare(marker, expected) {
+ for (let 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/unit/test_waterfall-utils-collapse-05.js b/devtools/client/performance/test/unit/test_waterfall-utils-collapse-05.js
new file mode 100644
index 000000000..ba85c2adc
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_waterfall-utils-collapse-05.js
@@ -0,0 +1,164 @@
+/* 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.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ const WaterfallUtils = require("devtools/client/performance/modules/logic/waterfall-utils");
+
+ let rootMarkerNode = WaterfallUtils.createParentNode({ name: "(root)" });
+
+ WaterfallUtils.collapseMarkersIntoNode({
+ rootNode: rootMarkerNode,
+ markersList: gTestMarkers
+ });
+
+ compare(rootMarkerNode, gExpectedOutput);
+
+ function compare(marker, expected) {
+ for (let 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/unit/xpcshell.ini b/devtools/client/performance/test/unit/xpcshell.ini
new file mode 100644
index 000000000..b9d0c1403
--- /dev/null
+++ b/devtools/client/performance/test/unit/xpcshell.ini
@@ -0,0 +1,36 @@
+[DEFAULT]
+tags = devtools
+head = head.js
+tail =
+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]