summaryrefslogtreecommitdiffstats
path: root/devtools/client/canvasdebugger/callslist.js
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /devtools/client/canvasdebugger/callslist.js
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'devtools/client/canvasdebugger/callslist.js')
-rw-r--r--devtools/client/canvasdebugger/callslist.js526
1 files changed, 526 insertions, 0 deletions
diff --git a/devtools/client/canvasdebugger/callslist.js b/devtools/client/canvasdebugger/callslist.js
new file mode 100644
index 000000000..a6fd132c0
--- /dev/null
+++ b/devtools/client/canvasdebugger/callslist.js
@@ -0,0 +1,526 @@
+/* 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 canvasdebugger.js */
+/* globals window, document */
+"use strict";
+
+/**
+ * Functions handling details about a single recorded animation frame snapshot
+ * (the calls list, rendering preview, thumbnails filmstrip etc.).
+ */
+var CallsListView = Heritage.extend(WidgetMethods, {
+ /**
+ * Initialization function, called when the tool is started.
+ */
+ initialize: function () {
+ this.widget = new SideMenuWidget($("#calls-list"));
+ this._slider = $("#calls-slider");
+ this._searchbox = $("#calls-searchbox");
+ this._filmstrip = $("#snapshot-filmstrip");
+
+ this._onSelect = this._onSelect.bind(this);
+ this._onSlideMouseDown = this._onSlideMouseDown.bind(this);
+ this._onSlideMouseUp = this._onSlideMouseUp.bind(this);
+ this._onSlide = this._onSlide.bind(this);
+ this._onSearch = this._onSearch.bind(this);
+ this._onScroll = this._onScroll.bind(this);
+ this._onExpand = this._onExpand.bind(this);
+ this._onStackFileClick = this._onStackFileClick.bind(this);
+ this._onThumbnailClick = this._onThumbnailClick.bind(this);
+
+ this.widget.addEventListener("select", this._onSelect, false);
+ this._slider.addEventListener("mousedown", this._onSlideMouseDown, false);
+ this._slider.addEventListener("mouseup", this._onSlideMouseUp, false);
+ this._slider.addEventListener("change", this._onSlide, false);
+ this._searchbox.addEventListener("input", this._onSearch, false);
+ this._filmstrip.addEventListener("wheel", this._onScroll, false);
+ },
+
+ /**
+ * Destruction function, called when the tool is closed.
+ */
+ destroy: function () {
+ this.widget.removeEventListener("select", this._onSelect, false);
+ this._slider.removeEventListener("mousedown", this._onSlideMouseDown, false);
+ this._slider.removeEventListener("mouseup", this._onSlideMouseUp, false);
+ this._slider.removeEventListener("change", this._onSlide, false);
+ this._searchbox.removeEventListener("input", this._onSearch, false);
+ this._filmstrip.removeEventListener("wheel", this._onScroll, false);
+ },
+
+ /**
+ * Populates this container with a list of function calls.
+ *
+ * @param array functionCalls
+ * A list of function call actors received from the backend.
+ */
+ showCalls: function (functionCalls) {
+ this.empty();
+
+ for (let i = 0, len = functionCalls.length; i < len; i++) {
+ let call = functionCalls[i];
+
+ let view = document.createElement("vbox");
+ view.className = "call-item-view devtools-monospace";
+ view.setAttribute("flex", "1");
+
+ let contents = document.createElement("hbox");
+ contents.className = "call-item-contents";
+ contents.setAttribute("align", "center");
+ contents.addEventListener("dblclick", this._onExpand);
+ view.appendChild(contents);
+
+ let index = document.createElement("label");
+ index.className = "plain call-item-index";
+ index.setAttribute("flex", "1");
+ index.setAttribute("value", i + 1);
+
+ let gutter = document.createElement("hbox");
+ gutter.className = "call-item-gutter";
+ gutter.appendChild(index);
+ contents.appendChild(gutter);
+
+ if (call.callerPreview) {
+ let context = document.createElement("label");
+ context.className = "plain call-item-context";
+ context.setAttribute("value", call.callerPreview);
+ contents.appendChild(context);
+
+ let separator = document.createElement("label");
+ separator.className = "plain call-item-separator";
+ separator.setAttribute("value", ".");
+ contents.appendChild(separator);
+ }
+
+ let name = document.createElement("label");
+ name.className = "plain call-item-name";
+ name.setAttribute("value", call.name);
+ contents.appendChild(name);
+
+ let argsPreview = document.createElement("label");
+ argsPreview.className = "plain call-item-args";
+ argsPreview.setAttribute("crop", "end");
+ argsPreview.setAttribute("flex", "100");
+ // Getters and setters are displayed differently from regular methods.
+ if (call.type == CallWatcherFront.METHOD_FUNCTION) {
+ argsPreview.setAttribute("value", "(" + call.argsPreview + ")");
+ } else {
+ argsPreview.setAttribute("value", " = " + call.argsPreview);
+ }
+ contents.appendChild(argsPreview);
+
+ let location = document.createElement("label");
+ location.className = "plain call-item-location";
+ location.setAttribute("value", getFileName(call.file) + ":" + call.line);
+ location.setAttribute("crop", "start");
+ location.setAttribute("flex", "1");
+ location.addEventListener("mousedown", this._onExpand);
+ contents.appendChild(location);
+
+ // Append a function call item to this container.
+ this.push([view], {
+ staged: true,
+ attachment: {
+ actor: call
+ }
+ });
+
+ // Highlight certain calls that are probably more interesting than
+ // everything else, making it easier to quickly glance over them.
+ if (CanvasFront.DRAW_CALLS.has(call.name)) {
+ view.setAttribute("draw-call", "");
+ }
+ if (CanvasFront.INTERESTING_CALLS.has(call.name)) {
+ view.setAttribute("interesting-call", "");
+ }
+ }
+
+ // Flushes all the prepared function call items into this container.
+ this.commit();
+ window.emit(EVENTS.CALL_LIST_POPULATED);
+
+ // Resetting the function selection slider's value (shown in this
+ // container's toolbar) would trigger a selection event, which should be
+ // ignored in this case.
+ this._ignoreSliderChanges = true;
+ this._slider.value = 0;
+ this._slider.max = functionCalls.length - 1;
+ this._ignoreSliderChanges = false;
+ },
+
+ /**
+ * Displays an image in the rendering preview of this container, generated
+ * for the specified draw call in the recorded animation frame snapshot.
+ *
+ * @param array screenshot
+ * A single "snapshot-image" instance received from the backend.
+ */
+ showScreenshot: function (screenshot) {
+ let { index, width, height, scaling, flipped, pixels } = screenshot;
+
+ let screenshotNode = $("#screenshot-image");
+ screenshotNode.setAttribute("flipped", flipped);
+ drawBackground("screenshot-rendering", width, height, pixels);
+
+ let dimensionsNode = $("#screenshot-dimensions");
+ let actualWidth = (width / scaling) | 0;
+ let actualHeight = (height / scaling) | 0;
+ dimensionsNode.setAttribute("value",
+ SHARED_L10N.getFormatStr("dimensions", actualWidth, actualHeight));
+
+ window.emit(EVENTS.CALL_SCREENSHOT_DISPLAYED);
+ },
+
+ /**
+ * Populates this container's footer with a list of thumbnails, one generated
+ * for each draw call in the recorded animation frame snapshot.
+ *
+ * @param array thumbnails
+ * An array of "snapshot-image" instances received from the backend.
+ */
+ showThumbnails: function (thumbnails) {
+ while (this._filmstrip.hasChildNodes()) {
+ this._filmstrip.firstChild.remove();
+ }
+ for (let thumbnail of thumbnails) {
+ this.appendThumbnail(thumbnail);
+ }
+
+ window.emit(EVENTS.THUMBNAILS_DISPLAYED);
+ },
+
+ /**
+ * Displays an image in the thumbnails list of this container, generated
+ * for the specified draw call in the recorded animation frame snapshot.
+ *
+ * @param array thumbnail
+ * A single "snapshot-image" instance received from the backend.
+ */
+ appendThumbnail: function (thumbnail) {
+ let { index, width, height, flipped, pixels } = thumbnail;
+
+ let thumbnailNode = document.createElementNS(HTML_NS, "canvas");
+ thumbnailNode.setAttribute("flipped", flipped);
+ thumbnailNode.width = Math.max(CanvasFront.THUMBNAIL_SIZE, width);
+ thumbnailNode.height = Math.max(CanvasFront.THUMBNAIL_SIZE, height);
+ drawImage(thumbnailNode, width, height, pixels, { centered: true });
+
+ thumbnailNode.className = "filmstrip-thumbnail";
+ thumbnailNode.onmousedown = e => this._onThumbnailClick(e, index);
+ thumbnailNode.setAttribute("index", index);
+ this._filmstrip.appendChild(thumbnailNode);
+ },
+
+ /**
+ * Sets the currently highlighted thumbnail in this container.
+ * A screenshot will always correlate to a thumbnail in the filmstrip,
+ * both being identified by the same 'index' of the context function call.
+ *
+ * @param number index
+ * The context function call's index.
+ */
+ set highlightedThumbnail(index) {
+ let currHighlightedThumbnail = $(".filmstrip-thumbnail[index='" + index + "']");
+ if (currHighlightedThumbnail == null) {
+ return;
+ }
+
+ let prevIndex = this._highlightedThumbnailIndex;
+ let prevHighlightedThumbnail = $(".filmstrip-thumbnail[index='" + prevIndex + "']");
+ if (prevHighlightedThumbnail) {
+ prevHighlightedThumbnail.removeAttribute("highlighted");
+ }
+
+ currHighlightedThumbnail.setAttribute("highlighted", "");
+ currHighlightedThumbnail.scrollIntoView();
+ this._highlightedThumbnailIndex = index;
+ },
+
+ /**
+ * Gets the currently highlighted thumbnail in this container.
+ * @return number
+ */
+ get highlightedThumbnail() {
+ return this._highlightedThumbnailIndex;
+ },
+
+ /**
+ * The select listener for this container.
+ */
+ _onSelect: function ({ detail: callItem }) {
+ if (!callItem) {
+ return;
+ }
+
+ // Some of the stepping buttons don't make sense specifically while the
+ // last function call is selected.
+ if (this.selectedIndex == this.itemCount - 1) {
+ $("#resume").setAttribute("disabled", "true");
+ $("#step-over").setAttribute("disabled", "true");
+ $("#step-out").setAttribute("disabled", "true");
+ } else {
+ $("#resume").removeAttribute("disabled");
+ $("#step-over").removeAttribute("disabled");
+ $("#step-out").removeAttribute("disabled");
+ }
+
+ // Correlate the currently selected item with the function selection
+ // slider's value. Avoid triggering a redundant selection event.
+ this._ignoreSliderChanges = true;
+ this._slider.value = this.selectedIndex;
+ this._ignoreSliderChanges = false;
+
+ // Can't generate screenshots for function call actors loaded from disk.
+ // XXX: Bug 984844.
+ if (callItem.attachment.actor.isLoadedFromDisk) {
+ return;
+ }
+
+ // To keep continuous selection buttery smooth (for example, while pressing
+ // the DOWN key or moving the slider), only display the screenshot after
+ // any kind of user input stops.
+ setConditionalTimeout("screenshot-display", SCREENSHOT_DISPLAY_DELAY, () => {
+ return !this._isSliding;
+ }, () => {
+ let frameSnapshot = SnapshotsListView.selectedItem.attachment.actor;
+ let functionCall = callItem.attachment.actor;
+ frameSnapshot.generateScreenshotFor(functionCall).then(screenshot => {
+ this.showScreenshot(screenshot);
+ this.highlightedThumbnail = screenshot.index;
+ }).catch(e => console.error(e));
+ });
+ },
+
+ /**
+ * The mousedown listener for the call selection slider.
+ */
+ _onSlideMouseDown: function () {
+ this._isSliding = true;
+ },
+
+ /**
+ * The mouseup listener for the call selection slider.
+ */
+ _onSlideMouseUp: function () {
+ this._isSliding = false;
+ },
+
+ /**
+ * The change listener for the call selection slider.
+ */
+ _onSlide: function () {
+ // Avoid performing any operations when programatically changing the value.
+ if (this._ignoreSliderChanges) {
+ return;
+ }
+ let selectedFunctionCallIndex = this.selectedIndex = this._slider.value;
+
+ // While sliding, immediately show the most relevant thumbnail for a
+ // function call, for a nice diff-like animation effect between draws.
+ let thumbnails = SnapshotsListView.selectedItem.attachment.thumbnails;
+ let thumbnail = getThumbnailForCall(thumbnails, selectedFunctionCallIndex);
+
+ // Avoid drawing and highlighting if the selected function call has the
+ // same thumbnail as the last one.
+ if (thumbnail.index == this.highlightedThumbnail) {
+ return;
+ }
+ // If a thumbnail wasn't found (e.g. the backend avoids creating thumbnails
+ // when rendering offscreen), simply defer to the first available one.
+ if (thumbnail.index == -1) {
+ thumbnail = thumbnails[0];
+ }
+
+ let { index, width, height, flipped, pixels } = thumbnail;
+ this.highlightedThumbnail = index;
+
+ let screenshotNode = $("#screenshot-image");
+ screenshotNode.setAttribute("flipped", flipped);
+ drawBackground("screenshot-rendering", width, height, pixels);
+ },
+
+ /**
+ * The input listener for the calls searchbox.
+ */
+ _onSearch: function (e) {
+ let lowerCaseSearchToken = this._searchbox.value.toLowerCase();
+
+ this.filterContents(e => {
+ let call = e.attachment.actor;
+ let name = call.name.toLowerCase();
+ let file = call.file.toLowerCase();
+ let line = call.line.toString().toLowerCase();
+ let args = call.argsPreview.toLowerCase();
+
+ return name.includes(lowerCaseSearchToken) ||
+ file.includes(lowerCaseSearchToken) ||
+ line.includes(lowerCaseSearchToken) ||
+ args.includes(lowerCaseSearchToken);
+ });
+ },
+
+ /**
+ * The wheel listener for the filmstrip that contains all the thumbnails.
+ */
+ _onScroll: function (e) {
+ this._filmstrip.scrollLeft += e.deltaX;
+ },
+
+ /**
+ * The click/dblclick listener for an item or location url in this container.
+ * When expanding an item, it's corresponding call stack will be displayed.
+ */
+ _onExpand: function (e) {
+ let callItem = this.getItemForElement(e.target);
+ let view = $(".call-item-view", callItem.target);
+
+ // If the call stack nodes were already created, simply re-show them
+ // or jump to the corresponding file and line in the Debugger if a
+ // location link was clicked.
+ if (view.hasAttribute("call-stack-populated")) {
+ let isExpanded = view.getAttribute("call-stack-expanded") == "true";
+
+ // If clicking on the location, jump to the Debugger.
+ if (e.target.classList.contains("call-item-location")) {
+ let { file, line } = callItem.attachment.actor;
+ this._viewSourceInDebugger(file, line);
+ return;
+ }
+ // Otherwise hide the call stack.
+ else {
+ view.setAttribute("call-stack-expanded", !isExpanded);
+ $(".call-item-stack", view).hidden = isExpanded;
+ return;
+ }
+ }
+
+ let list = document.createElement("vbox");
+ list.className = "call-item-stack";
+ view.setAttribute("call-stack-populated", "");
+ view.setAttribute("call-stack-expanded", "true");
+ view.appendChild(list);
+
+ /**
+ * Creates a function call nodes in this container for a stack.
+ */
+ let display = stack => {
+ for (let i = 1; i < stack.length; i++) {
+ let call = stack[i];
+
+ let contents = document.createElement("hbox");
+ contents.className = "call-item-stack-fn";
+ contents.style.paddingInlineStart = (i * STACK_FUNC_INDENTATION) + "px";
+
+ let name = document.createElement("label");
+ name.className = "plain call-item-stack-fn-name";
+ name.setAttribute("value", "↳ " + call.name + "()");
+ contents.appendChild(name);
+
+ let spacer = document.createElement("spacer");
+ spacer.setAttribute("flex", "100");
+ contents.appendChild(spacer);
+
+ let location = document.createElement("label");
+ location.className = "plain call-item-stack-fn-location";
+ location.setAttribute("value", getFileName(call.file) + ":" + call.line);
+ location.setAttribute("crop", "start");
+ location.setAttribute("flex", "1");
+ location.addEventListener("mousedown", e => this._onStackFileClick(e, call));
+ contents.appendChild(location);
+
+ list.appendChild(contents);
+ }
+
+ window.emit(EVENTS.CALL_STACK_DISPLAYED);
+ };
+
+ // If this animation snapshot is loaded from disk, there are no corresponding
+ // backend actors available and the data is immediately available.
+ let functionCall = callItem.attachment.actor;
+ if (functionCall.isLoadedFromDisk) {
+ display(functionCall.stack);
+ }
+ // ..otherwise we need to request the function call stack from the backend.
+ else {
+ callItem.attachment.actor.getDetails().then(fn => display(fn.stack));
+ }
+ },
+
+ /**
+ * The click listener for a location link in the call stack.
+ *
+ * @param string file
+ * The url of the source owning the function.
+ * @param number line
+ * The line of the respective function.
+ */
+ _onStackFileClick: function (e, { file, line }) {
+ this._viewSourceInDebugger(file, line);
+ },
+
+ /**
+ * The click listener for a thumbnail in the filmstrip.
+ *
+ * @param number index
+ * The function index in the recorded animation frame snapshot.
+ */
+ _onThumbnailClick: function (e, index) {
+ this.selectedIndex = index;
+ },
+
+ /**
+ * The click listener for the "resume" button in this container's toolbar.
+ */
+ _onResume: function () {
+ // Jump to the next draw call in the recorded animation frame snapshot.
+ let drawCall = getNextDrawCall(this.items, this.selectedItem);
+ if (drawCall) {
+ this.selectedItem = drawCall;
+ return;
+ }
+
+ // If there are no more draw calls, just jump to the last context call.
+ this._onStepOut();
+ },
+
+ /**
+ * The click listener for the "step over" button in this container's toolbar.
+ */
+ _onStepOver: function () {
+ this.selectedIndex++;
+ },
+
+ /**
+ * The click listener for the "step in" button in this container's toolbar.
+ */
+ _onStepIn: function () {
+ if (this.selectedIndex == -1) {
+ this._onResume();
+ return;
+ }
+ let callItem = this.selectedItem;
+ let { file, line } = callItem.attachment.actor;
+ this._viewSourceInDebugger(file, line);
+ },
+
+ /**
+ * The click listener for the "step out" button in this container's toolbar.
+ */
+ _onStepOut: function () {
+ this.selectedIndex = this.itemCount - 1;
+ },
+
+ /**
+ * Opens the specified file and line in the debugger. Falls back to Firefox's View Source.
+ */
+ _viewSourceInDebugger: function (file, line) {
+ gToolbox.viewSourceInDebugger(file, line).then(success => {
+ if (success) {
+ window.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
+ } else {
+ window.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER);
+ }
+ });
+ }
+});