From 5f8de423f190bbb79a62f804151bc24824fa32d8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 04:16:08 -0500 Subject: Add m-esr52 at 52.6.0 --- .../components/jit-optimizations-item.js | 175 ++++++ .../performance/components/jit-optimizations.js | 248 +++++++++ devtools/client/performance/components/moz.build | 19 + .../performance/components/recording-button.js | 37 ++ .../performance/components/recording-controls.js | 54 ++ .../performance/components/recording-list-item.js | 49 ++ .../performance/components/recording-list.js | 23 + .../client/performance/components/test/chrome.ini | 5 + .../client/performance/components/test/head.js | 187 +++++++ .../components/test/test_jit_optimizations_01.html | 70 +++ .../performance/components/waterfall-header.js | 69 +++ .../performance/components/waterfall-tree-row.js | 107 ++++ .../performance/components/waterfall-tree.js | 167 ++++++ .../client/performance/components/waterfall.js | 36 ++ devtools/client/performance/docs/markers.md | 189 +++++++ devtools/client/performance/events.js | 108 ++++ devtools/client/performance/legacy/actors.js | 263 +++++++++ .../client/performance/legacy/compatibility.js | 66 +++ devtools/client/performance/legacy/front.js | 484 +++++++++++++++++ devtools/client/performance/legacy/moz.build | 12 + devtools/client/performance/legacy/recording.js | 174 ++++++ devtools/client/performance/modules/categories.js | 128 +++++ devtools/client/performance/modules/constants.js | 11 + devtools/client/performance/modules/global.js | 36 ++ devtools/client/performance/modules/io.js | 171 ++++++ .../performance/modules/logic/frame-utils.js | 478 +++++++++++++++++ devtools/client/performance/modules/logic/jit.js | 342 ++++++++++++ .../client/performance/modules/logic/moz.build | 12 + .../client/performance/modules/logic/telemetry.js | 122 +++++ .../client/performance/modules/logic/tree-model.js | 556 +++++++++++++++++++ .../performance/modules/logic/waterfall-utils.js | 167 ++++++ .../performance/modules/marker-blueprint-utils.js | 104 ++++ .../client/performance/modules/marker-dom-utils.js | 257 +++++++++ .../performance/modules/marker-formatters.js | 199 +++++++ devtools/client/performance/modules/markers.js | 170 ++++++ devtools/client/performance/modules/moz.build | 22 + devtools/client/performance/modules/utils.js | 21 + .../client/performance/modules/waterfall-ticks.js | 98 ++++ .../client/performance/modules/widgets/graphs.js | 514 ++++++++++++++++++ .../performance/modules/widgets/marker-details.js | 164 ++++++ .../modules/widgets/markers-overview.js | 243 +++++++++ .../client/performance/modules/widgets/moz.build | 11 + .../performance/modules/widgets/tree-view.js | 406 ++++++++++++++ devtools/client/performance/moz.build | 19 + devtools/client/performance/panel.js | 100 ++++ .../client/performance/performance-controller.js | 595 +++++++++++++++++++++ devtools/client/performance/performance-view.js | 411 ++++++++++++++ devtools/client/performance/performance.xul | 368 +++++++++++++ devtools/client/performance/test/.eslintrc.js | 6 + devtools/client/performance/test/browser.ini | 124 +++++ .../test/browser_aaa-run-first-leaktest.js | 28 + .../performance/test/browser_perf-button-states.js | 76 +++ .../test/browser_perf-calltree-js-categories.js | 60 +++ .../test/browser_perf-calltree-js-columns.js | 66 +++ .../test/browser_perf-calltree-js-events.js | 58 ++ .../test/browser_perf-calltree-memory-columns.js | 69 +++ .../test/browser_perf-console-record-01.js | 43 ++ .../test/browser_perf-console-record-02.js | 70 +++ .../test/browser_perf-console-record-03.js | 58 ++ .../test/browser_perf-console-record-04.js | 58 ++ .../test/browser_perf-console-record-05.js | 92 ++++ .../test/browser_perf-console-record-06.js | 96 ++++ .../test/browser_perf-console-record-07.js | 170 ++++++ .../test/browser_perf-console-record-08.js | 268 ++++++++++ .../test/browser_perf-console-record-09.js | 64 +++ .../test/browser_perf-details-01-toggle.js | 67 +++ .../test/browser_perf-details-02-utility-fun.js | 59 ++ .../browser_perf-details-03-without-allocations.js | 127 +++++ .../browser_perf-details-04-toolbar-buttons.js | 145 +++++ .../test/browser_perf-details-05-preserve-view.js | 50 ++ ...rowser_perf-details-06-rerender-on-selection.js | 79 +++ .../test/browser_perf-details-07-bleed-events.js | 48 ++ .../browser_perf-details-render-00-waterfall.js | 40 ++ .../browser_perf-details-render-01-js-calltree.js | 40 ++ ...browser_perf-details-render-02-js-flamegraph.js | 40 ++ ...owser_perf-details-render-03-memory-calltree.js | 44 ++ ...ser_perf-details-render-04-memory-flamegraph.js | 45 ++ .../performance/test/browser_perf-docload.js | 43 ++ .../performance/test/browser_perf-gc-snap.js | 146 +++++ .../performance/test/browser_perf-highlighted.js | 48 ++ .../performance/test/browser_perf-loading-01.js | 52 ++ .../performance/test/browser_perf-loading-02.js | 82 +++ .../test/browser_perf-marker-details.js | 146 +++++ .../test/browser_perf-options-01-toggle-throw.js | 31 ++ .../browser_perf-options-02-toggle-throw-alt.js | 38 ++ .../test/browser_perf-options-03-toggle-meta.js | 38 ++ .../browser_perf-options-enable-framerate-01.js | 52 ++ .../browser_perf-options-enable-framerate-02.js | 43 ++ .../test/browser_perf-options-enable-memory-01.js | 58 ++ .../test/browser_perf-options-enable-memory-02.js | 49 ++ ...owser_perf-options-flatten-tree-recursion-01.js | 73 +++ ...owser_perf-options-flatten-tree-recursion-02.js | 86 +++ .../browser_perf-options-invert-call-tree-01.js | 43 ++ .../browser_perf-options-invert-call-tree-02.js | 45 ++ .../browser_perf-options-invert-flame-graph-01.js | 43 ++ .../browser_perf-options-invert-flame-graph-02.js | 46 ++ .../browser_perf-options-propagate-allocations.js | 36 ++ .../browser_perf-options-propagate-profiler.js | 32 ++ .../browser_perf-options-show-idle-blocks-01.js | 43 ++ .../browser_perf-options-show-idle-blocks-02.js | 45 ++ .../browser_perf-options-show-jit-optimizations.js | 260 +++++++++ .../browser_perf-options-show-platform-data-01.js | 43 ++ .../browser_perf-options-show-platform-data-02.js | 43 ++ .../test/browser_perf-overview-render-01.js | 34 ++ .../test/browser_perf-overview-render-02.js | 91 ++++ .../test/browser_perf-overview-render-03.js | 76 +++ .../test/browser_perf-overview-render-04.js | 74 +++ .../test/browser_perf-overview-selection-01.js | 71 +++ .../test/browser_perf-overview-selection-02.js | 73 +++ .../test/browser_perf-overview-selection-03.js | 82 +++ .../test/browser_perf-overview-time-interval.js | 73 +++ .../test/browser_perf-private-browsing.js | 114 ++++ .../test/browser_perf-range-changed-render.js | 81 +++ .../test/browser_perf-recording-notices-01.js | 45 ++ .../test/browser_perf-recording-notices-02.js | 65 +++ .../test/browser_perf-recording-notices-03.js | 135 +++++ .../test/browser_perf-recording-notices-04.js | 66 +++ .../test/browser_perf-recording-notices-05.js | 54 ++ .../test/browser_perf-recording-selected-01.js | 45 ++ .../test/browser_perf-recording-selected-02.js | 58 ++ .../test/browser_perf-recording-selected-03.js | 44 ++ .../test/browser_perf-recording-selected-04.js | 59 ++ .../test/browser_perf-recordings-clear-01.js | 54 ++ .../test/browser_perf-recordings-clear-02.js | 69 +++ .../test/browser_perf-recordings-io-01.js | 94 ++++ .../test/browser_perf-recordings-io-02.js | 26 + .../test/browser_perf-recordings-io-03.js | 56 ++ .../test/browser_perf-recordings-io-04.js | 178 ++++++ .../test/browser_perf-recordings-io-05.js | 43 ++ .../test/browser_perf-recordings-io-06.js | 142 +++++ .../performance/test/browser_perf-refresh.js | 36 ++ .../client/performance/test/browser_perf-states.js | 102 ++++ .../performance/test/browser_perf-telemetry-01.js | 53 ++ .../performance/test/browser_perf-telemetry-02.js | 48 ++ .../performance/test/browser_perf-telemetry-03.js | 56 ++ .../performance/test/browser_perf-telemetry-04.js | 50 ++ .../performance/test/browser_perf-theme-toggle.js | 78 +++ .../test/browser_perf-tree-abstract-01.js | 154 ++++++ .../test/browser_perf-tree-abstract-02.js | 138 +++++ .../test/browser_perf-tree-abstract-03.js | 151 ++++++ .../test/browser_perf-tree-abstract-04.js | 35 ++ .../test/browser_perf-tree-abstract-05.js | 103 ++++ .../performance/test/browser_perf-tree-view-01.js | 65 +++ .../performance/test/browser_perf-tree-view-02.js | 148 +++++ .../performance/test/browser_perf-tree-view-03.js | 79 +++ .../performance/test/browser_perf-tree-view-04.js | 78 +++ .../performance/test/browser_perf-tree-view-05.js | 36 ++ .../performance/test/browser_perf-tree-view-06.js | 52 ++ .../performance/test/browser_perf-tree-view-07.js | 40 ++ .../performance/test/browser_perf-tree-view-08.js | 109 ++++ .../performance/test/browser_perf-tree-view-09.js | 59 ++ .../performance/test/browser_perf-tree-view-10.js | 160 ++++++ .../performance/test/browser_perf-tree-view-11.js | 154 ++++++ .../performance/test/browser_perf-ui-recording.js | 39 ++ .../test/browser_timeline-filters-01.js | 119 +++++ .../test/browser_timeline-filters-02.js | 48 ++ .../test/browser_timeline-waterfall-background.js | 41 ++ .../test/browser_timeline-waterfall-generic.js | 105 ++++ .../test/browser_timeline-waterfall-rerender.js | 76 +++ .../test/browser_timeline-waterfall-sidebar.js | 77 +++ .../test/browser_timeline-waterfall-workers.js | 97 ++++ devtools/client/performance/test/doc_allocs.html | 26 + .../client/performance/test/doc_innerHTML.html | 21 + devtools/client/performance/test/doc_markers.html | 38 ++ .../client/performance/test/doc_simple-test.html | 27 + devtools/client/performance/test/doc_worker.html | 29 + devtools/client/performance/test/head.js | 93 ++++ .../client/performance/test/helpers/actions.js | 155 ++++++ .../client/performance/test/helpers/dom-utils.js | 30 ++ .../client/performance/test/helpers/event-utils.js | 114 ++++ .../client/performance/test/helpers/input-utils.js | 75 +++ devtools/client/performance/test/helpers/moz.build | 20 + .../client/performance/test/helpers/panel-utils.js | 106 ++++ devtools/client/performance/test/helpers/prefs.js | 72 +++ .../performance/test/helpers/profiler-mm-utils.js | 117 ++++ .../performance/test/helpers/recording-utils.js | 54 ++ .../client/performance/test/helpers/synth-utils.js | 99 ++++ .../client/performance/test/helpers/tab-utils.js | 85 +++ devtools/client/performance/test/helpers/urls.js | 6 + .../client/performance/test/helpers/wait-utils.js | 61 +++ .../client/performance/test/js_simpleWorker.js | 6 + devtools/client/performance/test/moz.build | 8 + devtools/client/performance/test/unit/.eslintrc.js | 6 + devtools/client/performance/test/unit/head.js | 46 ++ .../performance/test/unit/test_frame-utils-01.js | 133 +++++ .../performance/test/unit/test_frame-utils-02.js | 59 ++ .../performance/test/unit/test_jit-graph-data.js | 209 ++++++++ .../performance/test/unit/test_jit-model-01.js | 120 +++++ .../performance/test/unit/test_jit-model-02.js | 149 ++++++ .../performance/test/unit/test_marker-blueprint.js | 29 + .../performance/test/unit/test_marker-utils.js | 115 ++++ .../unit/test_perf-utils-allocations-to-samples.js | 96 ++++ .../test/unit/test_profiler-categories.js | 38 ++ .../performance/test/unit/test_tree-model-01.js | 160 ++++++ .../performance/test/unit/test_tree-model-02.js | 62 +++ .../performance/test/unit/test_tree-model-03.js | 95 ++++ .../performance/test/unit/test_tree-model-04.js | 91 ++++ .../performance/test/unit/test_tree-model-05.js | 82 +++ .../performance/test/unit/test_tree-model-06.js | 176 ++++++ .../performance/test/unit/test_tree-model-07.js | 101 ++++ .../performance/test/unit/test_tree-model-08.js | 99 ++++ .../performance/test/unit/test_tree-model-09.js | 84 +++ .../performance/test/unit/test_tree-model-10.js | 153 ++++++ .../performance/test/unit/test_tree-model-11.js | 90 ++++ .../performance/test/unit/test_tree-model-12.js | 94 ++++ .../performance/test/unit/test_tree-model-13.js | 86 +++ .../test/unit/test_tree-model-allocations-01.js | 95 ++++ .../test/unit/test_tree-model-allocations-02.js | 105 ++++ .../test/unit/test_waterfall-utils-collapse-01.js | 71 +++ .../test/unit/test_waterfall-utils-collapse-02.js | 82 +++ .../test/unit/test_waterfall-utils-collapse-03.js | 64 +++ .../test/unit/test_waterfall-utils-collapse-04.js | 103 ++++ .../test/unit/test_waterfall-utils-collapse-05.js | 164 ++++++ devtools/client/performance/test/unit/xpcshell.ini | 36 ++ .../performance/views/details-abstract-subview.js | 194 +++++++ .../performance/views/details-js-call-tree.js | 193 +++++++ .../performance/views/details-js-flamegraph.js | 125 +++++ .../performance/views/details-memory-call-tree.js | 130 +++++ .../performance/views/details-memory-flamegraph.js | 121 +++++ .../client/performance/views/details-waterfall.js | 252 +++++++++ devtools/client/performance/views/details.js | 263 +++++++++ devtools/client/performance/views/overview.js | 423 +++++++++++++++ devtools/client/performance/views/recordings.js | 202 +++++++ devtools/client/performance/views/toolbar.js | 160 ++++++ 224 files changed, 23107 insertions(+) create mode 100644 devtools/client/performance/components/jit-optimizations-item.js create mode 100644 devtools/client/performance/components/jit-optimizations.js create mode 100644 devtools/client/performance/components/moz.build create mode 100644 devtools/client/performance/components/recording-button.js create mode 100644 devtools/client/performance/components/recording-controls.js create mode 100644 devtools/client/performance/components/recording-list-item.js create mode 100644 devtools/client/performance/components/recording-list.js create mode 100644 devtools/client/performance/components/test/chrome.ini create mode 100644 devtools/client/performance/components/test/head.js create mode 100644 devtools/client/performance/components/test/test_jit_optimizations_01.html create mode 100644 devtools/client/performance/components/waterfall-header.js create mode 100644 devtools/client/performance/components/waterfall-tree-row.js create mode 100644 devtools/client/performance/components/waterfall-tree.js create mode 100644 devtools/client/performance/components/waterfall.js create mode 100644 devtools/client/performance/docs/markers.md create mode 100644 devtools/client/performance/events.js create mode 100644 devtools/client/performance/legacy/actors.js create mode 100644 devtools/client/performance/legacy/compatibility.js create mode 100644 devtools/client/performance/legacy/front.js create mode 100644 devtools/client/performance/legacy/moz.build create mode 100644 devtools/client/performance/legacy/recording.js create mode 100644 devtools/client/performance/modules/categories.js create mode 100644 devtools/client/performance/modules/constants.js create mode 100644 devtools/client/performance/modules/global.js create mode 100644 devtools/client/performance/modules/io.js create mode 100644 devtools/client/performance/modules/logic/frame-utils.js create mode 100644 devtools/client/performance/modules/logic/jit.js create mode 100644 devtools/client/performance/modules/logic/moz.build create mode 100644 devtools/client/performance/modules/logic/telemetry.js create mode 100644 devtools/client/performance/modules/logic/tree-model.js create mode 100644 devtools/client/performance/modules/logic/waterfall-utils.js create mode 100644 devtools/client/performance/modules/marker-blueprint-utils.js create mode 100644 devtools/client/performance/modules/marker-dom-utils.js create mode 100644 devtools/client/performance/modules/marker-formatters.js create mode 100644 devtools/client/performance/modules/markers.js create mode 100644 devtools/client/performance/modules/moz.build create mode 100644 devtools/client/performance/modules/utils.js create mode 100644 devtools/client/performance/modules/waterfall-ticks.js create mode 100644 devtools/client/performance/modules/widgets/graphs.js create mode 100644 devtools/client/performance/modules/widgets/marker-details.js create mode 100644 devtools/client/performance/modules/widgets/markers-overview.js create mode 100644 devtools/client/performance/modules/widgets/moz.build create mode 100644 devtools/client/performance/modules/widgets/tree-view.js create mode 100644 devtools/client/performance/moz.build create mode 100644 devtools/client/performance/panel.js create mode 100644 devtools/client/performance/performance-controller.js create mode 100644 devtools/client/performance/performance-view.js create mode 100644 devtools/client/performance/performance.xul create mode 100644 devtools/client/performance/test/.eslintrc.js create mode 100644 devtools/client/performance/test/browser.ini create mode 100644 devtools/client/performance/test/browser_aaa-run-first-leaktest.js create mode 100644 devtools/client/performance/test/browser_perf-button-states.js create mode 100644 devtools/client/performance/test/browser_perf-calltree-js-categories.js create mode 100644 devtools/client/performance/test/browser_perf-calltree-js-columns.js create mode 100644 devtools/client/performance/test/browser_perf-calltree-js-events.js create mode 100644 devtools/client/performance/test/browser_perf-calltree-memory-columns.js create mode 100644 devtools/client/performance/test/browser_perf-console-record-01.js create mode 100644 devtools/client/performance/test/browser_perf-console-record-02.js create mode 100644 devtools/client/performance/test/browser_perf-console-record-03.js create mode 100644 devtools/client/performance/test/browser_perf-console-record-04.js create mode 100644 devtools/client/performance/test/browser_perf-console-record-05.js create mode 100644 devtools/client/performance/test/browser_perf-console-record-06.js create mode 100644 devtools/client/performance/test/browser_perf-console-record-07.js create mode 100644 devtools/client/performance/test/browser_perf-console-record-08.js create mode 100644 devtools/client/performance/test/browser_perf-console-record-09.js create mode 100644 devtools/client/performance/test/browser_perf-details-01-toggle.js create mode 100644 devtools/client/performance/test/browser_perf-details-02-utility-fun.js create mode 100644 devtools/client/performance/test/browser_perf-details-03-without-allocations.js create mode 100644 devtools/client/performance/test/browser_perf-details-04-toolbar-buttons.js create mode 100644 devtools/client/performance/test/browser_perf-details-05-preserve-view.js create mode 100644 devtools/client/performance/test/browser_perf-details-06-rerender-on-selection.js create mode 100644 devtools/client/performance/test/browser_perf-details-07-bleed-events.js create mode 100644 devtools/client/performance/test/browser_perf-details-render-00-waterfall.js create mode 100644 devtools/client/performance/test/browser_perf-details-render-01-js-calltree.js create mode 100644 devtools/client/performance/test/browser_perf-details-render-02-js-flamegraph.js create mode 100644 devtools/client/performance/test/browser_perf-details-render-03-memory-calltree.js create mode 100644 devtools/client/performance/test/browser_perf-details-render-04-memory-flamegraph.js create mode 100644 devtools/client/performance/test/browser_perf-docload.js create mode 100644 devtools/client/performance/test/browser_perf-gc-snap.js create mode 100644 devtools/client/performance/test/browser_perf-highlighted.js create mode 100644 devtools/client/performance/test/browser_perf-loading-01.js create mode 100644 devtools/client/performance/test/browser_perf-loading-02.js create mode 100644 devtools/client/performance/test/browser_perf-marker-details.js create mode 100644 devtools/client/performance/test/browser_perf-options-01-toggle-throw.js create mode 100644 devtools/client/performance/test/browser_perf-options-02-toggle-throw-alt.js create mode 100644 devtools/client/performance/test/browser_perf-options-03-toggle-meta.js create mode 100644 devtools/client/performance/test/browser_perf-options-enable-framerate-01.js create mode 100644 devtools/client/performance/test/browser_perf-options-enable-framerate-02.js create mode 100644 devtools/client/performance/test/browser_perf-options-enable-memory-01.js create mode 100644 devtools/client/performance/test/browser_perf-options-enable-memory-02.js create mode 100644 devtools/client/performance/test/browser_perf-options-flatten-tree-recursion-01.js create mode 100644 devtools/client/performance/test/browser_perf-options-flatten-tree-recursion-02.js create mode 100644 devtools/client/performance/test/browser_perf-options-invert-call-tree-01.js create mode 100644 devtools/client/performance/test/browser_perf-options-invert-call-tree-02.js create mode 100644 devtools/client/performance/test/browser_perf-options-invert-flame-graph-01.js create mode 100644 devtools/client/performance/test/browser_perf-options-invert-flame-graph-02.js create mode 100644 devtools/client/performance/test/browser_perf-options-propagate-allocations.js create mode 100644 devtools/client/performance/test/browser_perf-options-propagate-profiler.js create mode 100644 devtools/client/performance/test/browser_perf-options-show-idle-blocks-01.js create mode 100644 devtools/client/performance/test/browser_perf-options-show-idle-blocks-02.js create mode 100644 devtools/client/performance/test/browser_perf-options-show-jit-optimizations.js create mode 100644 devtools/client/performance/test/browser_perf-options-show-platform-data-01.js create mode 100644 devtools/client/performance/test/browser_perf-options-show-platform-data-02.js create mode 100644 devtools/client/performance/test/browser_perf-overview-render-01.js create mode 100644 devtools/client/performance/test/browser_perf-overview-render-02.js create mode 100644 devtools/client/performance/test/browser_perf-overview-render-03.js create mode 100644 devtools/client/performance/test/browser_perf-overview-render-04.js create mode 100644 devtools/client/performance/test/browser_perf-overview-selection-01.js create mode 100644 devtools/client/performance/test/browser_perf-overview-selection-02.js create mode 100644 devtools/client/performance/test/browser_perf-overview-selection-03.js create mode 100644 devtools/client/performance/test/browser_perf-overview-time-interval.js create mode 100644 devtools/client/performance/test/browser_perf-private-browsing.js create mode 100644 devtools/client/performance/test/browser_perf-range-changed-render.js create mode 100644 devtools/client/performance/test/browser_perf-recording-notices-01.js create mode 100644 devtools/client/performance/test/browser_perf-recording-notices-02.js create mode 100644 devtools/client/performance/test/browser_perf-recording-notices-03.js create mode 100644 devtools/client/performance/test/browser_perf-recording-notices-04.js create mode 100644 devtools/client/performance/test/browser_perf-recording-notices-05.js create mode 100644 devtools/client/performance/test/browser_perf-recording-selected-01.js create mode 100644 devtools/client/performance/test/browser_perf-recording-selected-02.js create mode 100644 devtools/client/performance/test/browser_perf-recording-selected-03.js create mode 100644 devtools/client/performance/test/browser_perf-recording-selected-04.js create mode 100644 devtools/client/performance/test/browser_perf-recordings-clear-01.js create mode 100644 devtools/client/performance/test/browser_perf-recordings-clear-02.js create mode 100644 devtools/client/performance/test/browser_perf-recordings-io-01.js create mode 100644 devtools/client/performance/test/browser_perf-recordings-io-02.js create mode 100644 devtools/client/performance/test/browser_perf-recordings-io-03.js create mode 100644 devtools/client/performance/test/browser_perf-recordings-io-04.js create mode 100644 devtools/client/performance/test/browser_perf-recordings-io-05.js create mode 100644 devtools/client/performance/test/browser_perf-recordings-io-06.js create mode 100644 devtools/client/performance/test/browser_perf-refresh.js create mode 100644 devtools/client/performance/test/browser_perf-states.js create mode 100644 devtools/client/performance/test/browser_perf-telemetry-01.js create mode 100644 devtools/client/performance/test/browser_perf-telemetry-02.js create mode 100644 devtools/client/performance/test/browser_perf-telemetry-03.js create mode 100644 devtools/client/performance/test/browser_perf-telemetry-04.js create mode 100644 devtools/client/performance/test/browser_perf-theme-toggle.js create mode 100644 devtools/client/performance/test/browser_perf-tree-abstract-01.js create mode 100644 devtools/client/performance/test/browser_perf-tree-abstract-02.js create mode 100644 devtools/client/performance/test/browser_perf-tree-abstract-03.js create mode 100644 devtools/client/performance/test/browser_perf-tree-abstract-04.js create mode 100644 devtools/client/performance/test/browser_perf-tree-abstract-05.js create mode 100644 devtools/client/performance/test/browser_perf-tree-view-01.js create mode 100644 devtools/client/performance/test/browser_perf-tree-view-02.js create mode 100644 devtools/client/performance/test/browser_perf-tree-view-03.js create mode 100644 devtools/client/performance/test/browser_perf-tree-view-04.js create mode 100644 devtools/client/performance/test/browser_perf-tree-view-05.js create mode 100644 devtools/client/performance/test/browser_perf-tree-view-06.js create mode 100644 devtools/client/performance/test/browser_perf-tree-view-07.js create mode 100644 devtools/client/performance/test/browser_perf-tree-view-08.js create mode 100644 devtools/client/performance/test/browser_perf-tree-view-09.js create mode 100644 devtools/client/performance/test/browser_perf-tree-view-10.js create mode 100644 devtools/client/performance/test/browser_perf-tree-view-11.js create mode 100644 devtools/client/performance/test/browser_perf-ui-recording.js create mode 100644 devtools/client/performance/test/browser_timeline-filters-01.js create mode 100644 devtools/client/performance/test/browser_timeline-filters-02.js create mode 100644 devtools/client/performance/test/browser_timeline-waterfall-background.js create mode 100644 devtools/client/performance/test/browser_timeline-waterfall-generic.js create mode 100644 devtools/client/performance/test/browser_timeline-waterfall-rerender.js create mode 100644 devtools/client/performance/test/browser_timeline-waterfall-sidebar.js create mode 100644 devtools/client/performance/test/browser_timeline-waterfall-workers.js create mode 100644 devtools/client/performance/test/doc_allocs.html create mode 100644 devtools/client/performance/test/doc_innerHTML.html create mode 100644 devtools/client/performance/test/doc_markers.html create mode 100644 devtools/client/performance/test/doc_simple-test.html create mode 100644 devtools/client/performance/test/doc_worker.html create mode 100644 devtools/client/performance/test/head.js create mode 100644 devtools/client/performance/test/helpers/actions.js create mode 100644 devtools/client/performance/test/helpers/dom-utils.js create mode 100644 devtools/client/performance/test/helpers/event-utils.js create mode 100644 devtools/client/performance/test/helpers/input-utils.js create mode 100644 devtools/client/performance/test/helpers/moz.build create mode 100644 devtools/client/performance/test/helpers/panel-utils.js create mode 100644 devtools/client/performance/test/helpers/prefs.js create mode 100644 devtools/client/performance/test/helpers/profiler-mm-utils.js create mode 100644 devtools/client/performance/test/helpers/recording-utils.js create mode 100644 devtools/client/performance/test/helpers/synth-utils.js create mode 100644 devtools/client/performance/test/helpers/tab-utils.js create mode 100644 devtools/client/performance/test/helpers/urls.js create mode 100644 devtools/client/performance/test/helpers/wait-utils.js create mode 100644 devtools/client/performance/test/js_simpleWorker.js create mode 100644 devtools/client/performance/test/moz.build create mode 100644 devtools/client/performance/test/unit/.eslintrc.js create mode 100644 devtools/client/performance/test/unit/head.js create mode 100644 devtools/client/performance/test/unit/test_frame-utils-01.js create mode 100644 devtools/client/performance/test/unit/test_frame-utils-02.js create mode 100644 devtools/client/performance/test/unit/test_jit-graph-data.js create mode 100644 devtools/client/performance/test/unit/test_jit-model-01.js create mode 100644 devtools/client/performance/test/unit/test_jit-model-02.js create mode 100644 devtools/client/performance/test/unit/test_marker-blueprint.js create mode 100644 devtools/client/performance/test/unit/test_marker-utils.js create mode 100644 devtools/client/performance/test/unit/test_perf-utils-allocations-to-samples.js create mode 100644 devtools/client/performance/test/unit/test_profiler-categories.js create mode 100644 devtools/client/performance/test/unit/test_tree-model-01.js create mode 100644 devtools/client/performance/test/unit/test_tree-model-02.js create mode 100644 devtools/client/performance/test/unit/test_tree-model-03.js create mode 100644 devtools/client/performance/test/unit/test_tree-model-04.js create mode 100644 devtools/client/performance/test/unit/test_tree-model-05.js create mode 100644 devtools/client/performance/test/unit/test_tree-model-06.js create mode 100644 devtools/client/performance/test/unit/test_tree-model-07.js create mode 100644 devtools/client/performance/test/unit/test_tree-model-08.js create mode 100644 devtools/client/performance/test/unit/test_tree-model-09.js create mode 100644 devtools/client/performance/test/unit/test_tree-model-10.js create mode 100644 devtools/client/performance/test/unit/test_tree-model-11.js create mode 100644 devtools/client/performance/test/unit/test_tree-model-12.js create mode 100644 devtools/client/performance/test/unit/test_tree-model-13.js create mode 100644 devtools/client/performance/test/unit/test_tree-model-allocations-01.js create mode 100644 devtools/client/performance/test/unit/test_tree-model-allocations-02.js create mode 100644 devtools/client/performance/test/unit/test_waterfall-utils-collapse-01.js create mode 100644 devtools/client/performance/test/unit/test_waterfall-utils-collapse-02.js create mode 100644 devtools/client/performance/test/unit/test_waterfall-utils-collapse-03.js create mode 100644 devtools/client/performance/test/unit/test_waterfall-utils-collapse-04.js create mode 100644 devtools/client/performance/test/unit/test_waterfall-utils-collapse-05.js create mode 100644 devtools/client/performance/test/unit/xpcshell.ini create mode 100644 devtools/client/performance/views/details-abstract-subview.js create mode 100644 devtools/client/performance/views/details-js-call-tree.js create mode 100644 devtools/client/performance/views/details-js-flamegraph.js create mode 100644 devtools/client/performance/views/details-memory-call-tree.js create mode 100644 devtools/client/performance/views/details-memory-flamegraph.js create mode 100644 devtools/client/performance/views/details-waterfall.js create mode 100644 devtools/client/performance/views/details.js create mode 100644 devtools/client/performance/views/overview.js create mode 100644 devtools/client/performance/views/recordings.js create mode 100644 devtools/client/performance/views/toolbar.js (limited to 'devtools/client/performance') diff --git a/devtools/client/performance/components/jit-optimizations-item.js b/devtools/client/performance/components/jit-optimizations-item.js new file mode 100644 index 000000000..e5c77ef02 --- /dev/null +++ b/devtools/client/performance/components/jit-optimizations-item.js @@ -0,0 +1,175 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { LocalizationHelper } = require("devtools/shared/l10n"); +const STRINGS_URI = "devtools/client/locales/jit-optimizations.properties"; +const L10N = new LocalizationHelper(STRINGS_URI); + +const {PluralForm} = require("devtools/shared/plural-form"); +const { DOM: dom, PropTypes, createClass, createFactory } = require("devtools/client/shared/vendor/react"); +const Frame = createFactory(require("devtools/client/shared/components/frame")); +const PROPNAME_MAX_LENGTH = 4; +// If TREE_ROW_HEIGHT changes, be sure to change `var(--jit-tree-row-height)` +// in `devtools/client/themes/jit-optimizations.css` +const TREE_ROW_HEIGHT = 14; + +const OPTIMIZATION_ITEM_TYPES = ["site", "attempts", "types", "attempt", "type", + "observedtype"]; + +/* eslint-disable no-unused-vars */ +/** + * TODO - Re-enable this eslint rule. The JIT tool is a work in progress, and isn't fully + * integrated as of yet. + */ +const { + JITOptimizations, hasSuccessfulOutcome, isSuccessfulOutcome +} = require("devtools/client/performance/modules/logic/jit"); +const OPTIMIZATION_FAILURE = L10N.getStr("jit.optimizationFailure"); +const JIT_SAMPLES = L10N.getStr("jit.samples"); +const JIT_TYPES = L10N.getStr("jit.types"); +const JIT_ATTEMPTS = L10N.getStr("jit.attempts"); +/* eslint-enable no-unused-vars */ + +const JITOptimizationsItem = createClass({ + displayName: "JITOptimizationsItem", + + propTypes: { + onViewSourceInDebugger: PropTypes.func.isRequired, + frameData: PropTypes.object.isRequired, + type: PropTypes.oneOf(OPTIMIZATION_ITEM_TYPES).isRequired, + }, + + _renderSite({ item: site, onViewSourceInDebugger, frameData }) { + let attempts = site.data.attempts; + let lastStrategy = attempts[attempts.length - 1].strategy; + let propString = ""; + let propertyName = site.data.propertyName; + + // Display property name if it exists + if (propertyName) { + if (propertyName.length > PROPNAME_MAX_LENGTH) { + propString = ` (.${propertyName.substr(0, PROPNAME_MAX_LENGTH)}…)`; + } else { + propString = ` (.${propertyName})`; + } + } + + let sampleString = PluralForm.get(site.samples, JIT_SAMPLES) + .replace("#1", site.samples); + let text = dom.span( + { className: "optimization-site-title" }, + `${lastStrategy}${propString} – (${sampleString})` + ); + let frame = Frame({ + onClick: () => onViewSourceInDebugger(frameData.url, site.data.line), + frame: { + source: frameData.url, + line: +site.data.line, + column: site.data.column, + } + }); + let children = [text, frame]; + + if (!hasSuccessfulOutcome(site)) { + children.unshift(dom.span({ className: "opt-icon warning" })); + } + + return dom.span({ className: "optimization-site" }, ...children); + }, + + _renderAttempts({ item: attempts }) { + return dom.span({ className: "optimization-attempts" }, + `${JIT_ATTEMPTS} (${attempts.length})` + ); + }, + + _renderTypes({ item: types }) { + return dom.span({ className: "optimization-types" }, + `${JIT_TYPES} (${types.length})` + ); + }, + + _renderAttempt({ item: attempt }) { + let success = isSuccessfulOutcome(attempt.outcome); + let { strategy, outcome } = attempt; + return dom.span({ className: "optimization-attempt" }, + dom.span({ className: "optimization-strategy" }, strategy), + " → ", + dom.span({ className: `optimization-outcome ${success ? "success" : "failure"}` }, + outcome) + ); + }, + + _renderType({ item: type }) { + return dom.span({ className: "optimization-ion-type" }, + `${type.site}:${type.mirType}`); + }, + + _renderObservedType({ onViewSourceInDebugger, item: type }) { + let children = [ + dom.span({ className: "optimization-observed-type-keyed" }, + `${type.keyedBy}${type.name ? ` → ${type.name}` : ""}`) + ]; + + // If we have a line and location, make a link to the debugger + if (type.location && type.line) { + children.push( + Frame({ + onClick: () => onViewSourceInDebugger(type.location, type.line), + frame: { + source: type.location, + line: type.line, + column: type.column, + } + }) + ); + // Otherwise if we just have a location, it's probably just a memory location. + } else if (type.location) { + children.push(`@${type.location}`); + } + + return dom.span({ className: "optimization-observed-type" }, ...children); + }, + + render() { + /* eslint-disable no-unused-vars */ + /** + * TODO - Re-enable this eslint rule. The JIT tool is a work in progress, and these + * undefined variables may represent intended functionality. + */ + let { + depth, + arrow, + type, + // TODO - The following are currently unused. + item, + focused, + frameData, + onViewSourceInDebugger, + } = this.props; + /* eslint-enable no-unused-vars */ + + let content; + switch (type) { + case "site": content = this._renderSite(this.props); break; + case "attempts": content = this._renderAttempts(this.props); break; + case "types": content = this._renderTypes(this.props); break; + case "attempt": content = this._renderAttempt(this.props); break; + case "type": content = this._renderType(this.props); break; + case "observedtype": content = this._renderObservedType(this.props); break; + } + + return dom.div( + { + className: `optimization-tree-item optimization-tree-item-${type}`, + style: { marginInlineStart: depth * TREE_ROW_HEIGHT } + }, + arrow, + content + ); + }, +}); + +module.exports = JITOptimizationsItem; diff --git a/devtools/client/performance/components/jit-optimizations.js b/devtools/client/performance/components/jit-optimizations.js new file mode 100644 index 000000000..c189aa1ce --- /dev/null +++ b/devtools/client/performance/components/jit-optimizations.js @@ -0,0 +1,248 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { LocalizationHelper } = require("devtools/shared/l10n"); +const STRINGS_URI = "devtools/client/locales/jit-optimizations.properties"; +const L10N = new LocalizationHelper(STRINGS_URI); + +const { assert } = require("devtools/shared/DevToolsUtils"); +const { DOM: dom, createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react"); +const Tree = createFactory(require("../../shared/components/tree")); +const OptimizationsItem = createFactory(require("./jit-optimizations-item")); +const FrameView = createFactory(require("../../shared/components/frame")); +const JIT_TITLE = L10N.getStr("jit.title"); +// If TREE_ROW_HEIGHT changes, be sure to change `var(--jit-tree-row-height)` +// in `devtools/client/themes/jit-optimizations.css` +const TREE_ROW_HEIGHT = 14; + +/* eslint-disable no-unused-vars */ +/** + * TODO - Re-enable this eslint rule. The JIT tool is a work in progress, and isn't fully + * integrated as of yet, and this may represent intended functionality. + */ +const onClickTooltipString = frame => + L10N.getFormatStr("viewsourceindebugger", + `${frame.source}:${frame.line}:${frame.column}`); +/* eslint-enable no-unused-vars */ + +const optimizationAttemptModel = { + id: PropTypes.number.isRequired, + strategy: PropTypes.string.isRequired, + outcome: PropTypes.string.isRequired, +}; + +const optimizationObservedTypeModel = { + keyedBy: PropTypes.string.isRequired, + name: PropTypes.string, + location: PropTypes.string, + line: PropTypes.string, +}; + +const optimizationIonTypeModel = { + id: PropTypes.number.isRequired, + typeset: PropTypes.arrayOf(optimizationObservedTypeModel), + site: PropTypes.number.isRequired, + mirType: PropTypes.number.isRequired, +}; + +const optimizationSiteModel = { + id: PropTypes.number.isRequired, + propertyName: PropTypes.string, + line: PropTypes.number.isRequired, + column: PropTypes.number.isRequired, + data: PropTypes.shape({ + attempts: PropTypes.arrayOf(optimizationAttemptModel).isRequired, + types: PropTypes.arrayOf(optimizationIonTypeModel).isRequired, + }).isRequired, +}; + +const JITOptimizations = createClass({ + displayName: "JITOptimizations", + + propTypes: { + onViewSourceInDebugger: PropTypes.func.isRequired, + frameData: PropTypes.object.isRequired, + optimizationSites: PropTypes.arrayOf(optimizationSiteModel).isRequired, + autoExpandDepth: PropTypes.number, + }, + + getDefaultProps() { + return { + autoExpandDepth: 0 + }; + }, + + getInitialState() { + return { + expanded: new Set() + }; + }, + + /** + * Frame data generated from `frameNode.getInfo()`, or an empty + * object, as well as a handler for clicking on the frame component. + * + * @param {?Object} .frameData + * @param {Function} .onViewSourceInDebugger + * @return {ReactElement} + */ + _createHeader: function ({ frameData, onViewSourceInDebugger }) { + let { isMetaCategory, url, line } = frameData; + let name = isMetaCategory ? frameData.categoryData.label : + frameData.functionName || ""; + + // Simulate `SavedFrame`s interface + let frame = { source: url, line: +line, functionDisplayName: name }; + + // Neither Meta Category nodes, or the lack of a selected frame node, + // renders out a frame source, like "file.js:123"; so just use + // an empty span. + let frameComponent; + if (isMetaCategory || !name) { + frameComponent = dom.span(); + } else { + frameComponent = FrameView({ + frame, + onClick: () => onViewSourceInDebugger(frame), + }); + } + + return dom.div({ className: "optimization-header" }, + dom.span({ className: "header-title" }, JIT_TITLE), + dom.span({ className: "header-function-name" }, name), + frameComponent + ); + }, + + _createTree(props) { + let { + autoExpandDepth, + frameData, + onViewSourceInDebugger, + optimizationSites: sites + } = this.props; + + let getSite = id => sites.find(site => site.id === id); + let getIonTypeForObserved = type => { + return getSite(type.id).data.types + .find(iontype => (iontype.typeset || []) + .indexOf(type) !== -1); + }; + let isSite = site => getSite(site.id) === site; + let isAttempts = attempts => getSite(attempts.id).data.attempts === attempts; + let isAttempt = attempt => getSite(attempt.id).data.attempts.indexOf(attempt) !== -1; + let isTypes = types => getSite(types.id).data.types === types; + let isType = type => getSite(type.id).data.types.indexOf(type) !== -1; + let isObservedType = type => getIonTypeForObserved(type); + + let getRowType = node => { + if (isSite(node)) { + return "site"; + } + if (isAttempts(node)) { + return "attempts"; + } + if (isTypes(node)) { + return "types"; + } + if (isAttempt(node)) { + return "attempt"; + } + if (isType(node)) { + return "type"; + } + if (isObservedType(node)) { + return "observedtype"; + } + return null; + }; + + // Creates a unique key for each node in the + // optimizations data + let getKey = node => { + let site = getSite(node.id); + if (isSite(node)) { + return node.id; + } else if (isAttempts(node)) { + return `${node.id}-A`; + } else if (isTypes(node)) { + return `${node.id}-T`; + } else if (isType(node)) { + return `${node.id}-T-${site.data.types.indexOf(node)}`; + } else if (isAttempt(node)) { + return `${node.id}-A-${site.data.attempts.indexOf(node)}`; + } else if (isObservedType(node)) { + let iontype = getIonTypeForObserved(node); + return `${getKey(iontype)}-O-${iontype.typeset.indexOf(node)}`; + } + return ""; + }; + + return Tree({ + autoExpandDepth, + getParent: node => { + let site = getSite(node.id); + let parent; + if (isAttempts(node) || isTypes(node)) { + parent = site; + } else if (isType(node)) { + parent = site.data.types; + } else if (isAttempt(node)) { + parent = site.data.attempts; + } else if (isObservedType(node)) { + parent = getIonTypeForObserved(node); + } + assert(parent, "Could not find a parent for optimization data node"); + + return parent; + }, + getChildren: node => { + if (isSite(node)) { + return [node.data.types, node.data.attempts]; + } else if (isAttempts(node) || isTypes(node)) { + return node; + } else if (isType(node)) { + return node.typeset || []; + } + return []; + }, + isExpanded: node => this.state.expanded.has(node), + onExpand: node => this.setState(state => { + let expanded = new Set(state.expanded); + expanded.add(node); + return { expanded }; + }), + onCollapse: node => this.setState(state => { + let expanded = new Set(state.expanded); + expanded.delete(node); + return { expanded }; + }), + onFocus: function () {}, + getKey, + getRoots: () => sites || [], + itemHeight: TREE_ROW_HEIGHT, + renderItem: (item, depth, focused, arrow, expanded) => + new OptimizationsItem({ + onViewSourceInDebugger, + item, + depth, + focused, + arrow, + expanded, + type: getRowType(item), + frameData, + }), + }); + }, + + render() { + let header = this._createHeader(this.props); + let tree = this._createTree(this.props); + + return dom.div({}, header, tree); + } +}); + +module.exports = JITOptimizations; diff --git a/devtools/client/performance/components/moz.build b/devtools/client/performance/components/moz.build new file mode 100644 index 000000000..55de59215 --- /dev/null +++ b/devtools/client/performance/components/moz.build @@ -0,0 +1,19 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + 'jit-optimizations-item.js', + 'jit-optimizations.js', + 'recording-button.js', + 'recording-controls.js', + 'recording-list-item.js', + 'recording-list.js', + 'waterfall-header.js', + 'waterfall-tree-row.js', + 'waterfall-tree.js', + 'waterfall.js', +) + +MOCHITEST_CHROME_MANIFESTS += ['test/chrome.ini'] diff --git a/devtools/client/performance/components/recording-button.js b/devtools/client/performance/components/recording-button.js new file mode 100644 index 000000000..877fd0e2b --- /dev/null +++ b/devtools/client/performance/components/recording-button.js @@ -0,0 +1,37 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const {L10N} = require("devtools/client/performance/modules/global"); +const {DOM, createClass} = require("devtools/client/shared/vendor/react"); +const {button} = DOM; + +module.exports = createClass({ + displayName: "Recording Button", + + render() { + let { + onRecordButtonClick, + isRecording, + isLocked + } = this.props; + + let classList = ["devtools-button", "record-button"]; + + if (isRecording) { + classList.push("checked"); + } + + return button( + { + className: classList.join(" "), + onClick: onRecordButtonClick, + "data-standalone": "true", + "data-text-only": "true", + disabled: isLocked + }, + isRecording ? L10N.getStr("recordings.stop") : L10N.getStr("recordings.start") + ); + } +}); diff --git a/devtools/client/performance/components/recording-controls.js b/devtools/client/performance/components/recording-controls.js new file mode 100644 index 000000000..88f788ef3 --- /dev/null +++ b/devtools/client/performance/components/recording-controls.js @@ -0,0 +1,54 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const {L10N} = require("devtools/client/performance/modules/global"); +const {DOM, createClass} = require("devtools/client/shared/vendor/react"); +const {div, button} = DOM; + +module.exports = createClass({ + displayName: "Recording Controls", + + render() { + let { + onClearButtonClick, + onRecordButtonClick, + onImportButtonClick, + isRecording, + isLocked + } = this.props; + + let recordButtonClassList = ["devtools-button", "record-button"]; + + if (isRecording) { + recordButtonClassList.push("checked"); + } + + return ( + div({ className: "devtools-toolbar" }, + div({ className: "toolbar-group" }, + button({ + id: "clear-button", + className: "devtools-button", + title: L10N.getStr("recordings.clear.tooltip"), + onClick: onClearButtonClick + }), + button({ + id: "main-record-button", + className: recordButtonClassList.join(" "), + disabled: isLocked, + title: L10N.getStr("recordings.start.tooltip"), + onClick: onRecordButtonClick + }), + button({ + id: "import-button", + className: "devtools-button", + title: L10N.getStr("recordings.import.tooltip"), + onClick: onImportButtonClick + }) + ) + ) + ); + } +}); diff --git a/devtools/client/performance/components/recording-list-item.js b/devtools/client/performance/components/recording-list-item.js new file mode 100644 index 000000000..37efec90d --- /dev/null +++ b/devtools/client/performance/components/recording-list-item.js @@ -0,0 +1,49 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const {DOM, createClass} = require("devtools/client/shared/vendor/react"); +const {div, li, span, button} = DOM; +const {L10N} = require("devtools/client/performance/modules/global"); + +module.exports = createClass({ + displayName: "Recording List Item", + + render() { + const { + label, + duration, + onSelect, + onSave, + isLoading, + isSelected, + isRecording + } = this.props; + + const className = `recording-list-item ${isSelected ? "selected" : ""}`; + + let durationText; + if (isLoading) { + durationText = L10N.getStr("recordingsList.loadingLabel"); + } else if (isRecording) { + durationText = L10N.getStr("recordingsList.recordingLabel"); + } else { + durationText = L10N.getFormatStr("recordingsList.durationLabel", duration); + } + + return ( + li({ className, onClick: onSelect }, + div({ className: "recording-list-item-label" }, + label + ), + div({ className: "recording-list-item-footer" }, + span({ className: "recording-list-item-duration" }, durationText), + button({ className: "recording-list-item-save", onClick: onSave }, + L10N.getStr("recordingsList.saveLabel") + ) + ) + ) + ); + } +}); diff --git a/devtools/client/performance/components/recording-list.js b/devtools/client/performance/components/recording-list.js new file mode 100644 index 000000000..1df7f2b71 --- /dev/null +++ b/devtools/client/performance/components/recording-list.js @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const {DOM, createClass} = require("devtools/client/shared/vendor/react"); +const {L10N} = require("devtools/client/performance/modules/global"); +const {ul, div} = DOM; + +module.exports = createClass({ + displayName: "Recording List", + + render() { + const { + items, + itemComponent: Item, + } = this.props; + + return items.length > 0 + ? ul({ className: "recording-list" }, ...items.map(Item)) + : div({ className: "recording-list-empty" }, L10N.getStr("noRecordingsText")); + } +}); diff --git a/devtools/client/performance/components/test/chrome.ini b/devtools/client/performance/components/test/chrome.ini new file mode 100644 index 000000000..5ba24a9af --- /dev/null +++ b/devtools/client/performance/components/test/chrome.ini @@ -0,0 +1,5 @@ +[DEFAULT] +support-files = + head.js + +[test_jit_optimizations_01.html] diff --git a/devtools/client/performance/components/test/head.js b/devtools/client/performance/components/test/head.js new file mode 100644 index 000000000..be8184160 --- /dev/null +++ b/devtools/client/performance/components/test/head.js @@ -0,0 +1,187 @@ +/* Any copyright is dedicated to the Public Domain. + yield new Promise(function(){}); + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +/* global window, document, SimpleTest, requestAnimationFrame, is, ok */ +/* exported Cc, Ci, Cu, Cr, Assert, Task, TargetFactory, Toolbox, browserRequire, + forceRender, setProps, dumpn, checkOptimizationHeader, checkOptimizationTree */ +let { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +let { require } = Cu.import("resource://gre/modules/devtools/shared/Loader.jsm", {}); +let { Assert } = require("resource://testing-common/Assert.jsm"); +let { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {}); +let defer = require("devtools/shared/defer"); +let flags = require("devtools/shared/flags"); +let { Task } = require("devtools/shared/task"); +let { TargetFactory } = require("devtools/client/framework/target"); +let { Toolbox } = require("devtools/client/framework/toolbox"); + +flags.testing = true; +let { require: browserRequire } = BrowserLoader({ + baseURI: "resource://devtools/client/performance/", + window +}); + +let $ = (selector, scope = document) => scope.querySelector(selector); +let $$ = (selector, scope = document) => scope.querySelectorAll(selector); + +function forceRender(comp) { + return setState(comp, {}) + .then(() => setState(comp, {})); +} + +// All tests are asynchronous. +SimpleTest.waitForExplicitFinish(); + +function onNextAnimationFrame(fn) { + return () => + requestAnimationFrame(() => + requestAnimationFrame(fn)); +} + +function setState(component, newState) { + let deferred = defer(); + component.setState(newState, onNextAnimationFrame(deferred.resolve)); + return deferred.promise; +} + +function setProps(component, newState) { + let deferred = defer(); + component.setProps(newState, onNextAnimationFrame(deferred.resolve)); + return deferred.promise; +} + +function dumpn(msg) { + dump(`PERFORMANCE-COMPONENT-TEST: ${msg}\n`); +} + +/** + * Default opts data for testing. First site has a simple IonType, + * and an IonType with an ObservedType, and a successful outcome. + * Second site does not have a successful outcome. + */ +let OPTS_DATA_GENERAL = [{ + id: 1, + propertyName: "my property name", + line: 100, + column: 200, + samples: 90, + data: { + attempts: [ + { id: 1, strategy: "GetElem_TypedObject", outcome: "AccessNotTypedObject" }, + { id: 1, strategy: "GetElem_Dense", outcome: "AccessNotDense" }, + { id: 1, strategy: "GetElem_TypedStatic", outcome: "Disabled" }, + { id: 1, strategy: "GetElem_TypedArray", outcome: "GenericSuccess" }, + ], + types: [{ + id: 1, + site: "Receiver", + mirType: "Object", + typeset: [{ + id: 1, + keyedBy: "constructor", + name: "MyView", + location: "http://internet.com/file.js", + line: "123", + }] + }, { + id: 1, + typeset: void 0, + site: "Index", + mirType: "Int32", + }] + } +}, { + id: 2, + propertyName: void 0, + line: 50, + column: 51, + samples: 100, + data: { + attempts: [ + { id: 2, strategy: "Call_Inline", outcome: "CantInlineBigData" } + ], + types: [{ + id: 2, + site: "Call_Target", + mirType: "Object", + typeset: [ + { id: 2, keyedBy: "primitive" }, + { id: 2, keyedBy: "constructor", name: "B", location: "http://mypage.com/file.js", line: "2" }, + { id: 2, keyedBy: "constructor", name: "C", location: "http://mypage.com/file.js", line: "3" }, + { id: 2, keyedBy: "constructor", name: "D", location: "http://mypage.com/file.js", line: "4" }, + ], + }] + } +}]; + +OPTS_DATA_GENERAL.forEach(site => { + site.data.types.forEach(type => { + if (type.typeset) { + type.typeset.id = site.id; + } + }); + site.data.attempts.id = site.id; + site.data.types.id = site.id; +}); + +function checkOptimizationHeader(name, file, line) { + is($(".optimization-header .header-function-name").textContent, name, + "correct optimization header function name"); + is($(".optimization-header .frame-link-filename").textContent, file, + "correct optimization header file name"); + is($(".optimization-header .frame-link-line").textContent, `:${line}`, + "correct optimization header line"); +} + +function checkOptimizationTree(rowData) { + let rows = $$(".tree .tree-node"); + + for (let i = 0; i < rowData.length; i++) { + let row = rows[i]; + let expected = rowData[i]; + + switch (expected.type) { + case "site": + is($(".optimization-site-title", row).textContent, + `${expected.strategy} – (${expected.samples} samples)`, + `row ${i}th: correct optimization site row`); + + is(!!$(".opt-icon.warning", row), !!expected.failureIcon, + `row ${i}th: expected visibility of failure icon for unsuccessful outcomes`); + break; + case "types": + is($(".optimization-types", row).textContent, + `Types (${expected.count})`, + `row ${i}th: correct types row`); + break; + case "attempts": + is($(".optimization-attempts", row).textContent, + `Attempts (${expected.count})`, + `row ${i}th: correct attempts row`); + break; + case "type": + is($(".optimization-ion-type", row).textContent, + `${expected.site}:${expected.mirType}`, + `row ${i}th: correct ion type row`); + break; + case "observedtype": + is($(".optimization-observed-type-keyed", row).textContent, + expected.name ? + `${expected.keyedBy} → ${expected.name}` : + expected.keyedBy, + `row ${i}th: correct observed type row`); + break; + case "attempt": + is($(".optimization-strategy", row).textContent, expected.strategy, + `row ${i}th: correct attempt row, attempt item`); + is($(".optimization-outcome", row).textContent, expected.outcome, + `row ${i}th: correct attempt row, outcome item`); + ok($(".optimization-outcome", row) + .classList.contains(expected.success ? "success" : "failure"), + `row ${i}th: correct attempt row, failure/success status`); + break; + } + } +} diff --git a/devtools/client/performance/components/test/test_jit_optimizations_01.html b/devtools/client/performance/components/test/test_jit_optimizations_01.html new file mode 100644 index 000000000..edc9c34cd --- /dev/null +++ b/devtools/client/performance/components/test/test_jit_optimizations_01.html @@ -0,0 +1,70 @@ + + + + + + JITOptimizations component test + + + + +
+
+
+
+ + diff --git a/devtools/client/performance/components/waterfall-header.js b/devtools/client/performance/components/waterfall-header.js new file mode 100644 index 000000000..f3030091b --- /dev/null +++ b/devtools/client/performance/components/waterfall-header.js @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * The "waterfall ticks" view, a header for the markers displayed in the waterfall. + */ + +const { DOM: dom, PropTypes } = require("devtools/client/shared/vendor/react"); +const { L10N } = require("../modules/global"); +const { TickUtils } = require("../modules/waterfall-ticks"); + +// ms +const WATERFALL_HEADER_TICKS_MULTIPLE = 5; +// px +const WATERFALL_HEADER_TICKS_SPACING_MIN = 50; +// px +const WATERFALL_HEADER_TEXT_PADDING = 3; + +function WaterfallHeader(props) { + let { startTime, dataScale, sidebarWidth, waterfallWidth } = props; + + let tickInterval = TickUtils.findOptimalTickInterval({ + ticksMultiple: WATERFALL_HEADER_TICKS_MULTIPLE, + ticksSpacingMin: WATERFALL_HEADER_TICKS_SPACING_MIN, + dataScale: dataScale + }); + + let ticks = []; + for (let x = 0; x < waterfallWidth; x += tickInterval) { + let left = x + WATERFALL_HEADER_TEXT_PADDING; + let time = Math.round(x / dataScale + startTime); + let label = L10N.getFormatStr("timeline.tick", time); + + let node = dom.div({ + className: "plain waterfall-header-tick", + style: { transform: `translateX(${left}px)` } + }, label); + ticks.push(node); + } + + return dom.div( + { className: "waterfall-header" }, + dom.div( + { + className: "waterfall-sidebar theme-sidebar waterfall-header-name", + style: { width: sidebarWidth + "px" } + }, + L10N.getStr("timeline.records") + ), + dom.div( + { className: "waterfall-header-ticks waterfall-background-ticks" }, + ticks + ) + ); +} + +WaterfallHeader.displayName = "WaterfallHeader"; + +WaterfallHeader.propTypes = { + startTime: PropTypes.number.isRequired, + dataScale: PropTypes.number.isRequired, + sidebarWidth: PropTypes.number.isRequired, + waterfallWidth: PropTypes.number.isRequired, +}; + +module.exports = WaterfallHeader; diff --git a/devtools/client/performance/components/waterfall-tree-row.js b/devtools/client/performance/components/waterfall-tree-row.js new file mode 100644 index 000000000..b87750db1 --- /dev/null +++ b/devtools/client/performance/components/waterfall-tree-row.js @@ -0,0 +1,107 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * A single row (node) in the waterfall tree + */ + +const { DOM: dom, PropTypes } = require("devtools/client/shared/vendor/react"); +const { MarkerBlueprintUtils } = require("../modules/marker-blueprint-utils"); + +// px +const LEVEL_INDENT = 10; +// px +const ARROW_NODE_OFFSET = -14; +// px +const WATERFALL_MARKER_TIMEBAR_WIDTH_MIN = 5; + +function buildMarkerSidebar(blueprint, props) { + const { marker, level, sidebarWidth } = props; + + let bullet = dom.div({ + className: `waterfall-marker-bullet marker-color-${blueprint.colorName}`, + style: { transform: `translateX(${level * LEVEL_INDENT}px)` }, + "data-type": marker.name + }); + + let label = MarkerBlueprintUtils.getMarkerLabel(marker); + + let name = dom.div({ + className: "plain waterfall-marker-name", + style: { transform: `translateX(${level * LEVEL_INDENT}px)` }, + title: label + }, label); + + return dom.div({ + className: "waterfall-sidebar theme-sidebar", + style: { width: sidebarWidth + "px" } + }, bullet, name); +} + +function buildMarkerTimebar(blueprint, props) { + const { marker, startTime, dataScale, arrow } = props; + const offset = (marker.start - startTime) * dataScale + ARROW_NODE_OFFSET; + const width = Math.max((marker.end - marker.start) * dataScale, + WATERFALL_MARKER_TIMEBAR_WIDTH_MIN); + + let bar = dom.div( + { + className: "waterfall-marker-wrap", + style: { transform: `translateX(${offset}px)` } + }, + arrow, + dom.div({ + className: `waterfall-marker-bar marker-color-${blueprint.colorName}`, + style: { width: `${width}px` }, + "data-type": marker.name + }) + ); + + return dom.div( + { className: "waterfall-marker waterfall-background-ticks" }, + bar + ); +} + +function WaterfallTreeRow(props) { + const { marker, focused } = props; + const blueprint = MarkerBlueprintUtils.getBlueprintFor(marker); + + let attrs = { + className: "waterfall-tree-item" + (focused ? " focused" : ""), + "data-otmt": marker.isOffMainThread + }; + + // Don't render an expando-arrow for leaf nodes. + let submarkers = marker.submarkers; + let hasDescendants = submarkers && submarkers.length > 0; + if (hasDescendants) { + attrs["data-expandable"] = ""; + } else { + attrs["data-invisible"] = ""; + } + + return dom.div( + attrs, + buildMarkerSidebar(blueprint, props), + buildMarkerTimebar(blueprint, props) + ); +} + +WaterfallTreeRow.displayName = "WaterfallTreeRow"; + +WaterfallTreeRow.propTypes = { + marker: PropTypes.object.isRequired, + level: PropTypes.number.isRequired, + arrow: PropTypes.element.isRequired, + expanded: PropTypes.bool.isRequired, + focused: PropTypes.bool.isRequired, + startTime: PropTypes.number.isRequired, + dataScale: PropTypes.number.isRequired, + sidebarWidth: PropTypes.number.isRequired, +}; + +module.exports = WaterfallTreeRow; diff --git a/devtools/client/performance/components/waterfall-tree.js b/devtools/client/performance/components/waterfall-tree.js new file mode 100644 index 000000000..031c4facf --- /dev/null +++ b/devtools/client/performance/components/waterfall-tree.js @@ -0,0 +1,167 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react"); +const Tree = createFactory(require("devtools/client/shared/components/tree")); +const WaterfallTreeRow = createFactory(require("./waterfall-tree-row")); + +// px - keep in sync with var(--waterfall-tree-row-height) in performance.css +const WATERFALL_TREE_ROW_HEIGHT = 15; + +/** + * Checks if a given marker is in the specified time range. + * + * @param object e + * The marker containing the { start, end } timestamps. + * @param number start + * The earliest allowed time. + * @param number end + * The latest allowed time. + * @return boolean + * True if the marker fits inside the specified time range. + */ +function isMarkerInRange(e, start, end) { + let mStart = e.start | 0; + let mEnd = e.end | 0; + + return ( + // bounds inside + (mStart >= start && mEnd <= end) || + // bounds outside + (mStart < start && mEnd > end) || + // overlap start + (mStart < start && mEnd >= start && mEnd <= end) || + // overlap end + (mEnd > end && mStart >= start && mStart <= end) + ); +} + +const WaterfallTree = createClass({ + displayName: "WaterfallTree", + + propTypes: { + marker: PropTypes.object.isRequired, + startTime: PropTypes.number.isRequired, + endTime: PropTypes.number.isRequired, + dataScale: PropTypes.number.isRequired, + sidebarWidth: PropTypes.number.isRequired, + waterfallWidth: PropTypes.number.isRequired, + onFocus: PropTypes.func, + }, + + getInitialState() { + return { + focused: null, + expanded: new Set() + }; + }, + + _getRoots(node) { + let roots = this.props.marker.submarkers || []; + return roots.filter(this._filter); + }, + + /** + * Find the parent node of 'node' with a depth-first search of the marker tree + */ + _getParent(node) { + function findParent(marker) { + if (marker.submarkers) { + for (let submarker of marker.submarkers) { + if (submarker === node) { + return marker; + } + + let parent = findParent(submarker); + if (parent) { + return parent; + } + } + } + + return null; + } + + let rootMarker = this.props.marker; + let parent = findParent(rootMarker); + + // We are interested only in parent markers that are rendered, + // which rootMarker is not. Return null if the parent is rootMarker. + return parent !== rootMarker ? parent : null; + }, + + _getChildren(node) { + let submarkers = node.submarkers || []; + return submarkers.filter(this._filter); + }, + + _getKey(node) { + return `marker-${node.index}`; + }, + + _isExpanded(node) { + return this.state.expanded.has(node); + }, + + _onExpand(node) { + this.setState(state => { + let expanded = new Set(state.expanded); + expanded.add(node); + return { expanded }; + }); + }, + + _onCollapse(node) { + this.setState(state => { + let expanded = new Set(state.expanded); + expanded.delete(node); + return { expanded }; + }); + }, + + _onFocus(node) { + this.setState({ focused: node }); + if (this.props.onFocus) { + this.props.onFocus(node); + } + }, + + _filter(node) { + let { startTime, endTime } = this.props; + return isMarkerInRange(node, startTime, endTime); + }, + + _renderItem(marker, level, focused, arrow, expanded) { + let { startTime, dataScale, sidebarWidth } = this.props; + return WaterfallTreeRow({ + marker, + level, + arrow, + expanded, + focused, + startTime, + dataScale, + sidebarWidth + }); + }, + + render() { + return Tree({ + getRoots: this._getRoots, + getParent: this._getParent, + getChildren: this._getChildren, + getKey: this._getKey, + isExpanded: this._isExpanded, + onExpand: this._onExpand, + onCollapse: this._onCollapse, + onFocus: this._onFocus, + renderItem: this._renderItem, + focused: this.state.focused, + itemHeight: WATERFALL_TREE_ROW_HEIGHT + }); + } +}); + +module.exports = WaterfallTree; diff --git a/devtools/client/performance/components/waterfall.js b/devtools/client/performance/components/waterfall.js new file mode 100644 index 000000000..067033874 --- /dev/null +++ b/devtools/client/performance/components/waterfall.js @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +/** + * This file contains the "waterfall" view, essentially a detailed list + * of all the markers in the timeline data. + */ + +const { DOM: dom, createFactory, PropTypes } = require("devtools/client/shared/vendor/react"); +const WaterfallHeader = createFactory(require("./waterfall-header")); +const WaterfallTree = createFactory(require("./waterfall-tree")); + +function Waterfall(props) { + return dom.div( + { className: "waterfall-markers" }, + WaterfallHeader(props), + WaterfallTree(props) + ); +} + +Waterfall.displayName = "Waterfall"; + +Waterfall.propTypes = { + marker: PropTypes.object.isRequired, + startTime: PropTypes.number.isRequired, + endTime: PropTypes.number.isRequired, + dataScale: PropTypes.number.isRequired, + sidebarWidth: PropTypes.number.isRequired, + waterfallWidth: PropTypes.number.isRequired, + onFocus: PropTypes.func, + onBlur: PropTypes.func, +}; + +module.exports = Waterfall; diff --git a/devtools/client/performance/docs/markers.md b/devtools/client/performance/docs/markers.md new file mode 100644 index 000000000..e743f7fcd --- /dev/null +++ b/devtools/client/performance/docs/markers.md @@ -0,0 +1,189 @@ +# Timeline Markers + +## Common + +* DOMHighResTimeStamp start +* DOMHighResTimeStamp end +* DOMString name +* object? stack +* object? endStack +* unsigned short processType; +* boolean isOffMainThread; + +The `processType` a GeckoProcessType enum listed in xpcom/build/nsXULAppAPI.h, +specifying if this marker originates in a content, chrome, plugin etc. process. +Additionally, markers may be created from any thread on those processes, and +`isOffMainThread` highights whether or not they're from the main thread. The most +common type of marker is probably going to be from a GeckoProcessType_Content's +main thread when debugging content. + +## DOMEvent + +Triggered when a DOM event occurs, like a click or a keypress. + +* unsigned short eventPhase - a number indicating what phase this event is + in (target, bubbling, capturing, maps to Ci.nsIDOMEvent constants) +* DOMString type - the type of event, like "keypress" or "click" + +## Reflow + +Reflow markers (labeled as "Layout") indicate when a change has occurred to +a DOM element's positioning that requires the frame tree (rendering +representation of the DOM) to figure out the new position of a handful of +elements. Fired via `PresShell::DoReflow` + +## Paint + +* sequence<{ long height, long width, long x, long y }> rectangles - An array + of rectangle objects indicating where painting has occurred. + +## Styles + +Style markers (labeled as "Recalculating Styles") are triggered when Gecko +needs to figure out the computational style of an element. Fired via +`RestyleTracker::DoProcessRestyles` when there are elements to restyle. + +* DOMString restyleHint - A string indicating what kind of restyling will need + to be processed; for example "eRestyle_StyleAttribute" is relatively cheap, + whereas "eRestyle_Subtree" is more expensive. The hint can be a string of + any amount of the following, separated via " | ". All future restyleHints + are from `RestyleManager::RestyleHintToString`. + + * "eRestyle_Self" + * "eRestyle_Subtree" + * "eRestyle_LaterSiblings" + * "eRestyle_CSSTransitions" + * "eRestyle_CSSAnimations" + * "eRestyle_SVGAttrAnimations" + * "eRestyle_StyleAttribute" + * "eRestyle_StyleAttribute_Animations" + * "eRestyle_Force" + * "eRestyle_ForceDescendants" + + +## Javascript + +`Javascript` markers are emitted indicating when JS execution begins and ends, +with a reason that triggered it (causeName), like a requestAnimationFrame or +a setTimeout. + +* string causeName - The reason that JS was entered. There are many possible + reasons, and the interesting ones to show web developers (triggered by content) are: + + * "\ element" + * "EventListener.handleEvent" + * "setInterval handler" + * "setTimeout handler" + * "FrameRequestCallback" + * "EventHandlerNonNull" + * "promise callback" + * "promise initializer" + * "Worker runnable" + + There are also many more potential JS causes, some which are just internally + used and won't emit a marker, but the below ones are only of interest to + Gecko hackers, most likely + + * "promise thenable" + * "worker runnable" + * "nsHTTPIndex set HTTPIndex property" + * "XPCWrappedJS method call" + * "nsHTTPIndex OnFTPControlLog" + * "XPCWrappedJS QueryInterface" + * "xpcshell argument processing” + * "XPConnect sandbox evaluation " + * "component loader report global" + * "component loader load module" + * "Cross-Process Object Wrapper call/construct" + * "Cross-Process Object Wrapper ’set'" + * "Cross-Process Object Wrapper 'get'" + * "nsXULTemplateBuilder creation" + * "TestShellCommand" + * "precompiled XUL \ element" + * "XBL \ initialization " + * "NPAPI NPN_evaluate" + * "NPAPI get" + * "NPAPI set" + * "NPAPI doInvoke" + * "javascript: URI" + * "geolocation.always_precise indexing" + * "geolocation.app_settings enumeration" + * "WebIDL dictionary creation" + * "XBL \/\ invocation" + * "message manager script load" + * "message handler script load" + * "nsGlobalWindow report new global" + +## GarbageCollection + +Emitted after a full GC cycle has completed (which is after any number of +incremental slices). + +* DOMString causeName - The reason for a GC event to occur. A full list of + GC reasons can be found [on MDN](https://developer.mozilla.org/en-US/docs/Tools/Debugger-API/Debugger.Memory#Debugger.Memory_Handler_Functions). +* DOMString nonincremenetalReason - If the GC could not do an incremental + GC (smaller, quick GC events), and we have to walk the entire heap and + GC everything marked, then the reason listed here is why. + +## nsCycleCollector::Collect + +An `nsCycleCollector::Collect` marker is emitted for each incremental cycle +collection slice and each non-incremental cycle collection. + +# nsCycleCollector::ForgetSkippable + +`nsCycleCollector::ForgetSkippable` is presented as "Cycle Collection", but in +reality it is preparation/pre-optimization for cycle collection, and not the +actual tracing of edges and collecting of cycles. + +## ConsoleTime + +A marker generated via `console.time()` and `console.timeEnd()`. + +* DOMString causeName - the label passed into `console.time(label)` and + `console.timeEnd(label)` if passed in. + +## TimeStamp + +A marker generated via `console.timeStamp(label)`. + +* DOMString causeName - the label passed into `console.timeStamp(label)` + if passed in. + +## document::DOMContentLoaded + +A marker generated when the DOMContentLoaded event is fired. + +## document::Load + +A marker generated when the document's "load" event is fired. + +## Parse HTML + +## Parse XML + +## Worker + +Emitted whenever there's an operation dealing with Workers (any kind of worker, +Web Workers, Service Workers etc.). Currently there are 4 types of operations +being tracked: serializing/deserializing data on the main thread, and also +serializing/deserializing data off the main thread. + +* ProfileTimelineWorkerOperationType operationType - the type of operation + being done by the Worker or the main thread when dealing with workers. + Can be one of the following enums defined in ProfileTimelineMarker.webidl + * "serializeDataOffMainThread" + * "serializeDataOnMainThread" + * "deserializeDataOffMainThread" + * "deserializeDataOnMainThread" + +## Composite + +Composite markers trace the actual time an inner composite operation +took on the compositor thread. Currently, these markers are only especially +interesting for Gecko platform developers, and thus disabled by default. + +## CompositeForwardTransaction + +Markers generated when the IPC request was made to the compositor from +the child process's main thread. diff --git a/devtools/client/performance/events.js b/devtools/client/performance/events.js new file mode 100644 index 000000000..27514ed18 --- /dev/null +++ b/devtools/client/performance/events.js @@ -0,0 +1,108 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const ControllerEvents = { + // Fired when a performance pref changes (either because the user changed it + // via the tool's UI, by changing something about:config or programatically). + PREF_CHANGED: "Performance:PrefChanged", + + // Fired when the devtools theme changes. + THEME_CHANGED: "Performance:ThemeChanged", + + // When a new recording model is received by the controller. + RECORDING_ADDED: "Performance:RecordingAdded", + + // When a recording model gets removed from the controller. + RECORDING_DELETED: "Performance:RecordingDeleted", + + // When a recording model becomes "started", "stopping" or "stopped". + RECORDING_STATE_CHANGE: "Performance:RecordingStateChange", + + // When a recording is offering information on the profiler's circular buffer. + RECORDING_PROFILER_STATUS_UPDATE: "Performance:RecordingProfilerStatusUpdate", + + // When a recording model becomes marked as selected. + RECORDING_SELECTED: "Performance:RecordingSelected", + + // When starting a recording is attempted and fails because the backend + // does not permit it at this time. + BACKEND_FAILED_AFTER_RECORDING_START: "Performance:BackendFailedRecordingStart", + + // When a recording is started and the backend has started working. + BACKEND_READY_AFTER_RECORDING_START: "Performance:BackendReadyRecordingStart", + + // When a recording is stopped and the backend has finished cleaning up. + BACKEND_READY_AFTER_RECORDING_STOP: "Performance:BackendReadyRecordingStop", + + // When a recording is exported. + RECORDING_EXPORTED: "Performance:RecordingExported", + + // When a recording is imported. + RECORDING_IMPORTED: "Performance:RecordingImported", + + // When a source is shown in the JavaScript Debugger at a specific location. + SOURCE_SHOWN_IN_JS_DEBUGGER: "Performance:UI:SourceShownInJsDebugger", + SOURCE_NOT_FOUND_IN_JS_DEBUGGER: "Performance:UI:SourceNotFoundInJsDebugger", + + // Fired by the PerformanceController when `populateWithRecordings` is finished. + RECORDINGS_SEEDED: "Performance:RecordingsSeeded", +}; + +const ViewEvents = { + // Emitted by the `ToolbarView` when a preference changes. + UI_PREF_CHANGED: "Performance:UI:PrefChanged", + + // When the state (display mode) changes, for example when switching between + // "empty", "recording" or "recorded". This causes certain parts of the UI + // to be hidden or visible. + UI_STATE_CHANGED: "Performance:UI:StateChanged", + + // Emitted by the `PerformanceView` on clear button click. + UI_CLEAR_RECORDINGS: "Performance:UI:ClearRecordings", + + // Emitted by the `PerformanceView` on record button click. + UI_START_RECORDING: "Performance:UI:StartRecording", + UI_STOP_RECORDING: "Performance:UI:StopRecording", + + // Emitted by the `PerformanceView` on import/export button click. + UI_IMPORT_RECORDING: "Performance:UI:ImportRecording", + UI_EXPORT_RECORDING: "Performance:UI:ExportRecording", + + // Emitted by the `PerformanceView` when the profiler's circular buffer + // status has been rendered. + UI_RECORDING_PROFILER_STATUS_RENDERED: "Performance:UI:RecordingProfilerStatusRendered", + + // When a recording is selected in the UI. + UI_RECORDING_SELECTED: "Performance:UI:RecordingSelected", + + // Emitted by the `DetailsView` when a subview is selected + UI_DETAILS_VIEW_SELECTED: "Performance:UI:DetailsViewSelected", + + // Emitted by the `OverviewView` after something has been rendered. + UI_OVERVIEW_RENDERED: "Performance:UI:OverviewRendered", + UI_MARKERS_GRAPH_RENDERED: "Performance:UI:OverviewMarkersRendered", + UI_MEMORY_GRAPH_RENDERED: "Performance:UI:OverviewMemoryRendered", + UI_FRAMERATE_GRAPH_RENDERED: "Performance:UI:OverviewFramerateRendered", + + // Emitted by the `OverviewView` when a range has been selected in the graphs. + UI_OVERVIEW_RANGE_SELECTED: "Performance:UI:OverviewRangeSelected", + + // Emitted by the `WaterfallView` when it has been rendered. + UI_WATERFALL_RENDERED: "Performance:UI:WaterfallRendered", + + // Emitted by the `JsCallTreeView` when it has been rendered. + UI_JS_CALL_TREE_RENDERED: "Performance:UI:JsCallTreeRendered", + + // Emitted by the `JsFlameGraphView` when it has been rendered. + UI_JS_FLAMEGRAPH_RENDERED: "Performance:UI:JsFlameGraphRendered", + + // Emitted by the `MemoryCallTreeView` when it has been rendered. + UI_MEMORY_CALL_TREE_RENDERED: "Performance:UI:MemoryCallTreeRendered", + + // Emitted by the `MemoryFlameGraphView` when it has been rendered. + UI_MEMORY_FLAMEGRAPH_RENDERED: "Performance:UI:MemoryFlameGraphRendered", +}; + +module.exports = Object.assign({}, ControllerEvents, ViewEvents); diff --git a/devtools/client/performance/legacy/actors.js b/devtools/client/performance/legacy/actors.js new file mode 100644 index 000000000..22b4f85b1 --- /dev/null +++ b/devtools/client/performance/legacy/actors.js @@ -0,0 +1,263 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { Task } = require("devtools/shared/task"); + +const EventEmitter = require("devtools/shared/event-emitter"); +const { Poller } = require("devtools/client/shared/poller"); + +const CompatUtils = require("devtools/client/performance/legacy/compatibility"); +const RecordingUtils = require("devtools/shared/performance/recording-utils"); +const { TimelineFront } = require("devtools/shared/fronts/timeline"); +const { ProfilerFront } = require("devtools/shared/fronts/profiler"); + +// How often do we check the status of the profiler's circular buffer in milliseconds. +const PROFILER_CHECK_TIMER = 5000; + +const TIMELINE_ACTOR_METHODS = [ + "start", "stop", +]; + +const PROFILER_ACTOR_METHODS = [ + "startProfiler", "getStartOptions", "stopProfiler", + "registerEventNotifications", "unregisterEventNotifications" +]; + +/** + * Constructor for a facade around an underlying ProfilerFront. + */ +function LegacyProfilerFront(target) { + this._target = target; + this._onProfilerEvent = this._onProfilerEvent.bind(this); + this._checkProfilerStatus = this._checkProfilerStatus.bind(this); + this._PROFILER_CHECK_TIMER = this._target.TEST_MOCK_PROFILER_CHECK_TIMER || + PROFILER_CHECK_TIMER; + + EventEmitter.decorate(this); +} + +LegacyProfilerFront.prototype = { + EVENTS: ["console-api-profiler", "profiler-stopped"], + + // Connects to the targets underlying real ProfilerFront. + connect: Task.async(function* () { + let target = this._target; + this._front = new ProfilerFront(target.client, target.form); + + // Fetch and store information about the SPS profiler and + // server profiler. + this.traits = {}; + this.traits.filterable = target.getTrait("profilerDataFilterable"); + + // Directly register to event notifications when connected + // to hook into `console.profile|profileEnd` calls. + yield this.registerEventNotifications({ events: this.EVENTS }); + target.client.addListener("eventNotification", this._onProfilerEvent); + }), + + /** + * Unregisters events for the underlying profiler actor. + */ + destroy: Task.async(function* () { + if (this._poller) { + yield this._poller.destroy(); + } + yield this.unregisterEventNotifications({ events: this.EVENTS }); + this._target.client.removeListener("eventNotification", this._onProfilerEvent); + yield this._front.destroy(); + }), + + /** + * Starts the profiler actor, if necessary. + * + * @option {number?} bufferSize + * @option {number?} sampleFrequency + */ + start: Task.async(function* (options = {}) { + // Check for poller status even if the profiler is already active -- + // profiler can be activated via `console.profile` or another source, like + // the Gecko Profiler. + if (!this._poller) { + this._poller = new Poller(this._checkProfilerStatus, this._PROFILER_CHECK_TIMER, + false); + } + if (!this._poller.isPolling()) { + this._poller.on(); + } + + // Start the profiler only if it wasn't already active. The built-in + // nsIPerformance module will be kept recording, because it's the same instance + // for all targets and interacts with the whole platform, so we don't want + // to affect other clients by stopping (or restarting) it. + let { + isActive, + currentTime, + position, + generation, + totalSize + } = yield this.getStatus(); + + if (isActive) { + return { startTime: currentTime, position, generation, totalSize }; + } + + // Translate options from the recording model into profiler-specific + // options for the nsIProfiler + let profilerOptions = { + entries: options.bufferSize, + interval: options.sampleFrequency + ? (1000 / (options.sampleFrequency * 1000)) + : void 0 + }; + + let startInfo = yield this.startProfiler(profilerOptions); + let startTime = 0; + if ("currentTime" in startInfo) { + startTime = startInfo.currentTime; + } + + return { startTime, position, generation, totalSize }; + }), + + /** + * Indicates the end of a recording -- does not actually stop the profiler + * (stopProfiler does that), but notes that we no longer need to poll + * for buffer status. + */ + stop: Task.async(function* () { + yield this._poller.off(); + }), + + /** + * Wrapper around `profiler.isActive()` to take profiler status data and emit. + */ + getStatus: Task.async(function* () { + let data = yield (CompatUtils.callFrontMethod("isActive").call(this)); + // If no data, the last poll for `isActive()` was wrapping up, and the target.client + // is now null, so we no longer have data, so just abort here. + if (!data) { + return undefined; + } + + // If TEST_PROFILER_FILTER_STATUS defined (via array of fields), filter + // out any field from isActive, used only in tests. Used to filter out + // buffer status fields to simulate older geckos. + if (this._target.TEST_PROFILER_FILTER_STATUS) { + data = Object.keys(data).reduce((acc, prop) => { + if (this._target.TEST_PROFILER_FILTER_STATUS.indexOf(prop) === -1) { + acc[prop] = data[prop]; + } + return acc; + }, {}); + } + + this.emit("profiler-status", data); + return data; + }), + + /** + * Returns profile data from now since `startTime`. + */ + getProfile: Task.async(function* (options) { + let profilerData = yield (CompatUtils.callFrontMethod("getProfile") + .call(this, options)); + // If the backend is not deduped, dedupe it ourselves, as rest of the code + // expects a deduped profile. + if (profilerData.profile.meta.version === 2) { + RecordingUtils.deflateProfile(profilerData.profile); + } + + // If the backend does not support filtering by start and endtime on + // platform (< Fx40), do it on the client (much slower). + if (!this.traits.filterable) { + RecordingUtils.filterSamples(profilerData.profile, options.startTime || 0); + } + + return profilerData; + }), + + /** + * Invoked whenever a registered event was emitted by the profiler actor. + * + * @param object response + * The data received from the backend. + */ + _onProfilerEvent: function (_, { topic, subject, details }) { + if (topic === "console-api-profiler") { + if (subject.action === "profile") { + this.emit("console-profile-start", details); + } else if (subject.action === "profileEnd") { + this.emit("console-profile-stop", details); + } + } else if (topic === "profiler-stopped") { + this.emit("profiler-stopped"); + } + }, + + _checkProfilerStatus: Task.async(function* () { + // Calling `getStatus()` will emit the "profiler-status" on its own + yield this.getStatus(); + }), + + toString: () => "[object LegacyProfilerFront]" +}; + +/** + * Constructor for a facade around an underlying TimelineFront. + */ +function LegacyTimelineFront(target) { + this._target = target; + EventEmitter.decorate(this); +} + +LegacyTimelineFront.prototype = { + EVENTS: ["markers", "frames", "ticks"], + + connect: Task.async(function* () { + let supported = yield CompatUtils.timelineActorSupported(this._target); + this._front = supported ? + new TimelineFront(this._target.client, this._target.form) : + new CompatUtils.MockTimelineFront(); + + this.IS_MOCK = !supported; + + // Binds underlying actor events and consolidates them to a `timeline-data` + // exposed event. + this.EVENTS.forEach(type => { + let handler = this[`_on${type}`] = this._onTimelineData.bind(this, type); + this._front.on(type, handler); + }); + }), + + /** + * Override actor's destroy, so we can unregister listeners before + * destroying the underlying actor. + */ + destroy: Task.async(function* () { + this.EVENTS.forEach(type => this._front.off(type, this[`_on${type}`])); + yield this._front.destroy(); + }), + + /** + * An aggregate of all events (markers, frames, ticks) and exposes + * to PerformanceActorsConnection as a single event. + */ + _onTimelineData: function (type, ...data) { + this.emit("timeline-data", type, ...data); + }, + + toString: () => "[object LegacyTimelineFront]" +}; + +// Bind all the methods that directly proxy to the actor +PROFILER_ACTOR_METHODS.forEach(m => { + LegacyProfilerFront.prototype[m] = CompatUtils.callFrontMethod(m); +}); +TIMELINE_ACTOR_METHODS.forEach(m => { + LegacyTimelineFront.prototype[m] = CompatUtils.callFrontMethod(m); +}); + +exports.LegacyProfilerFront = LegacyProfilerFront; +exports.LegacyTimelineFront = LegacyTimelineFront; diff --git a/devtools/client/performance/legacy/compatibility.js b/devtools/client/performance/legacy/compatibility.js new file mode 100644 index 000000000..0c67800d0 --- /dev/null +++ b/devtools/client/performance/legacy/compatibility.js @@ -0,0 +1,66 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const EventEmitter = require("devtools/shared/event-emitter"); + +/** + * A dummy front decorated with the provided methods. + * + * @param array blueprint + * A list of [funcName, retVal] describing the class. + */ +function MockFront(blueprint) { + EventEmitter.decorate(this); + + for (let [funcName, retVal] of blueprint) { + this[funcName] = (x => typeof x === "function" ? x() : x).bind(this, retVal); + } +} + +function MockTimelineFront() { + MockFront.call(this, [ + ["destroy"], + ["start", 0], + ["stop", 0], + ]); +} + +/** + * Takes a TabTarget, and checks existence of a TimelineActor on + * the server, or if TEST_MOCK_TIMELINE_ACTOR is to be used. + * + * @param {TabTarget} target + * @return {Boolean} + */ +function timelineActorSupported(target) { + // This `target` property is used only in tests to test + // instances where the timeline actor is not available. + if (target.TEST_MOCK_TIMELINE_ACTOR) { + return false; + } + + return target.hasActor("timeline"); +} + +/** + * Returns a function to be used as a method on an "Front" in ./actors. + * Calls the underlying actor's method. + */ +function callFrontMethod(method) { + return function () { + // If there's no target or client on this actor facade, + // abort silently -- this occurs in tests when polling occurs + // after the test ends, when tests do not wait for toolbox destruction + // (which will destroy the actor facade, turning off the polling). + if (!this._target || !this._target.client) { + return undefined; + } + return this._front[method].apply(this._front, arguments); + }; +} + +exports.MockTimelineFront = MockTimelineFront; +exports.timelineActorSupported = timelineActorSupported; +exports.callFrontMethod = callFrontMethod; diff --git a/devtools/client/performance/legacy/front.js b/devtools/client/performance/legacy/front.js new file mode 100644 index 000000000..34fb16665 --- /dev/null +++ b/devtools/client/performance/legacy/front.js @@ -0,0 +1,484 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { Task } = require("devtools/shared/task"); + +const Services = require("Services"); +const promise = require("promise"); +const { extend } = require("sdk/util/object"); + +const Actors = require("devtools/client/performance/legacy/actors"); +const { LegacyPerformanceRecording } = require("devtools/client/performance/legacy/recording"); +const { importRecording } = require("devtools/client/performance/legacy/recording"); +const { normalizePerformanceFeatures } = require("devtools/shared/performance/recording-utils"); +const flags = require("devtools/shared/flags"); +const { getDeviceFront } = require("devtools/shared/device/device"); +const { getSystemInfo } = require("devtools/shared/system"); +const events = require("sdk/event/core"); +const { EventTarget } = require("sdk/event/target"); +const { Class } = require("sdk/core/heritage"); + +/** + * A connection to underlying actors (profiler, framerate, etc.) + * shared by all tools in a target. + */ +const LegacyPerformanceFront = Class({ + extends: EventTarget, + + LEGACY_FRONT: true, + + traits: { + features: { + withMarkers: true, + withTicks: true, + withMemory: false, + withFrames: false, + withGCEvents: false, + withDocLoadingEvents: false, + withAllocations: false, + }, + }, + + initialize: function (target) { + let { form, client } = target; + this._target = target; + this._form = form; + this._client = client; + this._pendingConsoleRecordings = []; + this._sitesPullTimeout = 0; + this._recordings = []; + + this._pipeToFront = this._pipeToFront.bind(this); + this._onTimelineData = this._onTimelineData.bind(this); + this._onConsoleProfileStart = this._onConsoleProfileStart.bind(this); + this._onConsoleProfileStop = this._onConsoleProfileStop.bind(this); + this._onProfilerStatus = this._onProfilerStatus.bind(this); + this._onProfilerUnexpectedlyStopped = this._onProfilerUnexpectedlyStopped.bind(this); + }, + + /** + * Initializes a connection to the profiler and other miscellaneous actors. + * If in the process of opening, or already open, nothing happens. + * + * @return object + * A promise that is resolved once the connection is established. + */ + connect: Task.async(function* () { + if (this._connecting) { + return this._connecting.promise; + } + + // Create a promise that gets resolved upon connecting, so that + // other attempts to open the connection use the same resolution promise + this._connecting = promise.defer(); + + // Sets `this._profiler`, `this._timeline`. + // Only initialize the timeline fronts if the respective actors + // are available. Older Gecko versions don't have existing implementations, + // in which case all the methods we need can be easily mocked. + yield this._connectActors(); + yield this._registerListeners(); + + this._connecting.resolve(); + return this._connecting.promise; + }), + + /** + * Destroys this connection. + */ + destroy: Task.async(function* () { + if (this._connecting) { + yield this._connecting.promise; + } else { + return; + } + + yield this._unregisterListeners(); + yield this._disconnectActors(); + + this._connecting = null; + this._profiler = null; + this._timeline = null; + this._client = null; + this._form = null; + this._target = this._target; + }), + + /** + * Initializes fronts and connects to the underlying actors using the facades + * found in ./actors.js. + */ + _connectActors: Task.async(function* () { + this._profiler = new Actors.LegacyProfilerFront(this._target); + this._timeline = new Actors.LegacyTimelineFront(this._target); + + yield promise.all([ + this._profiler.connect(), + this._timeline.connect() + ]); + + // If mocked timeline, update the traits + this.traits.features.withMarkers = !this._timeline.IS_MOCK; + this.traits.features.withTicks = !this._timeline.IS_MOCK; + }), + + /** + * Registers listeners on events from the underlying + * actors, so the connection can handle them. + */ + _registerListeners: function () { + this._timeline.on("timeline-data", this._onTimelineData); + this._profiler.on("console-profile-start", this._onConsoleProfileStart); + this._profiler.on("console-profile-stop", this._onConsoleProfileStop); + this._profiler.on("profiler-stopped", this._onProfilerUnexpectedlyStopped); + this._profiler.on("profiler-status", this._onProfilerStatus); + }, + + /** + * Unregisters listeners on events on the underlying actors. + */ + _unregisterListeners: function () { + this._timeline.off("timeline-data", this._onTimelineData); + this._profiler.off("console-profile-start", this._onConsoleProfileStart); + this._profiler.off("console-profile-stop", this._onConsoleProfileStop); + this._profiler.off("profiler-stopped", this._onProfilerUnexpectedlyStopped); + this._profiler.off("profiler-status", this._onProfilerStatus); + }, + + /** + * Closes the connections to non-profiler actors. + */ + _disconnectActors: Task.async(function* () { + yield promise.all([ + this._profiler.destroy(), + this._timeline.destroy(), + ]); + }), + + /** + * Invoked whenever `console.profile` is called. + * + * @param string profileLabel + * The provided string argument if available; undefined otherwise. + * @param number currentTime + * The time (in milliseconds) when the call was made, relative to when + * the nsIProfiler module was started. + */ + _onConsoleProfileStart: Task.async(function* (_, { profileLabel, + currentTime: startTime }) { + let recordings = this._recordings; + + // Abort if a profile with this label already exists. + if (recordings.find(e => e.getLabel() === profileLabel)) { + return; + } + + events.emit(this, "console-profile-start"); + + yield this.startRecording(extend({}, getLegacyPerformanceRecordingPrefs(), { + console: true, + label: profileLabel + })); + }), + + /** + * Invoked whenever `console.profileEnd` is called. + * + * @param string profileLabel + * The provided string argument if available; undefined otherwise. + * @param number currentTime + * The time (in milliseconds) when the call was made, relative to when + * the nsIProfiler module was started. + */ + _onConsoleProfileStop: Task.async(function* (_, data) { + // If no data, abort; can occur if profiler isn't running and we get a surprise + // call to console.profileEnd() + if (!data) { + return; + } + let { profileLabel } = data; + + let pending = this._recordings.filter(r => r.isConsole() && r.isRecording()); + if (pending.length === 0) { + return; + } + + let model; + // Try to find the corresponding `console.profile` call if + // a label was used in profileEnd(). If no matches, abort. + if (profileLabel) { + model = pending.find(e => e.getLabel() === profileLabel); + } else { + // If no label supplied, pop off the most recent pending console recording + model = pending[pending.length - 1]; + } + + // If `profileEnd()` was called with a label, and there are no matching + // sessions, abort. + if (!model) { + console.error( + "console.profileEnd() called with label that does not match a recording."); + return; + } + + yield this.stopRecording(model); + }), + + /** + * TODO handle bug 1144438 + */ + _onProfilerUnexpectedlyStopped: function () { + console.error("Profiler unexpectedly stopped.", arguments); + }, + + /** + * Called whenever there is timeline data of any of the following types: + * - markers + * - frames + * - ticks + * + * Populate our internal store of recordings for all currently recording sessions. + */ + _onTimelineData: function (_, ...data) { + this._recordings.forEach(e => e._addTimelineData.apply(e, data)); + events.emit(this, "timeline-data", ...data); + }, + + /** + * Called whenever the underlying profiler polls its current status. + */ + _onProfilerStatus: function (_, data) { + // If no data emitted (whether from an older actor being destroyed + // from a previous test, or the server does not support it), just ignore. + if (!data || data.position === void 0) { + return; + } + + this._currentBufferStatus = data; + events.emit(this, "profiler-status", data); + }, + + /** + * Begins a recording session + * + * @param object options + * An options object to pass to the actors. Supported properties are + * `withTicks`, `withMemory` and `withAllocations`, `probability`, and + * `maxLogLength`. + * @return object + * A promise that is resolved once recording has started. + */ + startRecording: Task.async(function* (options = {}) { + let model = new LegacyPerformanceRecording( + normalizePerformanceFeatures(options, this.traits.features)); + + // All actors are started asynchronously over the remote debugging protocol. + // Get the corresponding start times from each one of them. + // The timeline actors are target-dependent, so start those as well, + // even though these are mocked in older Geckos (FF < 35) + let profilerStart = this._profiler.start(options); + let timelineStart = this._timeline.start(options); + + let { startTime, position, generation, totalSize } = yield profilerStart; + let timelineStartTime = yield timelineStart; + + let data = { + profilerStartTime: startTime, timelineStartTime, + generation, position, totalSize + }; + + // Signify to the model that the recording has started, + // populate with data and store the recording model here. + model._populate(data); + this._recordings.push(model); + + events.emit(this, "recording-started", model); + return model; + }), + + /** + * Manually ends the recording session for the corresponding LegacyPerformanceRecording. + * + * @param LegacyPerformanceRecording model + * The corresponding LegacyPerformanceRecording that belongs to the recording + * session wished to stop. + * @return LegacyPerformanceRecording + * Returns the same model, populated with the profiling data. + */ + stopRecording: Task.async(function* (model) { + // If model isn't in the LegacyPerformanceFront internal store, + // then do nothing. + if (this._recordings.indexOf(model) === -1) { + return undefined; + } + + // Flag the recording as no longer recording, so that `model.isRecording()` + // is false. Do this before we fetch all the data, and then subsequently + // the recording can be considered "completed". + let endTime = Date.now(); + model._onStoppingRecording(endTime); + events.emit(this, "recording-stopping", model); + + // Currently there are two ways profiles stop recording. Either manually in the + // performance tool, or via console.profileEnd. Once a recording is done, + // we want to deliver the model to the performance tool (either as a return + // from the LegacyPerformanceFront or via `console-profile-stop` event) and then + // remove it from the internal store. + // + // In the case where a console.profile is generated via the console (so the tools are + // open), we initialize the Performance tool so it can listen to those events. + this._recordings.splice(this._recordings.indexOf(model), 1); + + let config = model.getConfiguration(); + let startTime = model._getProfilerStartTime(); + let profilerData = yield this._profiler.getProfile({ startTime }); + let timelineEndTime = Date.now(); + + // Only if there are no more sessions recording do we stop + // the underlying timeline actors. If we're still recording, + // juse use Date.now() for the timeline end times, as those + // are only used in tests. + if (!this.isRecording()) { + // This doesn't stop the profiler, just turns off polling for + // events, and also turns off events on timeline actors. + yield this._profiler.stop(); + timelineEndTime = yield this._timeline.stop(config); + } + + let form = yield this._client.listTabs(); + let systemHost = yield getDeviceFront(this._client, form).getDescription(); + let systemClient = yield getSystemInfo(); + + // Set the results on the LegacyPerformanceRecording itself. + model._onStopRecording({ + // Data available only at the end of a recording. + profile: profilerData.profile, + + // End times for all the actors. + profilerEndTime: profilerData.currentTime, + timelineEndTime: timelineEndTime, + systemHost, + systemClient, + }); + + events.emit(this, "recording-stopped", model); + return model; + }), + + /** + * Creates a recording object when given a nsILocalFile. + * + * @param {nsILocalFile} file + * The file to import the data from. + * @return {Promise} + */ + importRecording: function (file) { + return importRecording(file); + }, + + /** + * Checks all currently stored recording models and returns a boolean + * if there is a session currently being recorded. + * + * @return Boolean + */ + isRecording: function () { + return this._recordings.some(recording => recording.isRecording()); + }, + + /** + * Pass in a PerformanceRecording and get a normalized value from 0 to 1 of how much + * of this recording's lifetime remains without being overwritten. + * + * @param {PerformanceRecording} recording + * @return {number?} + */ + getBufferUsageForRecording: function (recording) { + if (!recording.isRecording() || !this._currentBufferStatus) { + return null; + } + let { + position: currentPosition, + totalSize, + generation: currentGeneration + } = this._currentBufferStatus; + let { + position: origPosition, + generation: origGeneration + } = recording.getStartingBufferStatus(); + + let normalizedCurrent = (totalSize * (currentGeneration - origGeneration)) + + currentPosition; + let percent = (normalizedCurrent - origPosition) / totalSize; + return percent > 1 ? 1 : percent; + }, + + /** + * Returns the configurations set on underlying components, used in tests. + * Returns an object with `probability`, `maxLogLength` for allocations, and + * `entries` and `interval` for profiler. + * + * @return {object} + */ + getConfiguration: Task.async(function* () { + let profilerConfig = yield this._request("profiler", "getStartOptions"); + return profilerConfig; + }), + + /** + * An event from an underlying actor that we just want + * to pipe to the front itself. + */ + _pipeToFront: function (eventName, ...args) { + events.emit(this, eventName, ...args); + }, + + /** + * Helper method to interface with the underlying actors directly. + * Used only in tests. + */ + _request: function (actorName, method, ...args) { + if (!flags.testing) { + throw new Error("LegacyPerformanceFront._request may only be used in tests."); + } + let actor = this[`_${actorName}`]; + return actor[method].apply(actor, args); + }, + + /** + * Sets how often the "profiler-status" event should be emitted. + * Used in tests. + */ + setProfilerStatusInterval: function (n) { + if (this._profiler._poller) { + this._profiler._poller._wait = n; + } + this._profiler._PROFILER_CHECK_TIMER = n; + }, + + toString: () => "[object LegacyPerformanceFront]" +}); + +/** + * Creates an object of configurations based off of preferences for a + * LegacyPerformanceRecording. + */ +function getLegacyPerformanceRecordingPrefs() { + return { + withMarkers: true, + withMemory: Services.prefs.getBoolPref( + "devtools.performance.ui.enable-memory"), + withTicks: Services.prefs.getBoolPref( + "devtools.performance.ui.enable-framerate"), + withAllocations: Services.prefs.getBoolPref( + "devtools.performance.ui.enable-allocations"), + allocationsSampleProbability: +Services.prefs.getCharPref( + "devtools.performance.memory.sample-probability"), + allocationsMaxLogLength: Services.prefs.getIntPref( + "devtools.performance.memory.max-log-length") + }; +} + +exports.LegacyPerformanceFront = LegacyPerformanceFront; diff --git a/devtools/client/performance/legacy/moz.build b/devtools/client/performance/legacy/moz.build new file mode 100644 index 000000000..00eab217b --- /dev/null +++ b/devtools/client/performance/legacy/moz.build @@ -0,0 +1,12 @@ +# -*- 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( + 'actors.js', + 'compatibility.js', + 'front.js', + 'recording.js', +) diff --git a/devtools/client/performance/legacy/recording.js b/devtools/client/performance/legacy/recording.js new file mode 100644 index 000000000..2ba141471 --- /dev/null +++ b/devtools/client/performance/legacy/recording.js @@ -0,0 +1,174 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { Task } = require("devtools/shared/task"); + +const PerformanceIO = require("devtools/client/performance/modules/io"); +const RecordingUtils = require("devtools/shared/performance/recording-utils"); +const { PerformanceRecordingCommon } = require("devtools/shared/performance/recording-common"); +const { merge } = require("sdk/util/object"); + +/** + * Model for a wholistic profile, containing the duration, profiling data, + * frames data, timeline (marker, tick, memory) data, and methods to mark + * a recording as 'in progress' or 'finished'. + */ +const LegacyPerformanceRecording = function (options = {}) { + this._label = options.label || ""; + this._console = options.console || false; + + this._configuration = { + withMarkers: options.withMarkers || false, + withTicks: options.withTicks || false, + withMemory: options.withMemory || false, + withAllocations: options.withAllocations || false, + allocationsSampleProbability: options.allocationsSampleProbability || 0, + allocationsMaxLogLength: options.allocationsMaxLogLength || 0, + bufferSize: options.bufferSize || 0, + sampleFrequency: options.sampleFrequency || 1 + }; +}; + +LegacyPerformanceRecording.prototype = merge({ + _profilerStartTime: 0, + _timelineStartTime: 0, + _memoryStartTime: 0, + + /** + * Saves the current recording to a file. + * + * @param nsILocalFile file + * The file to stream the data into. + */ + exportRecording: Task.async(function* (file) { + let recordingData = this.getAllData(); + yield PerformanceIO.saveRecordingToFile(recordingData, file); + }), + + /** + * Sets up the instance with data from the PerformanceFront when + * starting a recording. Should only be called by PerformanceFront. + */ + _populate: function (info) { + // Times must come from the actor in order to be self-consistent. + // However, we also want to update the view with the elapsed time + // even when the actor is not generating data. To do this we get + // the local time and use it to compute a reasonable elapsed time. + this._localStartTime = Date.now(); + + this._profilerStartTime = info.profilerStartTime; + this._timelineStartTime = info.timelineStartTime; + this._memoryStartTime = info.memoryStartTime; + this._startingBufferStatus = { + position: info.position, + totalSize: info.totalSize, + generation: info.generation + }; + + this._recording = true; + + this._systemHost = {}; + this._systemClient = {}; + this._markers = []; + this._frames = []; + this._memory = []; + this._ticks = []; + this._allocations = { sites: [], timestamps: [], frames: [], sizes: [] }; + }, + + /** + * Called when the signal was sent to the front to no longer record more + * data, and begin fetching the data. There's some delay during fetching, + * even though the recording is stopped, the model is not yet completed until + * all the data is fetched. + */ + _onStoppingRecording: function (endTime) { + this._duration = endTime - this._localStartTime; + this._recording = false; + }, + + /** + * Sets results available from stopping a recording from PerformanceFront. + * Should only be called by PerformanceFront. + */ + _onStopRecording: Task.async(function* ({ profilerEndTime, profile, systemClient, + systemHost }) { + // Update the duration with the accurate profilerEndTime, so we don't have + // samples outside of the approximate duration set in `_onStoppingRecording`. + this._duration = profilerEndTime - this._profilerStartTime; + this._profile = profile; + this._completed = true; + + // We filter out all samples that fall out of current profile's range + // since the profiler is continuously running. Because of this, sample + // times are not guaranteed to have a zero epoch, so offset the + // timestamps. + RecordingUtils.offsetSampleTimes(this._profile, this._profilerStartTime); + + // Markers need to be sorted ascending by time, to be properly displayed + // in a waterfall view. + this._markers = this._markers.sort((a, b) => (a.start > b.start)); + + this._systemHost = systemHost; + this._systemClient = systemClient; + }), + + /** + * Gets the profile's start time. + * @return number + */ + _getProfilerStartTime: function () { + return this._profilerStartTime; + }, + + /** + * Fired whenever the PerformanceFront emits markers, memory or ticks. + */ + _addTimelineData: function (eventName, ...data) { + // If this model isn't currently recording, + // ignore the timeline data. + if (!this.isRecording()) { + return; + } + + let config = this.getConfiguration(); + + switch (eventName) { + // Accumulate timeline markers into an array. Furthermore, the timestamps + // do not have a zero epoch, so offset all of them by the start time. + case "markers": { + if (!config.withMarkers) { + break; + } + let [markers] = data; + RecordingUtils.offsetMarkerTimes(markers, this._timelineStartTime); + RecordingUtils.pushAll(this._markers, markers); + break; + } + // Accumulate stack frames into an array. + case "frames": { + if (!config.withMarkers) { + break; + } + let [, frames] = data; + RecordingUtils.pushAll(this._frames, frames); + break; + } + // Save the accumulated refresh driver ticks. + case "ticks": { + if (!config.withTicks) { + break; + } + let [, timestamps] = data; + this._ticks = timestamps; + break; + } + } + }, + + toString: () => "[object LegacyPerformanceRecording]" +}, PerformanceRecordingCommon); + +exports.LegacyPerformanceRecording = LegacyPerformanceRecording; diff --git a/devtools/client/performance/modules/categories.js b/devtools/client/performance/modules/categories.js new file mode 100644 index 000000000..f3f05d567 --- /dev/null +++ b/devtools/client/performance/modules/categories.js @@ -0,0 +1,128 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { L10N } = require("devtools/client/performance/modules/global"); + +/** + * Details about each profile pseudo-stack entry cateogry. + * @see CATEGORY_MAPPINGS. + */ +const CATEGORIES = [{ + color: "#5e88b0", + abbrev: "other", + label: L10N.getStr("category.other") +}, { + color: "#46afe3", + abbrev: "css", + label: L10N.getStr("category.css") +}, { + color: "#d96629", + abbrev: "js", + label: L10N.getStr("category.js") +}, { + color: "#eb5368", + abbrev: "gc", + label: L10N.getStr("category.gc") +}, { + color: "#df80ff", + abbrev: "network", + label: L10N.getStr("category.network") +}, { + color: "#70bf53", + abbrev: "graphics", + label: L10N.getStr("category.graphics") +}, { + color: "#8fa1b2", + abbrev: "storage", + label: L10N.getStr("category.storage") +}, { + color: "#d99b28", + abbrev: "events", + label: L10N.getStr("category.events") +}, { + color: "#8fa1b2", + abbrev: "tools", + label: L10N.getStr("category.tools") +}]; + +/** + * Mapping from category bitmasks in the profiler data to additional details. + * To be kept in sync with the js::ProfileEntry::Category in ProfilingStack.h + */ +const CATEGORY_MAPPINGS = { + // js::ProfileEntry::Category::OTHER + "16": CATEGORIES[0], + // js::ProfileEntry::Category::CSS + "32": CATEGORIES[1], + // js::ProfileEntry::Category::JS + "64": CATEGORIES[2], + // js::ProfileEntry::Category::GC + "128": CATEGORIES[3], + // js::ProfileEntry::Category::CC + "256": CATEGORIES[3], + // js::ProfileEntry::Category::NETWORK + "512": CATEGORIES[4], + // js::ProfileEntry::Category::GRAPHICS + "1024": CATEGORIES[5], + // js::ProfileEntry::Category::STORAGE + "2048": CATEGORIES[6], + // js::ProfileEntry::Category::EVENTS + "4096": CATEGORIES[7], + // non-bitmasks for specially-assigned categories + "9000": CATEGORIES[8], +}; + +/** + * Get the numeric bitmask (or set of masks) for the given category + * abbreviation. See `CATEGORIES` and `CATEGORY_MAPPINGS` above. + * + * CATEGORY_MASK can be called with just a name if it is expected that the + * category is mapped to by exactly one bitmask. If the category is mapped + * to by multiple masks, CATEGORY_MASK for that name must be called with + * an additional argument specifying the desired id (in ascending order). + */ +const [CATEGORY_MASK, CATEGORY_MASK_LIST] = (() => { + let bitmasksForCategory = {}; + let all = Object.keys(CATEGORY_MAPPINGS); + + for (let category of CATEGORIES) { + bitmasksForCategory[category.abbrev] = all + .filter(mask => CATEGORY_MAPPINGS[mask] == category) + .map(mask => +mask) + .sort(); + } + + return [ + function (name, index) { + if (!(name in bitmasksForCategory)) { + throw new Error(`Category abbreviation "${name}" does not exist.`); + } + if (arguments.length == 1) { + if (bitmasksForCategory[name].length != 1) { + throw new Error(`Expected exactly one category number for "${name}".`); + } else { + return bitmasksForCategory[name][0]; + } + } else { + if (index > bitmasksForCategory[name].length) { + throw new Error(`Index "${index}" too high for category "${name}".`); + } + return bitmasksForCategory[name][index - 1]; + } + }, + + function (name) { + if (!(name in bitmasksForCategory)) { + throw new Error(`Category abbreviation "${name}" does not exist.`); + } + return bitmasksForCategory[name]; + } + ]; +})(); + +exports.CATEGORIES = CATEGORIES; +exports.CATEGORY_MAPPINGS = CATEGORY_MAPPINGS; +exports.CATEGORY_MASK = CATEGORY_MASK; +exports.CATEGORY_MASK_LIST = CATEGORY_MASK_LIST; diff --git a/devtools/client/performance/modules/constants.js b/devtools/client/performance/modules/constants.js new file mode 100644 index 000000000..a0adaf596 --- /dev/null +++ b/devtools/client/performance/modules/constants.js @@ -0,0 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +exports.Constants = { + // ms + FRAMERATE_GRAPH_LOW_RES_INTERVAL: 100, + // ms + FRAMERATE_GRAPH_HIGH_RES_INTERVAL: 16, +}; diff --git a/devtools/client/performance/modules/global.js b/devtools/client/performance/modules/global.js new file mode 100644 index 000000000..0c6c86f10 --- /dev/null +++ b/devtools/client/performance/modules/global.js @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { MultiLocalizationHelper } = require("devtools/shared/l10n"); +const { PrefsHelper } = require("devtools/client/shared/prefs"); + +/** + * Localization convenience methods. + */ +exports.L10N = new MultiLocalizationHelper( + "devtools/client/locales/markers.properties", + "devtools/client/locales/performance.properties" +); + +/** + * A list of preferences for this tool. The values automatically update + * if somebody edits edits about:config or the prefs change somewhere else. + * + * This needs to be registered and unregistered when used for the auto-update + * functionality to work. The PerformanceController handles this, but if you + * just use this module in a test independently, ensure you call + * `registerObserver()` and `unregisterUnobserver()`. + */ +exports.PREFS = new PrefsHelper("devtools.performance", { + "show-triggers-for-gc-types": ["Char", "ui.show-triggers-for-gc-types"], + "show-platform-data": ["Bool", "ui.show-platform-data"], + "hidden-markers": ["Json", "timeline.hidden-markers"], + "memory-sample-probability": ["Float", "memory.sample-probability"], + "memory-max-log-length": ["Int", "memory.max-log-length"], + "profiler-buffer-size": ["Int", "profiler.buffer-size"], + "profiler-sample-frequency": ["Int", "profiler.sample-frequency-khz"], + // TODO: re-enable once we flame charts via bug 1148663. + "enable-memory-flame": ["Bool", "ui.enable-memory-flame"], +}); diff --git a/devtools/client/performance/modules/io.js b/devtools/client/performance/modules/io.js new file mode 100644 index 000000000..08bfd034c --- /dev/null +++ b/devtools/client/performance/modules/io.js @@ -0,0 +1,171 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { Cc, Ci } = require("chrome"); + +const RecordingUtils = require("devtools/shared/performance/recording-utils"); +const { FileUtils } = require("resource://gre/modules/FileUtils.jsm"); +const { NetUtil } = require("resource://gre/modules/NetUtil.jsm"); + +// This identifier string is used to tentatively ascertain whether or not +// a JSON loaded from disk is actually something generated by this tool. +// It isn't, of course, a definitive verification, but a Good Enough™ +// approximation before continuing the import. Don't localize this. +const PERF_TOOL_SERIALIZER_IDENTIFIER = "Recorded Performance Data"; +const PERF_TOOL_SERIALIZER_LEGACY_VERSION = 1; +const PERF_TOOL_SERIALIZER_CURRENT_VERSION = 2; + +/** + * Helpers for importing/exporting JSON. + */ + +/** + * Gets a nsIScriptableUnicodeConverter instance with a default UTF-8 charset. + * @return object + */ +function getUnicodeConverter() { + let cname = "@mozilla.org/intl/scriptableunicodeconverter"; + let converter = Cc[cname].createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + return converter; +} + +/** + * Saves a recording as JSON to a file. The provided data is assumed to be + * acyclical, so that it can be properly serialized. + * + * @param object recordingData + * The recording data to stream as JSON. + * @param nsILocalFile file + * The file to stream the data into. + * @return object + * A promise that is resolved once streaming finishes, or rejected + * if there was an error. + */ +function saveRecordingToFile(recordingData, file) { + recordingData.fileType = PERF_TOOL_SERIALIZER_IDENTIFIER; + recordingData.version = PERF_TOOL_SERIALIZER_CURRENT_VERSION; + + let string = JSON.stringify(recordingData); + let inputStream = getUnicodeConverter().convertToInputStream(string); + let outputStream = FileUtils.openSafeFileOutputStream(file); + + return new Promise(resolve => { + NetUtil.asyncCopy(inputStream, outputStream, resolve); + }); +} + +/** + * Loads a recording stored as JSON from a file. + * + * @param nsILocalFile file + * The file to import the data from. + * @return object + * A promise that is resolved once importing finishes, or rejected + * if there was an error. + */ +function loadRecordingFromFile(file) { + let channel = NetUtil.newChannel({ + uri: NetUtil.newURI(file), + loadUsingSystemPrincipal: true + }); + + channel.contentType = "text/plain"; + + return new Promise((resolve, reject) => { + NetUtil.asyncFetch(channel, (inputStream) => { + let recordingData; + + try { + let string = NetUtil.readInputStreamToString(inputStream, + inputStream.available()); + recordingData = JSON.parse(string); + } catch (e) { + reject(new Error("Could not read recording data file.")); + return; + } + + if (recordingData.fileType != PERF_TOOL_SERIALIZER_IDENTIFIER) { + reject(new Error("Unrecognized recording data file.")); + return; + } + + if (!isValidSerializerVersion(recordingData.version)) { + reject(new Error("Unsupported recording data file version.")); + return; + } + + if (recordingData.version === PERF_TOOL_SERIALIZER_LEGACY_VERSION) { + recordingData = convertLegacyData(recordingData); + } + + if (recordingData.profile.meta.version === 2) { + RecordingUtils.deflateProfile(recordingData.profile); + } + + // If the recording has no label, set it to be the + // filename without its extension. + if (!recordingData.label) { + recordingData.label = file.leafName.replace(/\.[^.]+$/, ""); + } + + resolve(recordingData); + }); + }); +} + +/** + * Returns a boolean indicating whether or not the passed in `version` + * is supported by this serializer. + * + * @param number version + * @return boolean + */ +function isValidSerializerVersion(version) { + return !!~[ + PERF_TOOL_SERIALIZER_LEGACY_VERSION, + PERF_TOOL_SERIALIZER_CURRENT_VERSION + ].indexOf(version); +} + +/** + * Takes recording data (with version `1`, from the original profiler tool), + * and massages the data to be line with the current performance tool's + * property names and values. + * + * @param object legacyData + * @return object + */ +function convertLegacyData(legacyData) { + let { profilerData, ticksData, recordingDuration } = legacyData; + + // The `profilerData` and `ticksData` stay, but the previously unrecorded + // fields just are empty arrays or objects. + let data = { + label: profilerData.profilerLabel, + duration: recordingDuration, + markers: [], + frames: [], + memory: [], + ticks: ticksData, + allocations: { sites: [], timestamps: [], frames: [], sizes: [] }, + profile: profilerData.profile, + // Fake a configuration object here if there's tick data, + // so that it can be rendered. + configuration: { + withTicks: !!ticksData.length, + withMarkers: false, + withMemory: false, + withAllocations: false + }, + systemHost: {}, + systemClient: {}, + }; + + return data; +} + +exports.saveRecordingToFile = saveRecordingToFile; +exports.loadRecordingFromFile = loadRecordingFromFile; diff --git a/devtools/client/performance/modules/logic/frame-utils.js b/devtools/client/performance/modules/logic/frame-utils.js new file mode 100644 index 000000000..f82996be2 --- /dev/null +++ b/devtools/client/performance/modules/logic/frame-utils.js @@ -0,0 +1,478 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const global = require("devtools/client/performance/modules/global"); +const demangle = require("devtools/client/shared/demangle"); +const { assert } = require("devtools/shared/DevToolsUtils"); +const { isChromeScheme, isContentScheme, parseURL } = + require("devtools/client/shared/source-utils"); + +const { CATEGORY_MASK, CATEGORY_MAPPINGS } = require("devtools/client/performance/modules/categories"); + +// Character codes used in various parsing helper functions. +const CHAR_CODE_R = "r".charCodeAt(0); +const CHAR_CODE_0 = "0".charCodeAt(0); +const CHAR_CODE_9 = "9".charCodeAt(0); +const CHAR_CODE_CAP_Z = "Z".charCodeAt(0); + +const CHAR_CODE_LPAREN = "(".charCodeAt(0); +const CHAR_CODE_RPAREN = ")".charCodeAt(0); +const CHAR_CODE_COLON = ":".charCodeAt(0); +const CHAR_CODE_SPACE = " ".charCodeAt(0); +const CHAR_CODE_UNDERSCORE = "_".charCodeAt(0); + +const EVAL_TOKEN = "%20%3E%20eval"; + +// The cache used to store inflated frames. +const gInflatedFrameStore = new WeakMap(); + +// The cache used to store frame data from `getInfo`. +const gFrameData = new WeakMap(); + +/** + * Parses the raw location of this function call to retrieve the actual + * function name, source url, host name, line and column. + */ +function parseLocation(location, fallbackLine, fallbackColumn) { + // Parse the `location` for the function name, source url, line, column etc. + + let line, column, url; + + // These two indices are used to extract the resource substring, which is + // location[parenIndex + 1 .. lineAndColumnIndex]. + // + // There are 3 variants of location strings in the profiler (with optional + // column numbers): + // 1) "name (resource:line)" + // 2) "resource:line" + // 3) "resource" + // + // For example for (1), take "foo (bar.js:1)". + // ^ ^ + // | | + // | | + // | | + // parenIndex will point to ------+ | + // | + // lineAndColumnIndex will point to -----+ + // + // For an example without parentheses, take "bar.js:2". + // ^ ^ + // | | + // parenIndex will point to ----------------+ | + // | + // lineAndColumIndex will point to ----------------+ + // + // To parse, we look for the last occurrence of the string ' ('. + // + // For 1), all occurrences of space ' ' characters in the resource string + // are urlencoded, so the last occurrence of ' (' is the separator between + // the function name and the resource. + // + // For 2) and 3), there can be no occurences of ' (' since ' ' characters + // are urlencoded in the resource string. + // + // XXX: Note that 3) is ambiguous with SPS marker locations like + // "EnterJIT". We can't distinguish the two, so we treat 3) like a function + // name. + let parenIndex = -1; + let lineAndColumnIndex = -1; + + let lastCharCode = location.charCodeAt(location.length - 1); + let i; + if (lastCharCode === CHAR_CODE_RPAREN) { + // Case 1) + i = location.length - 2; + } else if (isNumeric(lastCharCode)) { + // Case 2) + i = location.length - 1; + } else { + // Case 3) + i = 0; + } + + if (i !== 0) { + // Look for a :number. + let end = i; + while (isNumeric(location.charCodeAt(i))) { + i--; + } + if (location.charCodeAt(i) === CHAR_CODE_COLON) { + column = location.substr(i + 1, end - i); + i--; + } + + // Look for a preceding :number. + end = i; + while (isNumeric(location.charCodeAt(i))) { + i--; + } + + // If two were found, the first is the line and the second is the + // column. If only a single :number was found, then it is the line number. + if (location.charCodeAt(i) === CHAR_CODE_COLON) { + line = location.substr(i + 1, end - i); + lineAndColumnIndex = i; + i--; + } else { + lineAndColumnIndex = i + 1; + line = column; + column = undefined; + } + } + + // Look for the last occurrence of ' (' in case 1). + if (lastCharCode === CHAR_CODE_RPAREN) { + for (; i >= 0; i--) { + if (location.charCodeAt(i) === CHAR_CODE_LPAREN && + i > 0 && + location.charCodeAt(i - 1) === CHAR_CODE_SPACE) { + parenIndex = i; + break; + } + } + } + + let parsedUrl; + if (lineAndColumnIndex > 0) { + let resource = location.substring(parenIndex + 1, lineAndColumnIndex); + url = resource.split(" -> ").pop(); + if (url) { + parsedUrl = parseURL(url); + } + } + + let functionName, fileName, port, host; + line = line || fallbackLine; + column = column || fallbackColumn; + + // If the URL digged out from the `location` is valid, this is a JS frame. + if (parsedUrl) { + functionName = location.substring(0, parenIndex - 1); + fileName = parsedUrl.fileName; + port = parsedUrl.port; + host = parsedUrl.host; + + // Check for the case of the filename containing eval + // e.g. "file.js%20line%2065%20%3E%20eval" + let evalIndex = fileName.indexOf(EVAL_TOKEN); + if (evalIndex !== -1 && evalIndex === (fileName.length - EVAL_TOKEN.length)) { + // Match the filename + let evalLine = line; + let [, _fileName, , _line] = fileName.match(/(.+)(%20line%20(\d+)%20%3E%20eval)/) + || []; + fileName = `${_fileName} (eval:${evalLine})`; + line = _line; + assert(_fileName !== undefined, + "Filename could not be found from an eval location site"); + assert(_line !== undefined, + "Line could not be found from an eval location site"); + + // Match the url as well + [, url] = url.match(/(.+)( line (\d+) > eval)/) || []; + assert(url !== undefined, + "The URL could not be parsed correctly from an eval location site"); + } + } else { + functionName = location; + url = null; + } + + return { functionName, fileName, host, port, url, line, column }; +} + +/** + * Sets the properties of `isContent` and `category` on a frame. + * + * @param {InflatedFrame} frame + */ +function computeIsContentAndCategory(frame) { + // Only C++ stack frames have associated category information. + if (frame.category) { + return; + } + + let location = frame.location; + + // There are 3 variants of location strings in the profiler (with optional + // column numbers): + // 1) "name (resource:line)" + // 2) "resource:line" + // 3) "resource" + let lastCharCode = location.charCodeAt(location.length - 1); + let schemeStartIndex = -1; + if (lastCharCode === CHAR_CODE_RPAREN) { + // Case 1) + // + // Need to search for the last occurrence of ' (' to find the start of the + // resource string. + for (let i = location.length - 2; i >= 0; i--) { + if (location.charCodeAt(i) === CHAR_CODE_LPAREN && + i > 0 && + location.charCodeAt(i - 1) === CHAR_CODE_SPACE) { + schemeStartIndex = i + 1; + break; + } + } + } else { + // Cases 2) and 3) + schemeStartIndex = 0; + } + + if (isContentScheme(location, schemeStartIndex)) { + frame.isContent = true; + return; + } + + if (schemeStartIndex !== 0) { + for (let j = schemeStartIndex; j < location.length; j++) { + if (location.charCodeAt(j) === CHAR_CODE_R && + isChromeScheme(location, j) && + (location.indexOf("resource://devtools") !== -1 || + location.indexOf("resource://devtools") !== -1)) { + frame.category = CATEGORY_MASK("tools"); + return; + } + } + } + + if (location === "EnterJIT") { + frame.category = CATEGORY_MASK("js"); + return; + } + + frame.category = CATEGORY_MASK("other"); +} + +/** + * Get caches to cache inflated frames and computed frame keys of a frame + * table. + * + * @param object framesTable + * @return object + */ +function getInflatedFrameCache(frameTable) { + let inflatedCache = gInflatedFrameStore.get(frameTable); + if (inflatedCache !== undefined) { + return inflatedCache; + } + + // Fill with nulls to ensure no holes. + inflatedCache = Array.from({ length: frameTable.data.length }, () => null); + gInflatedFrameStore.set(frameTable, inflatedCache); + return inflatedCache; +} + +/** + * Get or add an inflated frame to a cache. + * + * @param object cache + * @param number index + * @param object frameTable + * @param object stringTable + */ +function getOrAddInflatedFrame(cache, index, frameTable, stringTable) { + let inflatedFrame = cache[index]; + if (inflatedFrame === null) { + inflatedFrame = cache[index] = new InflatedFrame(index, frameTable, stringTable); + } + return inflatedFrame; +} + +/** + * An intermediate data structured used to hold inflated frames. + * + * @param number index + * @param object frameTable + * @param object stringTable + */ +function InflatedFrame(index, frameTable, stringTable) { + const LOCATION_SLOT = frameTable.schema.location; + const IMPLEMENTATION_SLOT = frameTable.schema.implementation; + const OPTIMIZATIONS_SLOT = frameTable.schema.optimizations; + const LINE_SLOT = frameTable.schema.line; + const CATEGORY_SLOT = frameTable.schema.category; + + let frame = frameTable.data[index]; + let category = frame[CATEGORY_SLOT]; + this.location = stringTable[frame[LOCATION_SLOT]]; + this.implementation = frame[IMPLEMENTATION_SLOT]; + this.optimizations = frame[OPTIMIZATIONS_SLOT]; + this.line = frame[LINE_SLOT]; + this.column = undefined; + this.category = category; + this.isContent = false; + + // Attempt to compute if this frame is a content frame, and if not, + // its category. + // + // Since only C++ stack frames have associated category information, + // attempt to generate a useful category, fallback to the one provided + // by the profiling data, or fallback to an unknown category. + computeIsContentAndCategory(this); +} + +/** + * Gets the frame key (i.e., equivalence group) according to options. Content + * frames are always identified by location. Chrome frames are identified by + * location if content-only filtering is off. If content-filtering is on, they + * are identified by their category. + * + * @param object options + * @return string + */ +InflatedFrame.prototype.getFrameKey = function getFrameKey(options) { + if (this.isContent || !options.contentOnly || options.isRoot) { + options.isMetaCategoryOut = false; + return this.location; + } + + if (options.isLeaf) { + // We only care about leaf platform frames if we are displaying content + // only. If no category is present, give the default category of "other". + // + // 1. The leaf is where time is _actually_ being spent, so we _need_ to + // show it to developers in some way to give them accurate profiling + // data. We decide to split the platform into various category buckets + // and just show time spent in each bucket. + // + // 2. The calls leading to the leaf _aren't_ where we are spending time, + // but _do_ give the developer context for how they got to the leaf + // where they _are_ spending time. For non-platform hackers, the + // non-leaf platform frames don't give any meaningful context, and so we + // can safely filter them out. + options.isMetaCategoryOut = true; + return this.category; + } + + // Return an empty string denoting that this frame should be skipped. + return ""; +}; + +function isNumeric(c) { + return c >= CHAR_CODE_0 && c <= CHAR_CODE_9; +} + +function shouldDemangle(name) { + return name && name.charCodeAt && + name.charCodeAt(0) === CHAR_CODE_UNDERSCORE && + name.charCodeAt(1) === CHAR_CODE_UNDERSCORE && + name.charCodeAt(2) === CHAR_CODE_CAP_Z; +} + +/** + * Calculates the relative costs of this frame compared to a root, + * and generates allocations information if specified. Uses caching + * if possible. + * + * @param {ThreadNode|FrameNode} node + * The node we are calculating. + * @param {ThreadNode} options.root + * The root thread node to calculate relative costs. + * Generates [self|total] [duration|percentage] values. + * @param {boolean} options.allocations + * Generates `totalAllocations` and `selfAllocations`. + * + * @return {object} + */ +function getFrameInfo(node, options) { + let data = gFrameData.get(node); + + if (!data) { + if (node.nodeType === "Thread") { + data = Object.create(null); + data.functionName = global.L10N.getStr("table.root"); + } else { + data = parseLocation(node.location, node.line, node.column); + data.hasOptimizations = node.hasOptimizations(); + data.isContent = node.isContent; + data.isMetaCategory = node.isMetaCategory; + } + data.samples = node.youngestFrameSamples; + data.categoryData = CATEGORY_MAPPINGS[node.category] || {}; + data.nodeType = node.nodeType; + + // Frame name (function location or some meta information) + if (data.isMetaCategory) { + data.name = data.categoryData.label; + } else if (shouldDemangle(data.functionName)) { + data.name = demangle(data.functionName); + } else { + data.name = data.functionName; + } + + data.tooltiptext = data.isMetaCategory ? + data.categoryData.label : + node.location || ""; + + gFrameData.set(node, data); + } + + // If no options specified, we can't calculate relative values, abort here + if (!options) { + return data; + } + + // If a root specified, calculate the relative costs in the context of + // this call tree. The cached store may already have this, but generate + // if it does not. + let totalSamples = options.root.samples; + let totalDuration = options.root.duration; + if (options && options.root && !data.COSTS_CALCULATED) { + data.selfDuration = node.youngestFrameSamples / totalSamples * totalDuration; + data.selfPercentage = node.youngestFrameSamples / totalSamples * 100; + data.totalDuration = node.samples / totalSamples * totalDuration; + data.totalPercentage = node.samples / totalSamples * 100; + data.COSTS_CALCULATED = true; + } + + if (options && options.allocations && !data.ALLOCATION_DATA_CALCULATED) { + let totalBytes = options.root.byteSize; + data.selfCount = node.youngestFrameSamples; + data.totalCount = node.samples; + data.selfCountPercentage = node.youngestFrameSamples / totalSamples * 100; + data.totalCountPercentage = node.samples / totalSamples * 100; + data.selfSize = node.youngestFrameByteSize; + data.totalSize = node.byteSize; + data.selfSizePercentage = node.youngestFrameByteSize / totalBytes * 100; + data.totalSizePercentage = node.byteSize / totalBytes * 100; + data.ALLOCATION_DATA_CALCULATED = true; + } + + return data; +} + +exports.getFrameInfo = getFrameInfo; + +/** + * Takes an inverted ThreadNode and searches its youngest frames for + * a FrameNode with matching location. + * + * @param {ThreadNode} threadNode + * @param {string} location + * @return {?FrameNode} + */ +function findFrameByLocation(threadNode, location) { + if (!threadNode.inverted) { + throw new Error( + "FrameUtils.findFrameByLocation only supports leaf nodes in an inverted tree."); + } + + let calls = threadNode.calls; + for (let i = 0; i < calls.length; i++) { + if (calls[i].location === location) { + return calls[i]; + } + } + return null; +} + +exports.findFrameByLocation = findFrameByLocation; +exports.computeIsContentAndCategory = computeIsContentAndCategory; +exports.parseLocation = parseLocation; +exports.getInflatedFrameCache = getInflatedFrameCache; +exports.getOrAddInflatedFrame = getOrAddInflatedFrame; +exports.InflatedFrame = InflatedFrame; +exports.shouldDemangle = shouldDemangle; diff --git a/devtools/client/performance/modules/logic/jit.js b/devtools/client/performance/modules/logic/jit.js new file mode 100644 index 000000000..a958c3c4a --- /dev/null +++ b/devtools/client/performance/modules/logic/jit.js @@ -0,0 +1,342 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +// An outcome of an OptimizationAttempt that is considered successful. +const SUCCESSFUL_OUTCOMES = [ + "GenericSuccess", "Inlined", "DOM", "Monomorphic", "Polymorphic" +]; + +/** + * Model representing JIT optimization sites from the profiler + * for a frame (represented by a FrameNode). Requires optimization data from + * a profile, which is an array of RawOptimizationSites. + * + * When the ThreadNode for the profile iterates over the samples' frames, each + * frame's optimizations are accumulated in their respective FrameNodes. Each + * FrameNode may contain many different optimization sites. One sample may + * pick up optimization X on line Y in the frame, with the next sample + * containing optimization Z on line W in the same frame, as each frame is + * only function. + * + * An OptimizationSite contains a record of how many times the + * RawOptimizationSite was sampled, as well as the unique id based off of the + * original profiler array, and the RawOptimizationSite itself as a reference. + * @see devtools/client/performance/modules/logic/tree-model.js + * + * @struct RawOptimizationSite + * A structure describing a location in a script that was attempted to be optimized. + * Contains all the IonTypes observed, and the sequence of OptimizationAttempts that + * were attempted, and the line and column in the script. This is retrieved from the + * profiler after a recording, and our base data structure. Should always be referenced, + * and unmodified. + * + * Note that propertyName is an index into a string table, which needs to be + * provided in order for the raw optimization site to be inflated. + * + * @type {Array} types + * @type {Array} attempts + * @type {?number} propertyName + * @type {number} line + * @type {number} column + * + * + * @struct IonType + * IonMonkey attempts to classify each value in an optimization site by some type. + * Based off of the observed types for a value (like a variable that could be a + * string or an instance of an object), it determines what kind of type it should be + * classified as. Each IonType here contains an array of all ObservedTypes under `types`, + * the Ion type that IonMonkey decided this value should be (Int32, Object, etc.) as + * `mirType`, and the component of this optimization type that this value refers to -- + * like a "getter" optimization, `a[b]`, has site `a` (the "Receiver") and `b` + * (the "Index"). + * + * Generally the more ObservedTypes, the more deoptimized this OptimizationSite is. + * There could be no ObservedTypes, in which case `typeset` is undefined. + * + * @type {?Array} typeset + * @type {string} site + * @type {string} mirType + * + * + * @struct ObservedType + * When IonMonkey attempts to determine what type a value is, it checks on each sample. + * The ObservedType can be thought of in more of JavaScripty-terms, rather than C++. + * The `keyedBy` property is a high level description of the type, like "primitive", + * "constructor", "function", "singleton", "alloc-site" (that one is a bit more weird). + * If the `keyedBy` type is a function or constructor, the ObservedType should have a + * `name` property, referring to the function or constructor name from the JS source. + * If IonMonkey can determine the origin of this type (like where the constructor is + * defined), the ObservedType will also have `location` and `line` properties, but + * `location` can sometimes be non-URL strings like "self-hosted" or a memory location + * like "102ca7880", or no location at all, and maybe `line` is 0 or undefined. + * + * @type {string} keyedBy + * @type {?string} name + * @type {?string} location + * @type {?string} line + * + * + * @struct OptimizationAttempt + * Each RawOptimizationSite contains an array of OptimizationAttempts. Generally, + * IonMonkey goes through a series of strategies for each kind of optimization, starting + * from most-niche and optimized, to the less-optimized, but more general strategies -- + * for example, a getter opt may first try to optimize for the scenario of a getter on an + * `arguments` object -- that will fail most of the time, as most objects are not + * arguments objects, but it will attempt several strategies in order until it finds a + * strategy that works, or fails. Even in the best scenarios, some attempts will fail + * (like the arguments getter example), which is OK, as long as some attempt succeeds + * (with the earlier attempts preferred, as those are more optimized). In an + * OptimizationAttempt structure, we store just the `strategy` name and `outcome` name, + * both from enums in js/public/TrackedOptimizationInfo.h as TRACKED_STRATEGY_LIST and + * TRACKED_OUTCOME_LIST, respectively. An array of successful outcome strings are above + * in SUCCESSFUL_OUTCOMES. + * + * @see js/public/TrackedOptimizationInfo.h + * + * @type {string} strategy + * @type {string} outcome + */ + +/* + * A wrapper around RawOptimizationSite to record sample count and ID (referring to the + * index of where this is in the initially seeded optimizations data), so we don't mutate + * the original data from the profiler. Provides methods to access the underlying + * optimization data easily, so understanding the semantics of JIT data isn't necessary. + * + * @constructor + * + * @param {Array} optimizations + * @param {number} optsIndex + * + * @type {RawOptimizationSite} data + * @type {number} samples + * @type {number} id + */ + +const OptimizationSite = function (id, opts) { + this.id = id; + this.data = opts; + this.samples = 1; +}; + +/** + * Constructor for JITOptimizations. A collection of OptimizationSites for a frame. + * + * @constructor + * @param {Array} rawSites + * Array of raw optimization sites. + * @param {Array} stringTable + * Array of strings from the profiler used to inflate + * JIT optimizations. Do not modify this! + */ + +const JITOptimizations = function (rawSites, stringTable) { + // Build a histogram of optimization sites. + let sites = []; + + for (let rawSite of rawSites) { + let existingSite = sites.find((site) => site.data === rawSite); + if (existingSite) { + existingSite.samples++; + } else { + sites.push(new OptimizationSite(sites.length, rawSite)); + } + } + + // Inflate the optimization information. + for (let site of sites) { + let data = site.data; + let STRATEGY_SLOT = data.attempts.schema.strategy; + let OUTCOME_SLOT = data.attempts.schema.outcome; + let attempts = data.attempts.data.map((a) => { + return { + id: site.id, + strategy: stringTable[a[STRATEGY_SLOT]], + outcome: stringTable[a[OUTCOME_SLOT]] + }; + }); + let types = data.types.map((t) => { + let typeset = maybeTypeset(t.typeset, stringTable); + if (typeset) { + typeset.forEach(ts => { + ts.id = site.id; + }); + } + + return { + id: site.id, + typeset, + site: stringTable[t.site], + mirType: stringTable[t.mirType] + }; + }); + // Add IDs to to all children objects, so we can correllate sites when + // just looking at a specific type, attempt, etc.. + attempts.id = types.id = site.id; + + site.data = { + attempts, + types, + propertyName: maybeString(stringTable, data.propertyName), + line: data.line, + column: data.column + }; + } + + this.optimizationSites = sites.sort((a, b) => b.samples - a.samples); +}; + +/** + * Make JITOptimizations iterable. + */ +JITOptimizations.prototype = { + [Symbol.iterator]: function* () { + yield* this.optimizationSites; + }, + + get length() { + return this.optimizationSites.length; + } +}; + +/** + * Takes an "outcome" string from an OptimizationAttempt and returns + * a boolean indicating whether or not its a successful outcome. + * + * @return {boolean} + */ + +function isSuccessfulOutcome(outcome) { + return !!~SUCCESSFUL_OUTCOMES.indexOf(outcome); +} + +/** + * Takes an OptimizationSite. Returns a boolean indicating if the passed + * in OptimizationSite has a "good" outcome at the end of its attempted strategies. + * + * @param {OptimizationSite} optimizationSite + * @return {boolean} + */ + +function hasSuccessfulOutcome(optimizationSite) { + let attempts = optimizationSite.data.attempts; + let lastOutcome = attempts[attempts.length - 1].outcome; + return isSuccessfulOutcome(lastOutcome); +} + +function maybeString(stringTable, index) { + return index ? stringTable[index] : undefined; +} + +function maybeTypeset(typeset, stringTable) { + if (!typeset) { + return undefined; + } + return typeset.map((ty) => { + return { + keyedBy: maybeString(stringTable, ty.keyedBy), + name: maybeString(stringTable, ty.name), + location: maybeString(stringTable, ty.location), + line: ty.line + }; + }); +} + +// Map of optimization implementation names to an enum. +const IMPLEMENTATION_MAP = { + "interpreter": 0, + "baseline": 1, + "ion": 2 +}; +const IMPLEMENTATION_NAMES = Object.keys(IMPLEMENTATION_MAP); + +/** + * Takes data from a FrameNode and computes rendering positions for + * a stacked mountain graph, to visualize JIT optimization tiers over time. + * + * @param {FrameNode} frameNode + * The FrameNode who's optimizations we're iterating. + * @param {Array} sampleTimes + * An array of every sample time within the range we're counting. + * From a ThreadNode's `sampleTimes` property. + * @param {number} bucketSize + * Size of each bucket in milliseconds. + * `duration / resolution = bucketSize` in OptimizationsGraph. + * @return {?Array} + */ +function createTierGraphDataFromFrameNode(frameNode, sampleTimes, bucketSize) { + let tierData = frameNode.getTierData(); + let stringTable = frameNode._stringTable; + let output = []; + let implEnum; + + let tierDataIndex = 0; + let nextOptSample = tierData[tierDataIndex]; + + // Bucket data + let samplesInCurrentBucket = 0; + let currentBucketStartTime = sampleTimes[0]; + let bucket = []; + + // Store previous data point so we can have straight vertical lines + let previousValues; + + // Iterate one after the samples, so we can finalize the last bucket + for (let i = 0; i <= sampleTimes.length; i++) { + let sampleTime = sampleTimes[i]; + + // If this sample is in the next bucket, or we're done + // checking sampleTimes and on the last iteration, finalize previous bucket + if (sampleTime >= (currentBucketStartTime + bucketSize) || + i >= sampleTimes.length) { + let dataPoint = {}; + dataPoint.values = []; + dataPoint.delta = currentBucketStartTime; + + // Map the opt site counts as a normalized percentage (0-1) + // of its count in context of total samples this bucket + for (let j = 0; j < IMPLEMENTATION_NAMES.length; j++) { + dataPoint.values[j] = (bucket[j] || 0) / (samplesInCurrentBucket || 1); + } + + // Push the values from the previous bucket to the same time + // as the current bucket so we get a straight vertical line. + if (previousValues) { + let data = Object.create(null); + data.values = previousValues; + data.delta = currentBucketStartTime; + output.push(data); + } + + output.push(dataPoint); + + // Set the new start time of this bucket and reset its count + currentBucketStartTime += bucketSize; + samplesInCurrentBucket = 0; + previousValues = dataPoint.values; + bucket = []; + } + + // If this sample observed an optimization in this frame, record it + if (nextOptSample && nextOptSample.time === sampleTime) { + // If no implementation defined, it was the "interpreter". + implEnum = IMPLEMENTATION_MAP[stringTable[nextOptSample.implementation] || + "interpreter"]; + bucket[implEnum] = (bucket[implEnum] || 0) + 1; + nextOptSample = tierData[++tierDataIndex]; + } + + samplesInCurrentBucket++; + } + + return output; +} + +exports.createTierGraphDataFromFrameNode = createTierGraphDataFromFrameNode; +exports.OptimizationSite = OptimizationSite; +exports.JITOptimizations = JITOptimizations; +exports.hasSuccessfulOutcome = hasSuccessfulOutcome; +exports.isSuccessfulOutcome = isSuccessfulOutcome; +exports.SUCCESSFUL_OUTCOMES = SUCCESSFUL_OUTCOMES; diff --git a/devtools/client/performance/modules/logic/moz.build b/devtools/client/performance/modules/logic/moz.build new file mode 100644 index 000000000..179cd71b3 --- /dev/null +++ b/devtools/client/performance/modules/logic/moz.build @@ -0,0 +1,12 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + 'frame-utils.js', + 'jit.js', + 'telemetry.js', + 'tree-model.js', + 'waterfall-utils.js', +) diff --git a/devtools/client/performance/modules/logic/telemetry.js b/devtools/client/performance/modules/logic/telemetry.js new file mode 100644 index 000000000..b8e322170 --- /dev/null +++ b/devtools/client/performance/modules/logic/telemetry.js @@ -0,0 +1,122 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const Telemetry = require("devtools/client/shared/telemetry"); +const flags = require("devtools/shared/flags"); +const EVENTS = require("devtools/client/performance/events"); + +const EVENT_MAP_FLAGS = new Map([ + [EVENTS.RECORDING_IMPORTED, "DEVTOOLS_PERFTOOLS_RECORDING_IMPORT_FLAG"], + [EVENTS.RECORDING_EXPORTED, "DEVTOOLS_PERFTOOLS_RECORDING_EXPORT_FLAG"], +]); + +const RECORDING_FEATURES = [ + "withMarkers", "withTicks", "withMemory", "withAllocations" +]; + +const SELECTED_VIEW_HISTOGRAM_NAME = "DEVTOOLS_PERFTOOLS_SELECTED_VIEW_MS"; + +function PerformanceTelemetry(emitter) { + this._emitter = emitter; + this._telemetry = new Telemetry(); + this.onFlagEvent = this.onFlagEvent.bind(this); + this.onRecordingStateChange = this.onRecordingStateChange.bind(this); + this.onViewSelected = this.onViewSelected.bind(this); + + for (let [event] of EVENT_MAP_FLAGS) { + this._emitter.on(event, this.onFlagEvent); + } + + this._emitter.on(EVENTS.RECORDING_STATE_CHANGE, this.onRecordingStateChange); + this._emitter.on(EVENTS.UI_DETAILS_VIEW_SELECTED, this.onViewSelected); + + if (flags.testing) { + this.recordLogs(); + } +} + +PerformanceTelemetry.prototype.destroy = function () { + if (this._previousView) { + this._telemetry.stopTimer(SELECTED_VIEW_HISTOGRAM_NAME, this._previousView); + } + + this._telemetry.destroy(); + for (let [event] of EVENT_MAP_FLAGS) { + this._emitter.off(event, this.onFlagEvent); + } + this._emitter.off(EVENTS.RECORDING_STATE_CHANGE, this.onRecordingStateChange); + this._emitter.off(EVENTS.UI_DETAILS_VIEW_SELECTED, this.onViewSelected); + this._emitter = null; +}; + +PerformanceTelemetry.prototype.onFlagEvent = function (eventName, ...data) { + this._telemetry.log(EVENT_MAP_FLAGS.get(eventName), true); +}; + +PerformanceTelemetry.prototype.onRecordingStateChange = function (_, status, model) { + if (status != "recording-stopped") { + return; + } + + if (model.isConsole()) { + this._telemetry.log("DEVTOOLS_PERFTOOLS_CONSOLE_RECORDING_COUNT", true); + } else { + this._telemetry.log("DEVTOOLS_PERFTOOLS_RECORDING_COUNT", true); + } + + this._telemetry.log("DEVTOOLS_PERFTOOLS_RECORDING_DURATION_MS", model.getDuration()); + + let config = model.getConfiguration(); + for (let k in config) { + if (RECORDING_FEATURES.indexOf(k) !== -1) { + this._telemetry.logKeyed("DEVTOOLS_PERFTOOLS_RECORDING_FEATURES_USED", k, + config[k]); + } + } +}; + +PerformanceTelemetry.prototype.onViewSelected = function (_, viewName) { + if (this._previousView) { + this._telemetry.stopTimer(SELECTED_VIEW_HISTOGRAM_NAME, this._previousView); + } + this._previousView = viewName; + this._telemetry.startTimer(SELECTED_VIEW_HISTOGRAM_NAME); +}; + +/** + * Utility to record histogram calls to this instance. + * Should only be used in testing mode; throws otherwise. + */ +PerformanceTelemetry.prototype.recordLogs = function () { + if (!flags.testing) { + throw new Error("Can only record telemetry logs in tests."); + } + + let originalLog = this._telemetry.log; + let originalLogKeyed = this._telemetry.logKeyed; + this._log = {}; + + this._telemetry.log = (function (histo, data) { + let results = this._log[histo] = this._log[histo] || []; + results.push(data); + originalLog(histo, data); + }).bind(this); + + this._telemetry.logKeyed = (function (histo, key, data) { + let results = this._log[histo] = this._log[histo] || []; + results.push([key, data]); + originalLogKeyed(histo, key, data); + }).bind(this); +}; + +PerformanceTelemetry.prototype.getLogs = function () { + if (!flags.testing) { + throw new Error("Can only get telemetry logs in tests."); + } + + return this._log; +}; + +exports.PerformanceTelemetry = PerformanceTelemetry; diff --git a/devtools/client/performance/modules/logic/tree-model.js b/devtools/client/performance/modules/logic/tree-model.js new file mode 100644 index 000000000..b6376ee8a --- /dev/null +++ b/devtools/client/performance/modules/logic/tree-model.js @@ -0,0 +1,556 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { JITOptimizations } = require("devtools/client/performance/modules/logic/jit"); +const FrameUtils = require("devtools/client/performance/modules/logic/frame-utils"); + +/** + * A call tree for a thread. This is essentially a linkage between all frames + * of all samples into a single tree structure, with additional information + * on each node, like the time spent (in milliseconds) and samples count. + * + * @param object thread + * The raw thread object received from the backend. Contains samples, + * stackTable, frameTable, and stringTable. + * @param object options + * Additional supported options + * - number startTime + * - number endTime + * - boolean contentOnly [optional] + * - boolean invertTree [optional] + * - boolean flattenRecursion [optional] + */ +function ThreadNode(thread, options = {}) { + if (options.endTime == void 0 || options.startTime == void 0) { + throw new Error("ThreadNode requires both `startTime` and `endTime`."); + } + this.samples = 0; + this.sampleTimes = []; + this.youngestFrameSamples = 0; + this.calls = []; + this.duration = options.endTime - options.startTime; + this.nodeType = "Thread"; + this.inverted = options.invertTree; + + // Total bytesize of all allocations if enabled + this.byteSize = 0; + this.youngestFrameByteSize = 0; + + let { samples, stackTable, frameTable, stringTable } = thread; + + // Nothing to do if there are no samples. + if (samples.data.length === 0) { + return; + } + + this._buildInverted(samples, stackTable, frameTable, stringTable, options); + if (!options.invertTree) { + this._uninvert(); + } +} + +ThreadNode.prototype = { + /** + * Build an inverted call tree from profile samples. The format of the + * samples is described in tools/profiler/ProfileEntry.h, under the heading + * "ThreadProfile JSON Format". + * + * The profile data is naturally presented inverted. Inverting the call tree + * is also the default in the Performance tool. + * + * @param object samples + * The raw samples array received from the backend. + * @param object stackTable + * The table of deduplicated stacks from the backend. + * @param object frameTable + * The table of deduplicated frames from the backend. + * @param object stringTable + * The table of deduplicated strings from the backend. + * @param object options + * Additional supported options + * - number startTime + * - number endTime + * - boolean contentOnly [optional] + * - boolean invertTree [optional] + */ + _buildInverted: function buildInverted(samples, stackTable, frameTable, stringTable, + options) { + function getOrAddFrameNode(calls, isLeaf, frameKey, inflatedFrame, isMetaCategory, + leafTable) { + // Insert the inflated frame into the call tree at the current level. + let frameNode; + + // Leaf nodes have fan out much greater than non-leaf nodes, thus the + // use of a hash table. Otherwise, do linear search. + // + // Note that this method is very hot, thus the manual looping over + // Array.prototype.find. + if (isLeaf) { + frameNode = leafTable[frameKey]; + } else { + for (let i = 0; i < calls.length; i++) { + if (calls[i].key === frameKey) { + frameNode = calls[i]; + break; + } + } + } + + if (!frameNode) { + frameNode = new FrameNode(frameKey, inflatedFrame, isMetaCategory); + if (isLeaf) { + leafTable[frameKey] = frameNode; + } + calls.push(frameNode); + } + + return frameNode; + } + + const SAMPLE_STACK_SLOT = samples.schema.stack; + const SAMPLE_TIME_SLOT = samples.schema.time; + const SAMPLE_BYTESIZE_SLOT = samples.schema.size; + + const STACK_PREFIX_SLOT = stackTable.schema.prefix; + const STACK_FRAME_SLOT = stackTable.schema.frame; + + const getOrAddInflatedFrame = FrameUtils.getOrAddInflatedFrame; + + let samplesData = samples.data; + let stacksData = stackTable.data; + + // Caches. + let inflatedFrameCache = FrameUtils.getInflatedFrameCache(frameTable); + let leafTable = Object.create(null); + + let startTime = options.startTime; + let endTime = options.endTime; + let flattenRecursion = options.flattenRecursion; + + // Reused options object passed to InflatedFrame.prototype.getFrameKey. + let mutableFrameKeyOptions = { + contentOnly: options.contentOnly, + isRoot: false, + isLeaf: false, + isMetaCategoryOut: false + }; + + let byteSize = 0; + for (let i = 0; i < samplesData.length; i++) { + let sample = samplesData[i]; + let sampleTime = sample[SAMPLE_TIME_SLOT]; + + if (SAMPLE_BYTESIZE_SLOT !== void 0) { + byteSize = sample[SAMPLE_BYTESIZE_SLOT]; + } + + // A sample's end time is considered to be its time of sampling. Its + // start time is the sampling time of the previous sample. + // + // Thus, we compare sampleTime <= start instead of < to filter out + // samples that end exactly at the start time. + if (!sampleTime || sampleTime <= startTime || sampleTime > endTime) { + continue; + } + + let stackIndex = sample[SAMPLE_STACK_SLOT]; + let calls = this.calls; + let prevCalls = this.calls; + let prevFrameKey; + let isLeaf = mutableFrameKeyOptions.isLeaf = true; + let skipRoot = options.invertTree; + + // Inflate the stack and build the FrameNode call tree directly. + // + // In the profiler data, each frame's stack is referenced by an index + // into stackTable. + // + // Each entry in stackTable is a pair [ prefixIndex, frameIndex ]. The + // prefixIndex is itself an index into stackTable, referencing the + // prefix of the current stack (that is, the younger frames). In other + // words, the stackTable is encoded as a trie of the inverted + // callstack. The frameIndex is an index into frameTable, describing the + // frame at the current depth. + // + // This algorithm inflates each frame in the frame table while walking + // the stack trie as described above. + // + // The frame key is then computed from the inflated frame /and/ the + // current depth in the FrameNode call tree. That is, the frame key is + // not wholly determinable from just the inflated frame. + // + // For content frames, the frame key is just its location. For chrome + // frames, the key may be a metacategory or its location, depending on + // rendering options and its position in the FrameNode call tree. + // + // The frame key is then used to build up the inverted FrameNode call + // tree. + // + // Note that various filtering functions, such as filtering for content + // frames or flattening recursion, are inlined into the stack inflation + // loop. This is important for performance as it avoids intermediate + // structures and multiple passes. + while (stackIndex !== null) { + let stackEntry = stacksData[stackIndex]; + let frameIndex = stackEntry[STACK_FRAME_SLOT]; + + // Fetch the stack prefix (i.e. older frames) index. + stackIndex = stackEntry[STACK_PREFIX_SLOT]; + + // Do not include the (root) node in this sample, as the costs of each frame + // will make it clear to differentiate (root)->B vs (root)->A->B + // when a tree is inverted, a revert of bug 1147604 + if (stackIndex === null && skipRoot) { + break; + } + + // Inflate the frame. + let inflatedFrame = getOrAddInflatedFrame(inflatedFrameCache, frameIndex, + frameTable, stringTable); + + // Compute the frame key. + mutableFrameKeyOptions.isRoot = stackIndex === null; + let frameKey = inflatedFrame.getFrameKey(mutableFrameKeyOptions); + + // An empty frame key means this frame should be skipped. + if (frameKey === "") { + continue; + } + + // If we shouldn't flatten the current frame into the previous one, advance a + // level in the call tree. + let shouldFlatten = flattenRecursion && frameKey === prevFrameKey; + if (!shouldFlatten) { + calls = prevCalls; + } + + let frameNode = getOrAddFrameNode(calls, isLeaf, frameKey, inflatedFrame, + mutableFrameKeyOptions.isMetaCategoryOut, + leafTable); + if (isLeaf) { + frameNode.youngestFrameSamples++; + frameNode._addOptimizations(inflatedFrame.optimizations, + inflatedFrame.implementation, sampleTime, + stringTable); + + if (byteSize) { + frameNode.youngestFrameByteSize += byteSize; + } + } + + // Don't overcount flattened recursive frames. + if (!shouldFlatten) { + frameNode.samples++; + if (byteSize) { + frameNode.byteSize += byteSize; + } + } + + prevFrameKey = frameKey; + prevCalls = frameNode.calls; + isLeaf = mutableFrameKeyOptions.isLeaf = false; + } + + this.samples++; + this.sampleTimes.push(sampleTime); + if (byteSize) { + this.byteSize += byteSize; + } + } + }, + + /** + * Uninverts the call tree after its having been built. + */ + _uninvert: function uninvert() { + function mergeOrAddFrameNode(calls, node, samples, size) { + // Unlike the inverted call tree, we don't use a root table for the top + // level, as in general, there are many fewer entry points than + // leaves. Instead, linear search is used regardless of level. + for (let i = 0; i < calls.length; i++) { + if (calls[i].key === node.key) { + let foundNode = calls[i]; + foundNode._merge(node, samples, size); + return foundNode.calls; + } + } + let copy = node._clone(samples, size); + calls.push(copy); + return copy.calls; + } + + let workstack = [{ node: this, level: 0 }]; + let spine = []; + let entry; + + // The new root. + let rootCalls = []; + + // Walk depth-first and keep the current spine (e.g., callstack). + do { + entry = workstack.pop(); + if (entry) { + spine[entry.level] = entry; + + let node = entry.node; + let calls = node.calls; + let callSamples = 0; + let callByteSize = 0; + + // Continue the depth-first walk. + for (let i = 0; i < calls.length; i++) { + workstack.push({ node: calls[i], level: entry.level + 1 }); + callSamples += calls[i].samples; + callByteSize += calls[i].byteSize; + } + + // The sample delta is used to distinguish stacks. + // + // Suppose we have the following stack samples: + // + // A -> B + // A -> C + // A + // + // The inverted tree is: + // + // A + // / \ + // B C + // + // with A.samples = 3, B.samples = 1, C.samples = 1. + // + // A is distinguished as being its own stack because + // A.samples - (B.samples + C.samples) > 0. + // + // Note that bottoming out is a degenerate where callSamples = 0. + + let samplesDelta = node.samples - callSamples; + let byteSizeDelta = node.byteSize - callByteSize; + if (samplesDelta > 0) { + // Reverse the spine and add them to the uninverted call tree. + let uninvertedCalls = rootCalls; + for (let level = entry.level; level > 0; level--) { + let callee = spine[level]; + uninvertedCalls = mergeOrAddFrameNode(uninvertedCalls, callee.node, + samplesDelta, byteSizeDelta); + } + } + } + } while (entry); + + // Replace the toplevel calls with rootCalls, which now contains the + // uninverted roots. + this.calls = rootCalls; + }, + + /** + * Gets additional details about this node. + * @see FrameNode.prototype.getInfo for more information. + * + * @return object + */ + getInfo: function (options) { + return FrameUtils.getFrameInfo(this, options); + }, + + /** + * Mimicks the interface of FrameNode, and a ThreadNode can never have + * optimization data (at the moment, anyway), so provide a function + * to return null so we don't need to check if a frame node is a thread + * or not everytime we fetch optimization data. + * + * @return {null} + */ + + hasOptimizations: function () { + return null; + } +}; + +/** + * A function call node in a tree. Represents a function call with a unique context, + * resulting in each FrameNode having its own row in the corresponding tree view. + * Take samples: + * A()->B()->C() + * A()->B() + * Q()->B() + * + * In inverted tree, A()->B()->C() would have one frame node, and A()->B() and + * Q()->B() would share a frame node. + * In an uninverted tree, A()->B()->C() and A()->B() would share a frame node, + * with Q()->B() having its own. + * + * In all cases, all the frame nodes originated from the same InflatedFrame. + * + * @param string frameKey + * The key associated with this frame. The key determines identity of + * the node. + * @param string location + * The location of this function call. Note that this isn't sanitized, + * so it may very well (not?) include the function name, url, etc. + * @param number line + * The line number inside the source containing this function call. + * @param number category + * The category type of this function call ("js", "graphics" etc.). + * @param number allocations + * The number of memory allocations performed in this frame. + * @param number isContent + * Whether this frame is content. + * @param boolean isMetaCategory + * Whether or not this is a platform node that should appear as a + * generalized meta category or not. + */ +function FrameNode(frameKey, { location, line, category, isContent }, isMetaCategory) { + this.key = frameKey; + this.location = location; + this.line = line; + this.youngestFrameSamples = 0; + this.samples = 0; + this.calls = []; + this.isContent = !!isContent; + this._optimizations = null; + this._tierData = []; + this._stringTable = null; + this.isMetaCategory = !!isMetaCategory; + this.category = category; + this.nodeType = "Frame"; + this.byteSize = 0; + this.youngestFrameByteSize = 0; +} + +FrameNode.prototype = { + /** + * Take optimization data observed for this frame. + * + * @param object optimizationSite + * Any JIT optimization information attached to the current + * sample. Lazily inflated via stringTable. + * @param number implementation + * JIT implementation used for this observed frame (baseline, ion); + * can be null indicating "interpreter" + * @param number time + * The time this optimization occurred. + * @param object stringTable + * The string table used to inflate the optimizationSite. + */ + _addOptimizations: function (site, implementation, time, stringTable) { + // Simply accumulate optimization sites for now. Processing is done lazily + // by JITOptimizations, if optimization information is actually displayed. + if (site) { + let opts = this._optimizations; + if (opts === null) { + opts = this._optimizations = []; + } + opts.push(site); + } + + if (!this._stringTable) { + this._stringTable = stringTable; + } + + // Record type of implementation used and the sample time + this._tierData.push({ implementation, time }); + }, + + _clone: function (samples, size) { + let newNode = new FrameNode(this.key, this, this.isMetaCategory); + newNode._merge(this, samples, size); + return newNode; + }, + + _merge: function (otherNode, samples, size) { + if (this === otherNode) { + return; + } + + this.samples += samples; + this.byteSize += size; + if (otherNode.youngestFrameSamples > 0) { + this.youngestFrameSamples += samples; + } + + if (otherNode.youngestFrameByteSize > 0) { + this.youngestFrameByteSize += otherNode.youngestFrameByteSize; + } + + if (this._stringTable === null) { + this._stringTable = otherNode._stringTable; + } + + if (otherNode._optimizations) { + if (!this._optimizations) { + this._optimizations = []; + } + let opts = this._optimizations; + let otherOpts = otherNode._optimizations; + for (let i = 0; i < otherOpts.length; i++) { + opts.push(otherOpts[i]); + } + } + + if (otherNode._tierData.length) { + let tierData = this._tierData; + let otherTierData = otherNode._tierData; + for (let i = 0; i < otherTierData.length; i++) { + tierData.push(otherTierData[i]); + } + tierData.sort((a, b) => a.time - b.time); + } + }, + + /** + * Returns the parsed location and additional data describing + * this frame. Uses cached data if possible. Takes the following + * options: + * + * @param {ThreadNode} options.root + * The root thread node to calculate relative costs. + * Generates [self|total] [duration|percentage] values. + * @param {boolean} options.allocations + * Generates `totalAllocations` and `selfAllocations`. + * + * @return object + * The computed { name, file, url, line } properties for this + * function call, as well as additional params if options specified. + */ + getInfo: function (options) { + return FrameUtils.getFrameInfo(this, options); + }, + + /** + * Returns whether or not the frame node has an JITOptimizations model. + * + * @return {Boolean} + */ + hasOptimizations: function () { + return !this.isMetaCategory && !!this._optimizations; + }, + + /** + * Returns the underlying JITOptimizations model representing + * the optimization attempts occuring in this frame. + * + * @return {JITOptimizations|null} + */ + getOptimizations: function () { + if (!this._optimizations) { + return null; + } + return new JITOptimizations(this._optimizations, this._stringTable); + }, + + /** + * Returns the tiers used overtime. + * + * @return {Array} + */ + getTierData: function () { + return this._tierData; + } +}; + +exports.ThreadNode = ThreadNode; +exports.FrameNode = FrameNode; diff --git a/devtools/client/performance/modules/logic/waterfall-utils.js b/devtools/client/performance/modules/logic/waterfall-utils.js new file mode 100644 index 000000000..04c05a544 --- /dev/null +++ b/devtools/client/performance/modules/logic/waterfall-utils.js @@ -0,0 +1,167 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +/** + * Utility functions for collapsing markers into a waterfall. + */ + +const { extend } = require("sdk/util/object"); +const { MarkerBlueprintUtils } = require("devtools/client/performance/modules/marker-blueprint-utils"); + +/** + * Creates a parent marker, which functions like a regular marker, + * but is able to hold additional child markers. + * + * The marker is seeded with values from `marker`. + * @param object marker + * @return object + */ +function createParentNode(marker) { + return extend(marker, { submarkers: [] }); +} + +/** + * Collapses markers into a tree-like structure. + * @param object rootNode + * @param array markersList + * @param array filter + */ +function collapseMarkersIntoNode({ rootNode, markersList, filter }) { + let { + getCurrentParentNode, + pushNode, + popParentNode + } = createParentNodeFactory(rootNode); + + for (let i = 0, len = markersList.length; i < len; i++) { + let curr = markersList[i]; + + // If this marker type should not be displayed, just skip + if (!MarkerBlueprintUtils.shouldDisplayMarker(curr, filter)) { + continue; + } + + let parentNode = getCurrentParentNode(); + let blueprint = MarkerBlueprintUtils.getBlueprintFor(curr); + + let nestable = "nestable" in blueprint ? blueprint.nestable : true; + let collapsible = "collapsible" in blueprint ? blueprint.collapsible : true; + + let finalized = false; + + // Extend the marker with extra properties needed in the marker tree + let extendedProps = { index: i }; + if (collapsible) { + extendedProps.submarkers = []; + } + curr = extend(curr, extendedProps); + + // If not nestible, just push it inside the root node. Additionally, + // markers originating outside the main thread are considered to be + // "never collapsible", to avoid confusion. + // A beter solution would be to collapse every marker with its siblings + // from the same thread, but that would require a thread id attached + // to all markers, which is potentially expensive and rather useless at + // the moment, since we don't really have that many OTMT markers. + if (!nestable || curr.isOffMainThread) { + pushNode(rootNode, curr); + continue; + } + + // First off, if any parent nodes exist, finish them off + // recursively upwards if this marker is outside their ranges and nestable. + while (!finalized && parentNode) { + // If this marker is eclipsed by the current parent marker, + // make it a child of the current parent and stop going upwards. + // If the markers aren't from the same process, attach them to the root + // node as well. Every process has its own main thread. + if (nestable && + curr.start >= parentNode.start && + curr.end <= parentNode.end && + curr.processType == parentNode.processType) { + pushNode(parentNode, curr); + finalized = true; + break; + } + + // If this marker is still nestable, but outside of the range + // of the current parent, iterate upwards on the next parent + // and finalize the current parent. + if (nestable) { + popParentNode(); + parentNode = getCurrentParentNode(); + continue; + } + } + + if (!finalized) { + pushNode(rootNode, curr); + } + } +} + +/** + * Takes a root marker node and creates a hash of functions used + * to manage the creation and nesting of additional parent markers. + * + * @param {object} root + * @return {object} + */ +function createParentNodeFactory(root) { + let parentMarkers = []; + let factory = { + /** + * Pops the most recent parent node off the stack, finalizing it. + * Sets the `end` time based on the most recent child if not defined. + */ + popParentNode: () => { + if (parentMarkers.length === 0) { + throw new Error("Cannot pop parent markers when none exist."); + } + + let lastParent = parentMarkers.pop(); + + // If this finished parent marker doesn't have an end time, + // so probably a synthesized marker, use the last marker's end time. + if (lastParent.end == void 0) { + lastParent.end = lastParent.submarkers[lastParent.submarkers.length - 1].end; + } + + // If no children were ever pushed into this parent node, + // remove its submarkers so it behaves like a non collapsible + // node. + if (!lastParent.submarkers.length) { + delete lastParent.submarkers; + } + + return lastParent; + }, + + /** + * Returns the most recent parent node. + */ + getCurrentParentNode: () => parentMarkers.length + ? parentMarkers[parentMarkers.length - 1] + : null, + + /** + * Push this marker into the most recent parent node. + */ + pushNode: (parent, marker) => { + parent.submarkers.push(marker); + + // If pushing a parent marker, track it as the top of + // the parent stack. + if (marker.submarkers) { + parentMarkers.push(marker); + } + } + }; + + return factory; +} + +exports.createParentNode = createParentNode; +exports.collapseMarkersIntoNode = collapseMarkersIntoNode; diff --git a/devtools/client/performance/modules/marker-blueprint-utils.js b/devtools/client/performance/modules/marker-blueprint-utils.js new file mode 100644 index 000000000..e60ea0eaa --- /dev/null +++ b/devtools/client/performance/modules/marker-blueprint-utils.js @@ -0,0 +1,104 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { TIMELINE_BLUEPRINT } = require("devtools/client/performance/modules/markers"); + +/** + * This file contains utilities for parsing out the markers blueprint + * to generate strings to be displayed in the UI. + */ + +exports.MarkerBlueprintUtils = { + /** + * Takes a marker and a list of marker names that should be hidden, and + * determines if this marker should be filtered or not. + * + * @param object marker + * @return boolean + */ + shouldDisplayMarker: function (marker, hiddenMarkerNames) { + if (!hiddenMarkerNames || hiddenMarkerNames.length == 0) { + return true; + } + + // If this marker isn't yet defined in the blueprint, simply check if the + // entire category of "UNKNOWN" markers are supposed to be visible or not. + let isUnknown = !(marker.name in TIMELINE_BLUEPRINT); + if (isUnknown) { + return hiddenMarkerNames.indexOf("UNKNOWN") == -1; + } + + return hiddenMarkerNames.indexOf(marker.name) == -1; + }, + + /** + * Takes a marker and returns the blueprint definition for that marker type, + * falling back to the UNKNOWN blueprint definition if undefined. + * + * @param object marker + * @return object + */ + getBlueprintFor: function (marker) { + return TIMELINE_BLUEPRINT[marker.name] || TIMELINE_BLUEPRINT.UNKNOWN; + }, + + /** + * Returns the label to display for a marker, based off the blueprints. + * + * @param object marker + * @return string + */ + getMarkerLabel: function (marker) { + let blueprint = this.getBlueprintFor(marker); + let dynamic = typeof blueprint.label === "function"; + let label = dynamic ? blueprint.label(marker) : blueprint.label; + return label; + }, + + /** + * Returns the generic label to display for a marker name. + * (e.g. "Function Call" for JS markers, rather than "setTimeout", etc.) + * + * @param string type + * @return string + */ + getMarkerGenericName: function (markerName) { + let blueprint = this.getBlueprintFor({ name: markerName }); + let dynamic = typeof blueprint.label === "function"; + let generic = dynamic ? blueprint.label() : blueprint.label; + + // If no class name found, attempt to throw a descriptive error as to + // how the marker implementor can fix this. + if (!generic) { + let message = `Could not find marker generic name for "${markerName}".`; + if (typeof blueprint.label === "function") { + message += ` The following function must return a generic name string when no` + + ` marker passed: ${blueprint.label}`; + } else { + message += ` ${markerName}.label must be defined in the marker blueprint.`; + } + throw new Error(message); + } + + return generic; + }, + + /** + * Returns an array of objects with key/value pairs of what should be rendered + * in the marker details view. + * + * @param object marker + * @return array + */ + getMarkerFields: function (marker) { + let blueprint = this.getBlueprintFor(marker); + let dynamic = typeof blueprint.fields === "function"; + let fields = dynamic ? blueprint.fields(marker) : blueprint.fields; + + return Object.entries(fields || {}) + .filter(([_, value]) => dynamic ? true : value in marker) + .map(([label, value]) => ({ label, value: dynamic ? value : marker[value] })); + }, +}; diff --git a/devtools/client/performance/modules/marker-dom-utils.js b/devtools/client/performance/modules/marker-dom-utils.js new file mode 100644 index 000000000..006b13171 --- /dev/null +++ b/devtools/client/performance/modules/marker-dom-utils.js @@ -0,0 +1,257 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +/** + * This file contains utilities for creating DOM nodes for markers + * to be displayed in the UI. + */ + +const { L10N, PREFS } = require("devtools/client/performance/modules/global"); +const { MarkerBlueprintUtils } = require("devtools/client/performance/modules/marker-blueprint-utils"); +const { getSourceNames } = require("devtools/client/shared/source-utils"); + +/** + * Utilites for creating elements for markers. + */ +exports.MarkerDOMUtils = { + /** + * Builds all the fields possible for the given marker. Returns an + * array of elements to be appended to a parent element. + * + * @param document doc + * @param object marker + * @return array + */ + buildFields: function (doc, marker) { + let fields = MarkerBlueprintUtils.getMarkerFields(marker); + return fields.map(({ label, value }) => this.buildNameValueLabel(doc, label, value)); + }, + + /** + * Builds the label representing the marker's type. + * + * @param document doc + * @param object marker + * @return nsIDOMNode + */ + buildTitle: function (doc, marker) { + let blueprint = MarkerBlueprintUtils.getBlueprintFor(marker); + + let hbox = doc.createElement("hbox"); + hbox.setAttribute("align", "center"); + + let bullet = doc.createElement("hbox"); + bullet.className = `marker-details-bullet marker-color-${blueprint.colorName}`; + + let title = MarkerBlueprintUtils.getMarkerLabel(marker); + let label = doc.createElement("label"); + label.className = "marker-details-type"; + label.setAttribute("value", title); + + hbox.appendChild(bullet); + hbox.appendChild(label); + + return hbox; + }, + + /** + * Builds the label representing the marker's duration. + * + * @param document doc + * @param object marker + * @return nsIDOMNode + */ + buildDuration: function (doc, marker) { + let label = L10N.getStr("marker.field.duration"); + let start = L10N.getFormatStrWithNumbers("timeline.tick", marker.start); + let end = L10N.getFormatStrWithNumbers("timeline.tick", marker.end); + let duration = L10N.getFormatStrWithNumbers("timeline.tick", + marker.end - marker.start); + + let el = this.buildNameValueLabel(doc, label, duration); + el.classList.add("marker-details-duration"); + el.setAttribute("tooltiptext", `${start} → ${end}`); + + return el; + }, + + /** + * Builds labels for name:value pairs. + * E.g. "Start: 100ms", "Duration: 200ms", ... + * + * @param document doc + * @param string field + * @param string value + * @return nsIDOMNode + */ + buildNameValueLabel: function (doc, field, value) { + let hbox = doc.createElement("hbox"); + hbox.className = "marker-details-labelcontainer"; + + let nameLabel = doc.createElement("label"); + nameLabel.className = "plain marker-details-name-label"; + nameLabel.setAttribute("value", field); + hbox.appendChild(nameLabel); + + let valueLabel = doc.createElement("label"); + valueLabel.className = "plain marker-details-value-label"; + valueLabel.setAttribute("value", value); + hbox.appendChild(valueLabel); + + return hbox; + }, + + /** + * Builds a stack trace in an element. + * + * @param document doc + * @param object params + * An options object with the following members: + * - string type: string identifier for type of stack ("stack", "startStack" + or "endStack" + * - number frameIndex: the index of the topmost stack frame + * - array frames: array of stack frames + */ + buildStackTrace: function (doc, { type, frameIndex, frames }) { + let container = doc.createElement("vbox"); + container.className = "marker-details-stack"; + container.setAttribute("type", type); + + let nameLabel = doc.createElement("label"); + nameLabel.className = "plain marker-details-name-label"; + nameLabel.setAttribute("value", L10N.getStr(`marker.field.${type}`)); + container.appendChild(nameLabel); + + // Workaround for profiles that have looping stack traces. See + // bug 1246555. + let wasAsyncParent = false; + let seen = new Set(); + + while (frameIndex > 0) { + if (seen.has(frameIndex)) { + break; + } + seen.add(frameIndex); + + let frame = frames[frameIndex]; + let url = frame.source; + let displayName = frame.functionDisplayName; + let line = frame.line; + + // If the previous frame had an async parent, then the async + // cause is in this frame and should be displayed. + if (wasAsyncParent) { + let asyncStr = L10N.getFormatStr("marker.field.asyncStack", frame.asyncCause); + let asyncBox = doc.createElement("hbox"); + let asyncLabel = doc.createElement("label"); + asyncLabel.className = "devtools-monospace"; + asyncLabel.setAttribute("value", asyncStr); + asyncBox.appendChild(asyncLabel); + container.appendChild(asyncBox); + wasAsyncParent = false; + } + + let hbox = doc.createElement("hbox"); + + if (displayName) { + let functionLabel = doc.createElement("label"); + functionLabel.className = "devtools-monospace"; + functionLabel.setAttribute("value", displayName); + hbox.appendChild(functionLabel); + } + + if (url) { + let linkNode = doc.createElement("a"); + linkNode.className = "waterfall-marker-location devtools-source-link"; + linkNode.href = url; + linkNode.draggable = false; + linkNode.setAttribute("title", url); + + let urlLabel = doc.createElement("label"); + urlLabel.className = "filename"; + urlLabel.setAttribute("value", getSourceNames(url).short); + linkNode.appendChild(urlLabel); + + let lineLabel = doc.createElement("label"); + lineLabel.className = "line-number"; + lineLabel.setAttribute("value", `:${line}`); + linkNode.appendChild(lineLabel); + + hbox.appendChild(linkNode); + + // Clicking here will bubble up to the parent, + // which handles the view source. + linkNode.setAttribute("data-action", JSON.stringify({ + url: url, + line: line, + action: "view-source" + })); + } + + if (!displayName && !url) { + let unknownLabel = doc.createElement("label"); + unknownLabel.setAttribute("value", L10N.getStr("marker.value.unknownFrame")); + hbox.appendChild(unknownLabel); + } + + container.appendChild(hbox); + + if (frame.asyncParent) { + frameIndex = frame.asyncParent; + wasAsyncParent = true; + } else { + frameIndex = frame.parent; + } + } + + return container; + }, + + /** + * Builds any custom fields specific to the marker. + * + * @param document doc + * @param object marker + * @param object options + * @return array + */ + buildCustom: function (doc, marker, options) { + let elements = []; + + if (options.allocations && shouldShowAllocationsTrigger(marker)) { + let hbox = doc.createElement("hbox"); + hbox.className = "marker-details-customcontainer"; + + let label = doc.createElement("label"); + label.className = "custom-button devtools-button"; + label.setAttribute("value", "Show allocation triggers"); + label.setAttribute("type", "show-allocations"); + label.setAttribute("data-action", JSON.stringify({ + endTime: marker.start, + action: "show-allocations" + })); + + hbox.appendChild(label); + elements.push(hbox); + } + + return elements; + }, +}; + +/** + * Takes a marker and determines if this marker should display + * the allocations trigger button. + * + * @param object marker + * @return boolean + */ +function shouldShowAllocationsTrigger(marker) { + if (marker.name == "GarbageCollection") { + let showTriggers = PREFS["show-triggers-for-gc-types"]; + return showTriggers.split(" ").indexOf(marker.causeName) !== -1; + } + return false; +} diff --git a/devtools/client/performance/modules/marker-formatters.js b/devtools/client/performance/modules/marker-formatters.js new file mode 100644 index 000000000..0d74913cc --- /dev/null +++ b/devtools/client/performance/modules/marker-formatters.js @@ -0,0 +1,199 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +/** + * This file contains utilities for creating elements for markers to be displayed, + * and parsing out the blueprint to generate correct values for markers. + */ +const { Ci } = require("chrome"); +const { L10N, PREFS } = require("devtools/client/performance/modules/global"); + +// String used to fill in platform data when it should be hidden. +const GECKO_SYMBOL = "(Gecko)"; + +/** + * Mapping of JS marker causes to a friendlier form. Only + * markers that are considered "from content" should be labeled here. + */ +const JS_MARKER_MAP = { + " + + + 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 @@ + + + + + + + Performance tool + innerHTML test page + + + + + + + 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 @@ + + + + + + + Performance tool marker generation + + + + + + + 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 @@ + + + + + + + Performance test page + + + + + + + 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 @@ + + + + + + + Performance test page + + + + + + + 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] diff --git a/devtools/client/performance/views/details-abstract-subview.js b/devtools/client/performance/views/details-abstract-subview.js new file mode 100644 index 000000000..86ea45366 --- /dev/null +++ b/devtools/client/performance/views/details-abstract-subview.js @@ -0,0 +1,194 @@ +/* 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/. */ +/* import-globals-from ../performance-controller.js */ +/* import-globals-from ../performance-view.js */ +/* exported DetailsSubview */ +"use strict"; + +/** + * A base class from which all detail views inherit. + */ +var DetailsSubview = { + /** + * Sets up the view with event binding. + */ + initialize: function () { + this._onRecordingStoppedOrSelected = this._onRecordingStoppedOrSelected.bind(this); + this._onOverviewRangeChange = this._onOverviewRangeChange.bind(this); + this._onDetailsViewSelected = this._onDetailsViewSelected.bind(this); + this._onPrefChanged = this._onPrefChanged.bind(this); + + PerformanceController.on(EVENTS.RECORDING_STATE_CHANGE, + this._onRecordingStoppedOrSelected); + PerformanceController.on(EVENTS.RECORDING_SELECTED, + this._onRecordingStoppedOrSelected); + PerformanceController.on(EVENTS.PREF_CHANGED, this._onPrefChanged); + OverviewView.on(EVENTS.UI_OVERVIEW_RANGE_SELECTED, this._onOverviewRangeChange); + DetailsView.on(EVENTS.UI_DETAILS_VIEW_SELECTED, this._onDetailsViewSelected); + + let self = this; + let originalRenderFn = this.render; + let afterRenderFn = () => { + this._wasRendered = true; + }; + + this.render = Task.async(function* (...args) { + let maybeRetval = yield originalRenderFn.apply(self, args); + afterRenderFn(); + return maybeRetval; + }); + }, + + /** + * Unbinds events. + */ + destroy: function () { + clearNamedTimeout("range-change-debounce"); + + PerformanceController.off(EVENTS.RECORDING_STATE_CHANGE, + this._onRecordingStoppedOrSelected); + PerformanceController.off(EVENTS.RECORDING_SELECTED, + this._onRecordingStoppedOrSelected); + PerformanceController.off(EVENTS.PREF_CHANGED, this._onPrefChanged); + OverviewView.off(EVENTS.UI_OVERVIEW_RANGE_SELECTED, this._onOverviewRangeChange); + DetailsView.off(EVENTS.UI_DETAILS_VIEW_SELECTED, this._onDetailsViewSelected); + }, + + /** + * Returns true if this view was rendered at least once. + */ + get wasRenderedAtLeastOnce() { + return !!this._wasRendered; + }, + + /** + * Amount of time (in milliseconds) to wait until this view gets updated, + * when the range is changed in the overview. + */ + rangeChangeDebounceTime: 0, + + /** + * When the overview range changes, all details views will require a + * rerendering at a later point, determined by `shouldUpdateWhenShown` and + * `canUpdateWhileHidden` and whether or not its the current view. + * Set `requiresUpdateOnRangeChange` to false to not invalidate the view + * when the range changes. + */ + requiresUpdateOnRangeChange: true, + + /** + * Flag specifying if this view should be updated when selected. This will + * be set to true, for example, when the range changes in the overview and + * this view is not currently visible. + */ + shouldUpdateWhenShown: false, + + /** + * Flag specifying if this view may get updated even when it's not selected. + * Should only be used in tests. + */ + canUpdateWhileHidden: false, + + /** + * An array of preferences under `devtools.performance.ui.` that the view should + * rerender and callback `this._onRerenderPrefChanged` upon change. + */ + rerenderPrefs: [], + + /** + * An array of preferences under `devtools.performance.` that the view should + * observe and callback `this._onObservedPrefChange` upon change. + */ + observedPrefs: [], + + /** + * Flag specifying if this view should update while the overview selection + * area is actively being dragged by the mouse. + */ + shouldUpdateWhileMouseIsActive: false, + + /** + * Called when recording stops or is selected. + */ + _onRecordingStoppedOrSelected: function (_, state, recording) { + if (typeof state !== "string") { + recording = state; + } + if (arguments.length === 3 && state !== "recording-stopped") { + return; + } + + if (!recording || !recording.isCompleted()) { + return; + } + if (DetailsView.isViewSelected(this) || this.canUpdateWhileHidden) { + this.render(OverviewView.getTimeInterval()); + } else { + this.shouldUpdateWhenShown = true; + } + }, + + /** + * Fired when a range is selected or cleared in the OverviewView. + */ + _onOverviewRangeChange: function (_, interval) { + if (!this.requiresUpdateOnRangeChange) { + return; + } + if (DetailsView.isViewSelected(this)) { + let debounced = () => { + if (!this.shouldUpdateWhileMouseIsActive && OverviewView.isMouseActive) { + // Don't render yet, while the selection is still being dragged. + setNamedTimeout("range-change-debounce", this.rangeChangeDebounceTime, + debounced); + } else { + this.render(interval); + } + }; + setNamedTimeout("range-change-debounce", this.rangeChangeDebounceTime, debounced); + } else { + this.shouldUpdateWhenShown = true; + } + }, + + /** + * Fired when a view is selected in the DetailsView. + */ + _onDetailsViewSelected: function () { + if (DetailsView.isViewSelected(this) && this.shouldUpdateWhenShown) { + this.render(OverviewView.getTimeInterval()); + this.shouldUpdateWhenShown = false; + } + }, + + /** + * Fired when a preference in `devtools.performance.ui.` is changed. + */ + _onPrefChanged: function (_, prefName) { + if (~this.observedPrefs.indexOf(prefName) && this._onObservedPrefChange) { + this._onObservedPrefChange(_, prefName); + } + + // All detail views require a recording to be complete, so do not + // attempt to render if recording is in progress or does not exist. + let recording = PerformanceController.getCurrentRecording(); + if (!recording || !recording.isCompleted()) { + return; + } + + if (!~this.rerenderPrefs.indexOf(prefName)) { + return; + } + + if (this._onRerenderPrefChanged) { + this._onRerenderPrefChanged(_, prefName); + } + + if (DetailsView.isViewSelected(this) || this.canUpdateWhileHidden) { + this.render(OverviewView.getTimeInterval()); + } else { + this.shouldUpdateWhenShown = true; + } + } +}; diff --git a/devtools/client/performance/views/details-js-call-tree.js b/devtools/client/performance/views/details-js-call-tree.js new file mode 100644 index 000000000..6c4e808af --- /dev/null +++ b/devtools/client/performance/views/details-js-call-tree.js @@ -0,0 +1,193 @@ +/* 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/. */ +/* import-globals-from ../performance-controller.js */ +/* import-globals-from ../performance-view.js */ +/* globals DetailsSubview */ +"use strict"; + +/** + * CallTree view containing profiler call tree, controlled by DetailsView. + */ +var JsCallTreeView = Heritage.extend(DetailsSubview, { + + rerenderPrefs: [ + "invert-call-tree", + "show-platform-data", + "flatten-tree-recursion", + "show-jit-optimizations", + ], + + // Units are in milliseconds. + rangeChangeDebounceTime: 75, + + /** + * Sets up the view with event binding. + */ + initialize: function () { + DetailsSubview.initialize.call(this); + + this._onLink = this._onLink.bind(this); + this._onFocus = this._onFocus.bind(this); + + this.container = $("#js-calltree-view .call-tree-cells-container"); + + this.optimizationsElement = $("#jit-optimizations-view"); + }, + + /** + * Unbinds events. + */ + destroy: function () { + ReactDOM.unmountComponentAtNode(this.optimizationsElement); + this.optimizationsElement = null; + this.container = null; + this.threadNode = null; + DetailsSubview.destroy.call(this); + }, + + /** + * Method for handling all the set up for rendering a new call tree. + * + * @param object interval [optional] + * The { startTime, endTime }, in milliseconds. + */ + render: function (interval = {}) { + let recording = PerformanceController.getCurrentRecording(); + let profile = recording.getProfile(); + let showOptimizations = PerformanceController.getOption("show-jit-optimizations"); + + let options = { + contentOnly: !PerformanceController.getOption("show-platform-data"), + invertTree: PerformanceController.getOption("invert-call-tree"), + flattenRecursion: PerformanceController.getOption("flatten-tree-recursion"), + showOptimizationHint: showOptimizations + }; + let threadNode = this.threadNode = this._prepareCallTree(profile, interval, options); + this._populateCallTree(threadNode, options); + + // For better or worse, re-rendering loses frame selection, + // so we should always hide opts on rerender + this.hideOptimizations(); + + this.emit(EVENTS.UI_JS_CALL_TREE_RENDERED); + }, + + showOptimizations: function () { + this.optimizationsElement.classList.remove("hidden"); + }, + + hideOptimizations: function () { + this.optimizationsElement.classList.add("hidden"); + }, + + _onFocus: function (_, treeItem) { + let showOptimizations = PerformanceController.getOption("show-jit-optimizations"); + let frameNode = treeItem.frame; + let optimizationSites = frameNode && frameNode.hasOptimizations() + ? frameNode.getOptimizations().optimizationSites + : []; + + if (!showOptimizations || !frameNode || optimizationSites.length === 0) { + this.hideOptimizations(); + this.emit("focus", treeItem); + return; + } + + this.showOptimizations(); + + let frameData = frameNode.getInfo(); + let optimizations = JITOptimizationsView({ + frameData, + optimizationSites, + onViewSourceInDebugger: (url, line) => { + gToolbox.viewSourceInDebugger(url, line).then(success => { + if (success) { + this.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER); + } else { + this.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER); + } + }); + } + }); + + ReactDOM.render(optimizations, this.optimizationsElement); + + this.emit("focus", treeItem); + }, + + /** + * Fired on the "link" event for the call tree in this container. + */ + _onLink: function (_, treeItem) { + let { url, line } = treeItem.frame.getInfo(); + gToolbox.viewSourceInDebugger(url, line).then(success => { + if (success) { + this.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER); + } else { + this.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER); + } + }); + }, + + /** + * Called when the recording is stopped and prepares data to + * populate the call tree. + */ + _prepareCallTree: function (profile, { startTime, endTime }, options) { + let thread = profile.threads[0]; + let { contentOnly, invertTree, flattenRecursion } = options; + let threadNode = new ThreadNode(thread, { startTime, endTime, contentOnly, invertTree, + flattenRecursion }); + + // Real profiles from nsProfiler (i.e. not synthesized from allocation + // logs) always have a (root) node. Go down one level in the uninverted + // view to avoid displaying both the synthesized root node and the (root) + // node from the profiler. + if (!invertTree) { + threadNode.calls = threadNode.calls[0].calls; + } + + return threadNode; + }, + + /** + * Renders the call tree. + */ + _populateCallTree: function (frameNode, options = {}) { + // If we have an empty profile (no samples), then don't invert the tree, as + // it would hide the root node and a completely blank call tree space can be + // mis-interpreted as an error. + let inverted = options.invertTree && frameNode.samples > 0; + + let root = new CallView({ + frame: frameNode, + inverted: inverted, + // The synthesized root node is hidden in inverted call trees. + hidden: inverted, + // Call trees should only auto-expand when not inverted. Passing undefined + // will default to the CALL_TREE_AUTO_EXPAND depth. + autoExpandDepth: inverted ? 0 : undefined, + showOptimizationHint: options.showOptimizationHint + }); + + // Bind events. + root.on("link", this._onLink); + root.on("focus", this._onFocus); + + // Clear out other call trees. + this.container.innerHTML = ""; + root.attachTo(this.container); + + // When platform data isn't shown, hide the cateogry labels, since they're + // only available for C++ frames. Pass *false* to make them invisible. + root.toggleCategories(!options.contentOnly); + + // Return the CallView for tests + return root; + }, + + toString: () => "[object JsCallTreeView]" +}); + +EventEmitter.decorate(JsCallTreeView); diff --git a/devtools/client/performance/views/details-js-flamegraph.js b/devtools/client/performance/views/details-js-flamegraph.js new file mode 100644 index 000000000..0aca21252 --- /dev/null +++ b/devtools/client/performance/views/details-js-flamegraph.js @@ -0,0 +1,125 @@ +/* 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/. */ +/* import-globals-from ../performance-controller.js */ +/* import-globals-from ../performance-view.js */ +/* globals DetailsSubview */ +"use strict"; + +/** + * FlameGraph view containing a pyramid-like visualization of a profile, + * controlled by DetailsView. + */ +var JsFlameGraphView = Heritage.extend(DetailsSubview, { + + shouldUpdateWhileMouseIsActive: true, + + rerenderPrefs: [ + "invert-flame-graph", + "flatten-tree-recursion", + "show-platform-data", + "show-idle-blocks" + ], + + /** + * Sets up the view with event binding. + */ + initialize: Task.async(function* () { + DetailsSubview.initialize.call(this); + + this.graph = new FlameGraph($("#js-flamegraph-view")); + this.graph.timelineTickUnits = L10N.getStr("graphs.ms"); + this.graph.setTheme(PerformanceController.getTheme()); + yield this.graph.ready(); + + this._onRangeChangeInGraph = this._onRangeChangeInGraph.bind(this); + this._onThemeChanged = this._onThemeChanged.bind(this); + + PerformanceController.on(EVENTS.THEME_CHANGED, this._onThemeChanged); + this.graph.on("selecting", this._onRangeChangeInGraph); + }), + + /** + * Unbinds events. + */ + destroy: Task.async(function* () { + DetailsSubview.destroy.call(this); + + PerformanceController.off(EVENTS.THEME_CHANGED, this._onThemeChanged); + this.graph.off("selecting", this._onRangeChangeInGraph); + + yield this.graph.destroy(); + }), + + /** + * Method for handling all the set up for rendering a new flamegraph. + * + * @param object interval [optional] + * The { startTime, endTime }, in milliseconds. + */ + render: function (interval = {}) { + let recording = PerformanceController.getCurrentRecording(); + let duration = recording.getDuration(); + let profile = recording.getProfile(); + let thread = profile.threads[0]; + + let data = FlameGraphUtils.createFlameGraphDataFromThread(thread, { + invertTree: PerformanceController.getOption("invert-flame-graph"), + flattenRecursion: PerformanceController.getOption("flatten-tree-recursion"), + contentOnly: !PerformanceController.getOption("show-platform-data"), + showIdleBlocks: PerformanceController.getOption("show-idle-blocks") + && L10N.getStr("table.idle") + }); + + this.graph.setData({ data, + bounds: { + startTime: 0, + endTime: duration + }, + visible: { + startTime: interval.startTime || 0, + endTime: interval.endTime || duration + } + }); + + this.graph.focus(); + + this.emit(EVENTS.UI_JS_FLAMEGRAPH_RENDERED); + }, + + /** + * Fired when a range is selected or cleared in the FlameGraph. + */ + _onRangeChangeInGraph: function () { + let interval = this.graph.getViewRange(); + + // Squelch rerendering this view when we update the range here + // to avoid recursion, as our FlameGraph handles rerendering itself + // when originating from within the graph. + this.requiresUpdateOnRangeChange = false; + OverviewView.setTimeInterval(interval); + this.requiresUpdateOnRangeChange = true; + }, + + /** + * Called whenever a pref is changed and this view needs to be rerendered. + */ + _onRerenderPrefChanged: function () { + let recording = PerformanceController.getCurrentRecording(); + let profile = recording.getProfile(); + let thread = profile.threads[0]; + FlameGraphUtils.removeFromCache(thread); + }, + + /** + * Called when `devtools.theme` changes. + */ + _onThemeChanged: function (_, theme) { + this.graph.setTheme(theme); + this.graph.refresh({ force: true }); + }, + + toString: () => "[object JsFlameGraphView]" +}); + +EventEmitter.decorate(JsFlameGraphView); diff --git a/devtools/client/performance/views/details-memory-call-tree.js b/devtools/client/performance/views/details-memory-call-tree.js new file mode 100644 index 000000000..883d92e63 --- /dev/null +++ b/devtools/client/performance/views/details-memory-call-tree.js @@ -0,0 +1,130 @@ +/* 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/. */ +/* import-globals-from ../performance-controller.js */ +/* import-globals-from ../performance-view.js */ +/* globals DetailsSubview */ +"use strict"; + +/** + * CallTree view containing memory allocation sites, controlled by DetailsView. + */ +var MemoryCallTreeView = Heritage.extend(DetailsSubview, { + + rerenderPrefs: [ + "invert-call-tree" + ], + + // Units are in milliseconds. + rangeChangeDebounceTime: 100, + + /** + * Sets up the view with event binding. + */ + initialize: function () { + DetailsSubview.initialize.call(this); + + this._onLink = this._onLink.bind(this); + + this.container = $("#memory-calltree-view > .call-tree-cells-container"); + }, + + /** + * Unbinds events. + */ + destroy: function () { + DetailsSubview.destroy.call(this); + }, + + /** + * Method for handling all the set up for rendering a new call tree. + * + * @param object interval [optional] + * The { startTime, endTime }, in milliseconds. + */ + render: function (interval = {}) { + let options = { + invertTree: PerformanceController.getOption("invert-call-tree") + }; + let recording = PerformanceController.getCurrentRecording(); + let allocations = recording.getAllocations(); + let threadNode = this._prepareCallTree(allocations, interval, options); + this._populateCallTree(threadNode, options); + this.emit(EVENTS.UI_MEMORY_CALL_TREE_RENDERED); + }, + + /** + * Fired on the "link" event for the call tree in this container. + */ + _onLink: function (_, treeItem) { + let { url, line } = treeItem.frame.getInfo(); + gToolbox.viewSourceInDebugger(url, line).then(success => { + if (success) { + this.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER); + } else { + this.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER); + } + }); + }, + + /** + * Called when the recording is stopped and prepares data to + * populate the call tree. + */ + _prepareCallTree: function (allocations, { startTime, endTime }, options) { + let thread = RecordingUtils.getProfileThreadFromAllocations(allocations); + let { invertTree } = options; + + return new ThreadNode(thread, { startTime, endTime, invertTree }); + }, + + /** + * Renders the call tree. + */ + _populateCallTree: function (frameNode, options = {}) { + // If we have an empty profile (no samples), then don't invert the tree, as + // it would hide the root node and a completely blank call tree space can be + // mis-interpreted as an error. + let inverted = options.invertTree && frameNode.samples > 0; + + let root = new CallView({ + frame: frameNode, + inverted: inverted, + // Root nodes are hidden in inverted call trees. + hidden: inverted, + // Call trees should only auto-expand when not inverted. Passing undefined + // will default to the CALL_TREE_AUTO_EXPAND depth. + autoExpandDepth: inverted ? 0 : undefined, + // Some cells like the time duration and cost percentage don't make sense + // for a memory allocations call tree. + visibleCells: { + selfCount: true, + count: true, + selfSize: true, + size: true, + selfCountPercentage: true, + countPercentage: true, + selfSizePercentage: true, + sizePercentage: true, + function: true + } + }); + + // Bind events. + root.on("link", this._onLink); + + // Pipe "focus" events to the view, mostly for tests + root.on("focus", () => this.emit("focus")); + + // Clear out other call trees. + this.container.innerHTML = ""; + root.attachTo(this.container); + + // Memory allocation samples don't contain cateogry labels. + root.toggleCategories(false); + }, + + toString: () => "[object MemoryCallTreeView]" +}); + +EventEmitter.decorate(MemoryCallTreeView); diff --git a/devtools/client/performance/views/details-memory-flamegraph.js b/devtools/client/performance/views/details-memory-flamegraph.js new file mode 100644 index 000000000..70eaa3c7a --- /dev/null +++ b/devtools/client/performance/views/details-memory-flamegraph.js @@ -0,0 +1,121 @@ +/* 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/. */ +/* import-globals-from ../performance-controller.js */ +/* import-globals-from ../performance-view.js */ +/* globals DetailsSubview */ +"use strict"; + +/** + * FlameGraph view containing a pyramid-like visualization of memory allocation + * sites, controlled by DetailsView. + */ +var MemoryFlameGraphView = Heritage.extend(DetailsSubview, { + + shouldUpdateWhileMouseIsActive: true, + + rerenderPrefs: [ + "invert-flame-graph", + "flatten-tree-recursion", + "show-idle-blocks" + ], + + /** + * Sets up the view with event binding. + */ + initialize: Task.async(function* () { + DetailsSubview.initialize.call(this); + + this.graph = new FlameGraph($("#memory-flamegraph-view")); + this.graph.timelineTickUnits = L10N.getStr("graphs.ms"); + this.graph.setTheme(PerformanceController.getTheme()); + yield this.graph.ready(); + + this._onRangeChangeInGraph = this._onRangeChangeInGraph.bind(this); + this._onThemeChanged = this._onThemeChanged.bind(this); + + PerformanceController.on(EVENTS.THEME_CHANGED, this._onThemeChanged); + this.graph.on("selecting", this._onRangeChangeInGraph); + }), + + /** + * Unbinds events. + */ + destroy: Task.async(function* () { + DetailsSubview.destroy.call(this); + + PerformanceController.off(EVENTS.THEME_CHANGED, this._onThemeChanged); + this.graph.off("selecting", this._onRangeChangeInGraph); + + yield this.graph.destroy(); + }), + + /** + * Method for handling all the set up for rendering a new flamegraph. + * + * @param object interval [optional] + * The { startTime, endTime }, in milliseconds. + */ + render: function (interval = {}) { + let recording = PerformanceController.getCurrentRecording(); + let duration = recording.getDuration(); + let allocations = recording.getAllocations(); + + let thread = RecordingUtils.getProfileThreadFromAllocations(allocations); + let data = FlameGraphUtils.createFlameGraphDataFromThread(thread, { + invertStack: PerformanceController.getOption("invert-flame-graph"), + flattenRecursion: PerformanceController.getOption("flatten-tree-recursion"), + showIdleBlocks: PerformanceController.getOption("show-idle-blocks") + && L10N.getStr("table.idle") + }); + + this.graph.setData({ data, + bounds: { + startTime: 0, + endTime: duration + }, + visible: { + startTime: interval.startTime || 0, + endTime: interval.endTime || duration + } + }); + + this.emit(EVENTS.UI_MEMORY_FLAMEGRAPH_RENDERED); + }, + + /** + * Fired when a range is selected or cleared in the FlameGraph. + */ + _onRangeChangeInGraph: function () { + let interval = this.graph.getViewRange(); + + // Squelch rerendering this view when we update the range here + // to avoid recursion, as our FlameGraph handles rerendering itself + // when originating from within the graph. + this.requiresUpdateOnRangeChange = false; + OverviewView.setTimeInterval(interval); + this.requiresUpdateOnRangeChange = true; + }, + + /** + * Called whenever a pref is changed and this view needs to be rerendered. + */ + _onRerenderPrefChanged: function () { + let recording = PerformanceController.getCurrentRecording(); + let allocations = recording.getAllocations(); + let thread = RecordingUtils.getProfileThreadFromAllocations(allocations); + FlameGraphUtils.removeFromCache(thread); + }, + + /** + * Called when `devtools.theme` changes. + */ + _onThemeChanged: function (_, theme) { + this.graph.setTheme(theme); + this.graph.refresh({ force: true }); + }, + + toString: () => "[object MemoryFlameGraphView]" +}); + +EventEmitter.decorate(MemoryFlameGraphView); diff --git a/devtools/client/performance/views/details-waterfall.js b/devtools/client/performance/views/details-waterfall.js new file mode 100644 index 000000000..db8def053 --- /dev/null +++ b/devtools/client/performance/views/details-waterfall.js @@ -0,0 +1,252 @@ +/* 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/. */ +/* import-globals-from ../performance-controller.js */ +/* import-globals-from ../performance-view.js */ +/* globals window, DetailsSubview */ +"use strict"; + +const MARKER_DETAILS_WIDTH = 200; +// Units are in milliseconds. +const WATERFALL_RESIZE_EVENTS_DRAIN = 100; + +const { TickUtils } = require("devtools/client/performance/modules/waterfall-ticks"); + +/** + * Waterfall view containing the timeline markers, controlled by DetailsView. + */ +var WaterfallView = Heritage.extend(DetailsSubview, { + + // Smallest unit of time between two markers. Larger by 10x^3 than Number.EPSILON. + MARKER_EPSILON: 0.000000000001, + // px + WATERFALL_MARKER_SIDEBAR_WIDTH: 175, + // px + WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS: 20, + + observedPrefs: [ + "hidden-markers" + ], + + rerenderPrefs: [ + "hidden-markers" + ], + + // Units are in milliseconds. + rangeChangeDebounceTime: 75, + + /** + * Sets up the view with event binding. + */ + initialize: function () { + DetailsSubview.initialize.call(this); + + this._cache = new WeakMap(); + + this._onMarkerSelected = this._onMarkerSelected.bind(this); + this._onResize = this._onResize.bind(this); + this._onViewSource = this._onViewSource.bind(this); + this._onShowAllocations = this._onShowAllocations.bind(this); + this._hiddenMarkers = PerformanceController.getPref("hidden-markers"); + + this.treeContainer = $("#waterfall-tree"); + this.detailsContainer = $("#waterfall-details"); + this.detailsSplitter = $("#waterfall-view > splitter"); + + this.details = new MarkerDetails($("#waterfall-details"), + $("#waterfall-view > splitter")); + this.details.hidden = true; + + this.details.on("resize", this._onResize); + this.details.on("view-source", this._onViewSource); + this.details.on("show-allocations", this._onShowAllocations); + window.addEventListener("resize", this._onResize); + + // TODO bug 1167093 save the previously set width, and ensure minimum width + this.details.width = MARKER_DETAILS_WIDTH; + }, + + /** + * Unbinds events. + */ + destroy: function () { + DetailsSubview.destroy.call(this); + + clearNamedTimeout("waterfall-resize"); + + this._cache = null; + + this.details.off("resize", this._onResize); + this.details.off("view-source", this._onViewSource); + this.details.off("show-allocations", this._onShowAllocations); + window.removeEventListener("resize", this._onResize); + + ReactDOM.unmountComponentAtNode(this.treeContainer); + }, + + /** + * Method for handling all the set up for rendering a new waterfall. + * + * @param object interval [optional] + * The { startTime, endTime }, in milliseconds. + */ + render: function (interval = {}) { + let recording = PerformanceController.getCurrentRecording(); + if (recording.isRecording()) { + return; + } + let startTime = interval.startTime || 0; + let endTime = interval.endTime || recording.getDuration(); + let markers = recording.getMarkers(); + let rootMarkerNode = this._prepareWaterfallTree(markers); + + this._populateWaterfallTree(rootMarkerNode, { startTime, endTime }); + this.emit(EVENTS.UI_WATERFALL_RENDERED); + }, + + /** + * Called when a marker is selected in the waterfall view, + * updating the markers detail view. + */ + _onMarkerSelected: function (event, marker) { + let recording = PerformanceController.getCurrentRecording(); + let frames = recording.getFrames(); + let allocations = recording.getConfiguration().withAllocations; + + if (event === "selected") { + this.details.render({ marker, frames, allocations }); + this.details.hidden = false; + } + if (event === "unselected") { + this.details.empty(); + } + }, + + /** + * Called when the marker details view is resized. + */ + _onResize: function () { + setNamedTimeout("waterfall-resize", WATERFALL_RESIZE_EVENTS_DRAIN, () => { + this.render(OverviewView.getTimeInterval()); + }); + }, + + /** + * Called whenever an observed pref is changed. + */ + _onObservedPrefChange: function (_, prefName) { + this._hiddenMarkers = PerformanceController.getPref("hidden-markers"); + + // Clear the cache as we'll need to recompute the collapsed + // marker model + this._cache = new WeakMap(); + }, + + /** + * Called when MarkerDetails view emits an event to view source. + */ + _onViewSource: function (_, data) { + gToolbox.viewSourceInDebugger(data.url, data.line); + }, + + /** + * Called when MarkerDetails view emits an event to snap to allocations. + */ + _onShowAllocations: function (_, data) { + let { endTime } = data; + let startTime = 0; + let recording = PerformanceController.getCurrentRecording(); + let markers = recording.getMarkers(); + + let lastGCMarkerFromPreviousCycle = null; + let lastGCMarker = null; + // Iterate over markers looking for the most recent GC marker + // from the cycle before the marker's whose allocations we're interested in. + for (let marker of markers) { + // We found the marker whose allocations we're tracking; abort + if (marker.start === endTime) { + break; + } + + if (marker.name === "GarbageCollection") { + if (lastGCMarker && lastGCMarker.cycle !== marker.cycle) { + lastGCMarkerFromPreviousCycle = lastGCMarker; + } + lastGCMarker = marker; + } + } + + if (lastGCMarkerFromPreviousCycle) { + startTime = lastGCMarkerFromPreviousCycle.end; + } + + // Adjust times so we don't include the range of these markers themselves. + endTime -= this.MARKER_EPSILON; + startTime += startTime !== 0 ? this.MARKER_EPSILON : 0; + + OverviewView.setTimeInterval({ startTime, endTime }); + DetailsView.selectView("memory-calltree"); + }, + + /** + * Called when the recording is stopped and prepares data to + * populate the waterfall tree. + */ + _prepareWaterfallTree: function (markers) { + let cached = this._cache.get(markers); + if (cached) { + return cached; + } + + let rootMarkerNode = WaterfallUtils.createParentNode({ name: "(root)" }); + + WaterfallUtils.collapseMarkersIntoNode({ + rootNode: rootMarkerNode, + markersList: markers, + filter: this._hiddenMarkers + }); + + this._cache.set(markers, rootMarkerNode); + return rootMarkerNode; + }, + + /** + * Calculates the available width for the waterfall. + * This should be invoked every time the container node is resized. + */ + _recalculateBounds: function () { + this.waterfallWidth = this.treeContainer.clientWidth + - this.WATERFALL_MARKER_SIDEBAR_WIDTH + - this.WATERFALL_MARKER_SIDEBAR_SAFE_BOUNDS; + }, + + /** + * Renders the waterfall tree. + */ + _populateWaterfallTree: function (rootMarkerNode, interval) { + this._recalculateBounds(); + + let doc = this.treeContainer.ownerDocument; + let startTime = interval.startTime | 0; + let endTime = interval.endTime | 0; + let dataScale = this.waterfallWidth / (endTime - startTime); + + this.canvas = TickUtils.drawWaterfallBackground(doc, dataScale, this.waterfallWidth); + + let treeView = Waterfall({ + marker: rootMarkerNode, + startTime, + endTime, + dataScale, + sidebarWidth: this.WATERFALL_MARKER_SIDEBAR_WIDTH, + waterfallWidth: this.waterfallWidth, + onFocus: node => this._onMarkerSelected("selected", node) + }); + + ReactDOM.render(treeView, this.treeContainer); + }, + + toString: () => "[object WaterfallView]" +}); + +EventEmitter.decorate(WaterfallView); diff --git a/devtools/client/performance/views/details.js b/devtools/client/performance/views/details.js new file mode 100644 index 000000000..95557bc36 --- /dev/null +++ b/devtools/client/performance/views/details.js @@ -0,0 +1,263 @@ +/* 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/. */ +/* import-globals-from ../performance-controller.js */ +/* import-globals-from ../performance-view.js */ +/* globals WaterfallView, JsCallTreeView, JsFlameGraphView, MemoryCallTreeView, + MemoryFlameGraphView */ +"use strict"; + +/** + * Details view containing call trees, flamegraphs and markers waterfall. + * Manages subviews and toggles visibility between them. + */ +var DetailsView = { + /** + * Name to (node id, view object, actor requirements, pref killswitch) + * mapping of subviews. + */ + components: { + "waterfall": { + id: "waterfall-view", + view: WaterfallView, + features: ["withMarkers"] + }, + "js-calltree": { + id: "js-profile-view", + view: JsCallTreeView + }, + "js-flamegraph": { + id: "js-flamegraph-view", + view: JsFlameGraphView, + }, + "memory-calltree": { + id: "memory-calltree-view", + view: MemoryCallTreeView, + features: ["withAllocations"] + }, + "memory-flamegraph": { + id: "memory-flamegraph-view", + view: MemoryFlameGraphView, + features: ["withAllocations"], + prefs: ["enable-memory-flame"], + }, + }, + + /** + * Sets up the view with event binding, initializes subviews. + */ + initialize: Task.async(function* () { + this.el = $("#details-pane"); + this.toolbar = $("#performance-toolbar-controls-detail-views"); + + this._onViewToggle = this._onViewToggle.bind(this); + this._onRecordingStoppedOrSelected = this._onRecordingStoppedOrSelected.bind(this); + this.setAvailableViews = this.setAvailableViews.bind(this); + + for (let button of $$("toolbarbutton[data-view]", this.toolbar)) { + button.addEventListener("command", this._onViewToggle); + } + + yield this.setAvailableViews(); + + PerformanceController.on(EVENTS.RECORDING_STATE_CHANGE, + this._onRecordingStoppedOrSelected); + PerformanceController.on(EVENTS.RECORDING_SELECTED, + this._onRecordingStoppedOrSelected); + PerformanceController.on(EVENTS.PREF_CHANGED, this.setAvailableViews); + }), + + /** + * Unbinds events, destroys subviews. + */ + destroy: Task.async(function* () { + for (let button of $$("toolbarbutton[data-view]", this.toolbar)) { + button.removeEventListener("command", this._onViewToggle); + } + + for (let component of Object.values(this.components)) { + component.initialized && (yield component.view.destroy()); + } + + PerformanceController.off(EVENTS.RECORDING_STATE_CHANGE, + this._onRecordingStoppedOrSelected); + PerformanceController.off(EVENTS.RECORDING_SELECTED, + this._onRecordingStoppedOrSelected); + PerformanceController.off(EVENTS.PREF_CHANGED, this.setAvailableViews); + }), + + /** + * Sets the possible views based off of recording features and server actor support + * by hiding/showing the buttons that select them and going to default view + * if currently selected. Called when a preference changes in + * `devtools.performance.ui.`. + */ + setAvailableViews: Task.async(function* () { + let recording = PerformanceController.getCurrentRecording(); + let isCompleted = recording && recording.isCompleted(); + let invalidCurrentView = false; + + for (let [name, { view }] of Object.entries(this.components)) { + let isSupported = this._isViewSupported(name); + + $(`toolbarbutton[data-view=${name}]`).hidden = !isSupported; + + // If the view is currently selected and not supported, go back to the + // default view. + if (!isSupported && this.isViewSelected(view)) { + invalidCurrentView = true; + } + } + + // Two scenarios in which we select the default view. + // + // 1: If we currently have selected a view that is no longer valid due + // to feature support, and this isn't the first view, and the current recording + // is completed. + // + // 2. If we have a finished recording and no panel was selected yet, + // use a default now that we have the recording configurations + if ((this._initialized && isCompleted && invalidCurrentView) || + (!this._initialized && isCompleted && recording)) { + yield this.selectDefaultView(); + } + }), + + /** + * Takes a view name and determines if the current recording + * can support the view. + * + * @param {string} viewName + * @return {boolean} + */ + _isViewSupported: function (viewName) { + let { features, prefs } = this.components[viewName]; + let recording = PerformanceController.getCurrentRecording(); + + if (!recording || !recording.isCompleted()) { + return false; + } + + let prefSupported = (prefs && prefs.length) ? + prefs.every(p => PerformanceController.getPref(p)) : + true; + return PerformanceController.isFeatureSupported(features) && prefSupported; + }, + + /** + * Select one of the DetailView's subviews to be rendered, + * hiding the others. + * + * @param String viewName + * Name of the view to be shown. + */ + selectView: Task.async(function* (viewName) { + let component = this.components[viewName]; + this.el.selectedPanel = $("#" + component.id); + + yield this._whenViewInitialized(component); + + for (let button of $$("toolbarbutton[data-view]", this.toolbar)) { + if (button.getAttribute("data-view") === viewName) { + button.setAttribute("checked", true); + } else { + button.removeAttribute("checked"); + } + } + + // Set a flag indicating that a view was explicitly set based on a + // recording's features. + this._initialized = true; + + this.emit(EVENTS.UI_DETAILS_VIEW_SELECTED, viewName); + }), + + /** + * Selects a default view based off of protocol support + * and preferences enabled. + */ + selectDefaultView: function () { + // We want the waterfall to be default view in almost all cases, except when + // timeline actor isn't supported, or we have markers disabled (which should only + // occur temporarily via bug 1156499 + if (this._isViewSupported("waterfall")) { + return this.selectView("waterfall"); + } + // The JS CallTree should always be supported since the profiler + // actor is as old as the world. + return this.selectView("js-calltree"); + }, + + /** + * Checks if the provided view is currently selected. + * + * @param object viewObject + * @return boolean + */ + isViewSelected: function (viewObject) { + // If not initialized, and we have no recordings, + // no views are selected (even though there's a selected panel) + if (!this._initialized) { + return false; + } + + let selectedPanel = this.el.selectedPanel; + let selectedId = selectedPanel.id; + + for (let { id, view } of Object.values(this.components)) { + if (id == selectedId && view == viewObject) { + return true; + } + } + + return false; + }, + + /** + * Initializes a subview if it wasn't already set up, and makes sure + * it's populated with recording data if there is some available. + * + * @param object component + * A component descriptor from DetailsView.components + */ + _whenViewInitialized: Task.async(function* (component) { + if (component.initialized) { + return; + } + component.initialized = true; + yield component.view.initialize(); + + // If this view is initialized *after* a recording is shown, it won't display + // any data. Make sure it's populated by setting `shouldUpdateWhenShown`. + // All detail views require a recording to be complete, so do not + // attempt to render if recording is in progress or does not exist. + let recording = PerformanceController.getCurrentRecording(); + if (recording && recording.isCompleted()) { + component.view.shouldUpdateWhenShown = true; + } + }), + + /** + * Called when recording stops or is selected. + */ + _onRecordingStoppedOrSelected: function (_, state, recording) { + if (typeof state === "string" && state !== "recording-stopped") { + return; + } + this.setAvailableViews(); + }, + + /** + * Called when a view button is clicked. + */ + _onViewToggle: function (e) { + this.selectView(e.target.getAttribute("data-view")); + }, + + toString: () => "[object DetailsView]" +}; + +/** + * Convenient way of emitting events from the view. + */ +EventEmitter.decorate(DetailsView); diff --git a/devtools/client/performance/views/overview.js b/devtools/client/performance/views/overview.js new file mode 100644 index 000000000..f45a6d844 --- /dev/null +++ b/devtools/client/performance/views/overview.js @@ -0,0 +1,423 @@ +/* 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/. */ +/* import-globals-from ../performance-controller.js */ +/* import-globals-from ../performance-view.js */ +"use strict"; + +// No sense updating the overview more often than receiving data from the +// backend. Make sure this isn't lower than DEFAULT_TIMELINE_DATA_PULL_TIMEOUT +// in devtools/server/actors/timeline.js + +// The following units are in milliseconds. +const OVERVIEW_UPDATE_INTERVAL = 200; +const FRAMERATE_GRAPH_LOW_RES_INTERVAL = 100; +const FRAMERATE_GRAPH_HIGH_RES_INTERVAL = 16; +const GRAPH_REQUIREMENTS = { + timeline: { + features: ["withMarkers"] + }, + framerate: { + features: ["withTicks"] + }, + memory: { + features: ["withMemory"] + }, +}; + +/** + * View handler for the overview panel's time view, displaying + * framerate, timeline and memory over time. + */ +var OverviewView = { + + /** + * How frequently we attempt to render the graphs. Overridden + * in tests. + */ + OVERVIEW_UPDATE_INTERVAL: OVERVIEW_UPDATE_INTERVAL, + + /** + * Sets up the view with event binding. + */ + initialize: function () { + this.graphs = new GraphsController({ + root: $("#overview-pane"), + getFilter: () => PerformanceController.getPref("hidden-markers"), + getTheme: () => PerformanceController.getTheme(), + }); + + // If no timeline support, shut it all down. + if (!PerformanceController.getTraits().features.withMarkers) { + this.disable(); + return; + } + + // Store info on multiprocess support. + this._multiprocessData = PerformanceController.getMultiprocessStatus(); + + this._onRecordingStateChange = this._onRecordingStateChange.bind(this); + this._onRecordingSelected = this._onRecordingSelected.bind(this); + this._onRecordingTick = this._onRecordingTick.bind(this); + this._onGraphSelecting = this._onGraphSelecting.bind(this); + this._onGraphRendered = this._onGraphRendered.bind(this); + this._onPrefChanged = this._onPrefChanged.bind(this); + this._onThemeChanged = this._onThemeChanged.bind(this); + + // Toggle the initial visibility of memory and framerate graph containers + // based off of prefs. + PerformanceController.on(EVENTS.PREF_CHANGED, this._onPrefChanged); + PerformanceController.on(EVENTS.THEME_CHANGED, this._onThemeChanged); + PerformanceController.on(EVENTS.RECORDING_STATE_CHANGE, this._onRecordingStateChange); + PerformanceController.on(EVENTS.RECORDING_SELECTED, this._onRecordingSelected); + this.graphs.on("selecting", this._onGraphSelecting); + this.graphs.on("rendered", this._onGraphRendered); + }, + + /** + * Unbinds events. + */ + destroy: Task.async(function* () { + PerformanceController.off(EVENTS.PREF_CHANGED, this._onPrefChanged); + PerformanceController.off(EVENTS.THEME_CHANGED, this._onThemeChanged); + PerformanceController.off(EVENTS.RECORDING_STATE_CHANGE, + this._onRecordingStateChange); + PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelected); + this.graphs.off("selecting", this._onGraphSelecting); + this.graphs.off("rendered", this._onGraphRendered); + yield this.graphs.destroy(); + }), + + /** + * Returns true if any of the overview graphs have mouse dragging active, + * false otherwise. + */ + get isMouseActive() { + // Fetch all graphs currently stored in the GraphsController. + // These graphs are not necessarily active, but will not have + // an active mouse, in that case. + return !!this.graphs.getWidgets().some(e => e.isMouseActive); + }, + + /** + * Disabled in the event we're using a Timeline mock, so we'll have no + * timeline, ticks or memory data to show, so just block rendering and hide + * the panel. + */ + disable: function () { + this._disabled = true; + this.graphs.disableAll(); + }, + + /** + * Returns the disabled status. + * + * @return boolean + */ + isDisabled: function () { + return this._disabled; + }, + + /** + * Sets the time interval selection for all graphs in this overview. + * + * @param object interval + * The { startTime, endTime }, in milliseconds. + */ + setTimeInterval: function (interval, options = {}) { + let recording = PerformanceController.getCurrentRecording(); + if (recording == null) { + throw new Error("A recording should be available in order to set the selection."); + } + if (this.isDisabled()) { + return; + } + let mapStart = () => 0; + let mapEnd = () => recording.getDuration(); + let selection = { start: interval.startTime, end: interval.endTime }; + this._stopSelectionChangeEventPropagation = options.stopPropagation; + this.graphs.setMappedSelection(selection, { mapStart, mapEnd }); + this._stopSelectionChangeEventPropagation = false; + }, + + /** + * Gets the time interval selection for all graphs in this overview. + * + * @return object + * The { startTime, endTime }, in milliseconds. + */ + getTimeInterval: function () { + let recording = PerformanceController.getCurrentRecording(); + if (recording == null) { + throw new Error("A recording should be available in order to get the selection."); + } + if (this.isDisabled()) { + return { startTime: 0, endTime: recording.getDuration() }; + } + let mapStart = () => 0; + let mapEnd = () => recording.getDuration(); + let selection = this.graphs.getMappedSelection({ mapStart, mapEnd }); + // If no selection returned, this means the overview graphs have not been rendered + // yet, so act as if we have no selection (the full recording). Also + // if the selection range distance is tiny, assume the range was cleared or just + // clicked, and we do not have a range. + if (!selection || (selection.max - selection.min) < 1) { + return { startTime: 0, endTime: recording.getDuration() }; + } + return { startTime: selection.min, endTime: selection.max }; + }, + + /** + * Method for handling all the set up for rendering the overview graphs. + * + * @param number resolution + * The fps graph resolution. @see Graphs.js + */ + render: Task.async(function* (resolution) { + if (this.isDisabled()) { + return; + } + + let recording = PerformanceController.getCurrentRecording(); + yield this.graphs.render(recording.getAllData(), resolution); + + // Finished rendering all graphs in this overview. + this.emit(EVENTS.UI_OVERVIEW_RENDERED, resolution); + }), + + /** + * Called at most every OVERVIEW_UPDATE_INTERVAL milliseconds + * and uses data fetched from the controller to render + * data into all the corresponding overview graphs. + */ + _onRecordingTick: Task.async(function* () { + yield this.render(FRAMERATE_GRAPH_LOW_RES_INTERVAL); + this._prepareNextTick(); + }), + + /** + * Called to refresh the timer to keep firing _onRecordingTick. + */ + _prepareNextTick: function () { + // Check here to see if there's still a _timeoutId, incase + // `stop` was called before the _prepareNextTick call was executed. + if (this.isRendering()) { + this._timeoutId = setTimeout(this._onRecordingTick, this.OVERVIEW_UPDATE_INTERVAL); + } + }, + + /** + * Called when recording state changes. + */ + _onRecordingStateChange: OverviewViewOnStateChange(Task.async( + function* (_, state, recording) { + if (state !== "recording-stopped") { + return; + } + // Check to see if the recording that just stopped is the current recording. + // If it is, render the high-res graphs. For manual recordings, it will also + // be the current recording, but profiles generated by `console.profile` can stop + // while having another profile selected -- in this case, OverviewView should keep + // rendering the current recording. + if (recording !== PerformanceController.getCurrentRecording()) { + return; + } + this.render(FRAMERATE_GRAPH_HIGH_RES_INTERVAL); + yield this._checkSelection(recording); + })), + + /** + * Called when a new recording is selected. + */ + _onRecordingSelected: OverviewViewOnStateChange(Task.async(function* (_, recording) { + this._setGraphVisibilityFromRecordingFeatures(recording); + + // If this recording is complete, render the high res graph + if (recording.isCompleted()) { + yield this.render(FRAMERATE_GRAPH_HIGH_RES_INTERVAL); + } + yield this._checkSelection(recording); + this.graphs.dropSelection(); + })), + + /** + * Start the polling for rendering the overview graph. + */ + _startPolling: function () { + this._timeoutId = setTimeout(this._onRecordingTick, this.OVERVIEW_UPDATE_INTERVAL); + }, + + /** + * Stop the polling for rendering the overview graph. + */ + _stopPolling: function () { + clearTimeout(this._timeoutId); + this._timeoutId = null; + }, + + /** + * Whether or not the overview view is in a state of polling rendering. + */ + isRendering: function () { + return !!this._timeoutId; + }, + + /** + * Makes sure the selection is enabled or disabled in all the graphs, + * based on whether a recording currently exists and is not in progress. + */ + _checkSelection: Task.async(function* (recording) { + let isEnabled = recording ? recording.isCompleted() : false; + yield this.graphs.selectionEnabled(isEnabled); + }), + + /** + * Fired when the graph selection has changed. Called by + * mouseup and scroll events. + */ + _onGraphSelecting: function () { + if (this._stopSelectionChangeEventPropagation) { + return; + } + + this.emit(EVENTS.UI_OVERVIEW_RANGE_SELECTED, this.getTimeInterval()); + }, + + _onGraphRendered: function (_, graphName) { + switch (graphName) { + case "timeline": + this.emit(EVENTS.UI_MARKERS_GRAPH_RENDERED); + break; + case "memory": + this.emit(EVENTS.UI_MEMORY_GRAPH_RENDERED); + break; + case "framerate": + this.emit(EVENTS.UI_FRAMERATE_GRAPH_RENDERED); + break; + } + }, + + /** + * Called whenever a preference in `devtools.performance.ui.` changes. + * Does not care about the enabling of memory/framerate graphs, + * because those will set values on a recording model, and + * the graphs will render based on the existence. + */ + _onPrefChanged: Task.async(function* (_, prefName, prefValue) { + switch (prefName) { + case "hidden-markers": { + let graph = yield this.graphs.isAvailable("timeline"); + if (graph) { + let filter = PerformanceController.getPref("hidden-markers"); + graph.setFilter(filter); + graph.refresh({ force: true }); + } + break; + } + } + }), + + _setGraphVisibilityFromRecordingFeatures: function (recording) { + for (let [graphName, requirements] of Object.entries(GRAPH_REQUIREMENTS)) { + this.graphs.enable(graphName, + PerformanceController.isFeatureSupported(requirements.features)); + } + }, + + /** + * Fetch the multiprocess status and if e10s is not currently on, disable + * realtime rendering. + * + * @return {boolean} + */ + isRealtimeRenderingEnabled: function () { + return this._multiprocessData.enabled; + }, + + /** + * Show the graphs overview panel when a recording is finished + * when non-realtime graphs are enabled. Also set the graph visibility + * so the performance graphs know which graphs to render. + * + * @param {RecordingModel} recording + */ + _showGraphsPanel: function (recording) { + this._setGraphVisibilityFromRecordingFeatures(recording); + $("#overview-pane").classList.remove("hidden"); + }, + + /** + * Hide the graphs container completely. + */ + _hideGraphsPanel: function () { + $("#overview-pane").classList.add("hidden"); + }, + + /** + * Called when `devtools.theme` changes. + */ + _onThemeChanged: function (_, theme) { + this.graphs.setTheme({ theme, redraw: true }); + }, + + toString: () => "[object OverviewView]" +}; + +/** + * Utility that can wrap a method of OverviewView that + * handles a recording state change like when a recording is starting, + * stopping, or about to start/stop, and determines whether or not + * the polling for rendering the overview graphs needs to start or stop. + * Must be called with the OverviewView context. + * + * @param {function?} fn + * @return {function} + */ +function OverviewViewOnStateChange(fn) { + return function _onRecordingStateChange(eventName, recording) { + // Normalize arguments for the RECORDING_STATE_CHANGE event, + // as it also has a `state` argument. + if (typeof recording === "string") { + recording = arguments[2]; + } + + let currentRecording = PerformanceController.getCurrentRecording(); + + // All these methods require a recording to exist selected and + // from the event name, since there is a delay between starting + // a recording and changing the selection. + if (!currentRecording || !recording) { + // If no recording (this can occur when having a console.profile recording, and + // we do not stop it from the backend), and we are still rendering updates, + // stop that. + if (this.isRendering()) { + this._stopPolling(); + } + return; + } + + // If realtime rendering is not enabed (e10s not on), then + // show the disabled message, or the full graphs if the recording is completed + if (!this.isRealtimeRenderingEnabled()) { + if (recording.isRecording()) { + this._hideGraphsPanel(); + // Abort, as we do not want to change polling status. + return; + } + this._showGraphsPanel(recording); + } + + if (this.isRendering() && !currentRecording.isRecording()) { + this._stopPolling(); + } else if (currentRecording.isRecording() && !this.isRendering()) { + this._startPolling(); + } + + if (fn) { + fn.apply(this, arguments); + } + }; +} + +// Decorates the OverviewView as an EventEmitter +EventEmitter.decorate(OverviewView); diff --git a/devtools/client/performance/views/recordings.js b/devtools/client/performance/views/recordings.js new file mode 100644 index 000000000..487ea4f03 --- /dev/null +++ b/devtools/client/performance/views/recordings.js @@ -0,0 +1,202 @@ +/* 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/. */ +/* import-globals-from ../performance-controller.js */ +/* import-globals-from ../performance-view.js */ +/* globals document, window */ +"use strict"; + +/** + * Functions handling the recordings UI. + */ +var RecordingsView = { + /** + * Initialization function, called when the tool is started. + */ + initialize: function () { + this._onSelect = this._onSelect.bind(this); + this._onRecordingStateChange = this._onRecordingStateChange.bind(this); + this._onNewRecording = this._onNewRecording.bind(this); + this._onSaveButtonClick = this._onSaveButtonClick.bind(this); + this._onRecordingDeleted = this._onRecordingDeleted.bind(this); + this._onRecordingExported = this._onRecordingExported.bind(this); + + PerformanceController.on(EVENTS.RECORDING_STATE_CHANGE, this._onRecordingStateChange); + PerformanceController.on(EVENTS.RECORDING_ADDED, this._onNewRecording); + PerformanceController.on(EVENTS.RECORDING_DELETED, this._onRecordingDeleted); + PerformanceController.on(EVENTS.RECORDING_EXPORTED, this._onRecordingExported); + + // DE-XUL: Begin migrating the recording sidebar to React. Temporarily hold state + // here. + this._listState = { + recordings: [], + labels: new WeakMap(), + selected: null, + }; + this._listMount = PerformanceUtils.createHtmlMount($("#recording-list-mount")); + this._renderList(); + }, + + /** + * Get the index of the currently selected recording. Only used by tests. + * @return {integer} index + */ + getSelectedIndex() { + const { recordings, selected } = this._listState; + return recordings.indexOf(selected); + }, + + /** + * Set the currently selected recording via its index. Only used by tests. + * @param {integer} index + */ + setSelectedByIndex(index) { + this._onSelect(this._listState.recordings[index]); + this._renderList(); + }, + + /** + * DE-XUL: During the migration, this getter will access the selected recording from + * the private _listState object so that tests will continue to pass. + */ + get selected() { + return this._listState.selected; + }, + + /** + * DE-XUL: During the migration, this getter will access the number of recordings. + */ + get itemCount() { + return this._listState.recordings.length; + }, + + /** + * DE-XUL: Render the recording list using React. + */ + _renderList: function () { + const {recordings, labels, selected} = this._listState; + + const recordingList = RecordingList({ + itemComponent: RecordingListItem, + items: recordings.map(recording => ({ + onSelect: () => this._onSelect(recording), + onSave: () => this._onSaveButtonClick(recording), + isLoading: !recording.isRecording() && !recording.isCompleted(), + isRecording: recording.isRecording(), + isSelected: recording === selected, + duration: recording.getDuration().toFixed(0), + label: labels.get(recording), + })) + }); + + ReactDOM.render(recordingList, this._listMount); + }, + + /** + * Destruction function, called when the tool is closed. + */ + destroy: function () { + PerformanceController.off(EVENTS.RECORDING_STATE_CHANGE, + this._onRecordingStateChange); + PerformanceController.off(EVENTS.RECORDING_ADDED, this._onNewRecording); + PerformanceController.off(EVENTS.RECORDING_DELETED, this._onRecordingDeleted); + PerformanceController.off(EVENTS.RECORDING_EXPORTED, this._onRecordingExported); + }, + + /** + * Called when a new recording is stored in the UI. This handles + * when recordings are lazily loaded (like a console.profile occurring + * before the tool is loaded) or imported. In normal manual recording cases, + * this will also be fired. + */ + _onNewRecording: function (_, recording) { + this._onRecordingStateChange(_, null, recording); + }, + + /** + * Signals that a recording has changed state. + * + * @param string state + * Can be "recording-started", "recording-stopped", "recording-stopping" + * @param RecordingModel recording + * Model of the recording that was started. + */ + _onRecordingStateChange: function (_, state, recording) { + const { recordings, labels } = this._listState; + + if (!recordings.includes(recording)) { + recordings.push(recording); + labels.set(recording, recording.getLabel() || + L10N.getFormatStr("recordingsList.itemLabel", recordings.length)); + + // If this is a manual recording, immediately select it, or + // select a console profile if its the only one + if (!recording.isConsole() || !this._listState.selected) { + this._onSelect(recording); + } + } + + // Determine if the recording needs to be selected. + const isCompletedManualRecording = !recording.isConsole() && recording.isCompleted(); + if (recording.isImported() || isCompletedManualRecording) { + this._onSelect(recording); + } + + this._renderList(); + }, + + /** + * Clears out all non-console recordings. + */ + _onRecordingDeleted: function (_, recording) { + const { recordings } = this._listState; + const index = recordings.indexOf(recording); + if (index === -1) { + throw new Error("Attempting to remove a recording that doesn't exist."); + } + recordings.splice(index, 1); + this._renderList(); + }, + + /** + * The select listener for this container. + */ + _onSelect: Task.async(function* (recording) { + this._listState.selected = recording; + this.emit(EVENTS.UI_RECORDING_SELECTED, recording); + this._renderList(); + }), + + /** + * The click listener for the "save" button of each item in this container. + */ + _onSaveButtonClick: function (recording) { + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + fp.init(window, L10N.getStr("recordingsList.saveDialogTitle"), + Ci.nsIFilePicker.modeSave); + fp.appendFilter(L10N.getStr("recordingsList.saveDialogJSONFilter"), "*.json"); + fp.appendFilter(L10N.getStr("recordingsList.saveDialogAllFilter"), "*.*"); + fp.defaultString = "profile.json"; + + fp.open({ done: result => { + if (result == Ci.nsIFilePicker.returnCancel) { + return; + } + this.emit(EVENTS.UI_EXPORT_RECORDING, recording, fp.file); + }}); + }, + + _onRecordingExported: function (_, recording, file) { + if (recording.isConsole()) { + return; + } + const name = file.leafName.replace(/\..+$/, ""); + this._listState.labels.set(recording, name); + this._renderList(); + } +}; + +/** + * Convenient way of emitting events from the RecordingsView. + */ +EventEmitter.decorate(RecordingsView); diff --git a/devtools/client/performance/views/toolbar.js b/devtools/client/performance/views/toolbar.js new file mode 100644 index 000000000..bcab09a86 --- /dev/null +++ b/devtools/client/performance/views/toolbar.js @@ -0,0 +1,160 @@ +/* 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/. */ +/* import-globals-from ../performance-controller.js */ +/* import-globals-from ../performance-view.js */ +/* globals document */ +"use strict"; + +/** + * View handler for toolbar events (mostly option toggling and triggering) + */ +var ToolbarView = { + /** + * Sets up the view with event binding. + */ + initialize: Task.async(function* () { + this._onFilterPopupShowing = this._onFilterPopupShowing.bind(this); + this._onFilterPopupHiding = this._onFilterPopupHiding.bind(this); + this._onHiddenMarkersChanged = this._onHiddenMarkersChanged.bind(this); + this._onPrefChanged = this._onPrefChanged.bind(this); + this._popup = $("#performance-options-menupopup"); + + this.optionsView = new OptionsView({ + branchName: BRANCH_NAME, + menupopup: this._popup + }); + + // Set the visibility of experimental UI options on load + // based off of `devtools.performance.ui.experimental` preference + let experimentalEnabled = PerformanceController.getOption("experimental"); + this._toggleExperimentalUI(experimentalEnabled); + + yield this.optionsView.initialize(); + this.optionsView.on("pref-changed", this._onPrefChanged); + + this._buildMarkersFilterPopup(); + this._updateHiddenMarkersPopup(); + $("#performance-filter-menupopup").addEventListener("popupshowing", + this._onFilterPopupShowing); + $("#performance-filter-menupopup").addEventListener("popuphiding", + this._onFilterPopupHiding); + }), + + /** + * Unbinds events and cleans up view. + */ + destroy: function () { + $("#performance-filter-menupopup").removeEventListener("popupshowing", + this._onFilterPopupShowing); + $("#performance-filter-menupopup").removeEventListener("popuphiding", + this._onFilterPopupHiding); + this._popup = null; + + this.optionsView.off("pref-changed", this._onPrefChanged); + this.optionsView.destroy(); + }, + + /** + * Creates the timeline markers filter popup. + */ + _buildMarkersFilterPopup: function () { + for (let [markerName, markerDetails] of Object.entries(TIMELINE_BLUEPRINT)) { + let menuitem = document.createElement("menuitem"); + menuitem.setAttribute("closemenu", "none"); + menuitem.setAttribute("type", "checkbox"); + menuitem.setAttribute("align", "center"); + menuitem.setAttribute("flex", "1"); + menuitem.setAttribute("label", + MarkerBlueprintUtils.getMarkerGenericName(markerName)); + menuitem.setAttribute("marker-type", markerName); + menuitem.className = `marker-color-${markerDetails.colorName}`; + + menuitem.addEventListener("command", this._onHiddenMarkersChanged); + + $("#performance-filter-menupopup").appendChild(menuitem); + } + }, + + /** + * Updates the menu items checked state in the timeline markers filter popup. + */ + _updateHiddenMarkersPopup: function () { + let menuItems = $$("#performance-filter-menupopup menuitem[marker-type]"); + let hiddenMarkers = PerformanceController.getPref("hidden-markers"); + + for (let menuitem of menuItems) { + if (~hiddenMarkers.indexOf(menuitem.getAttribute("marker-type"))) { + menuitem.removeAttribute("checked"); + } else { + menuitem.setAttribute("checked", "true"); + } + } + }, + + /** + * Fired when `devtools.performance.ui.experimental` is changed, or + * during init. Toggles the visibility of experimental performance tool options + * in the UI options. + * + * Sets or removes "experimental-enabled" on the menu and main elements, + * hiding or showing all elements with class "experimental-option". + * + * TODO re-enable "#option-enable-memory" permanently once stable in bug 1163350 + * TODO re-enable "#option-show-jit-optimizations" permanently once stable in + * bug 1163351 + * + * @param {boolean} isEnabled + */ + _toggleExperimentalUI: function (isEnabled) { + if (isEnabled) { + $(".theme-body").classList.add("experimental-enabled"); + this._popup.classList.add("experimental-enabled"); + } else { + $(".theme-body").classList.remove("experimental-enabled"); + this._popup.classList.remove("experimental-enabled"); + } + }, + + /** + * Fired when the markers filter popup starts to show. + */ + _onFilterPopupShowing: function () { + $("#filter-button").setAttribute("open", "true"); + }, + + /** + * Fired when the markers filter popup starts to hide. + */ + _onFilterPopupHiding: function () { + $("#filter-button").removeAttribute("open"); + }, + + /** + * Fired when a menu item in the markers filter popup is checked or unchecked. + */ + _onHiddenMarkersChanged: function () { + let checkedMenuItems = + $$("#performance-filter-menupopup menuitem[marker-type]:not([checked])"); + let hiddenMarkers = Array.map(checkedMenuItems, e => e.getAttribute("marker-type")); + PerformanceController.setPref("hidden-markers", hiddenMarkers); + }, + + /** + * Fired when a preference changes in the underlying OptionsView. + * Propogated by the PerformanceController. + */ + _onPrefChanged: function (_, prefName) { + let value = PerformanceController.getOption(prefName); + + if (prefName === "experimental") { + this._toggleExperimentalUI(value); + } + + this.emit(EVENTS.UI_PREF_CHANGED, prefName, value); + }, + + toString: () => "[object ToolbarView]" +}; + +EventEmitter.decorate(ToolbarView); -- cgit v1.2.3