diff options
Diffstat (limited to 'devtools/client/performance/test')
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] |