diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /devtools/client/canvasdebugger/callslist.js | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-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.js | 526 |
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); + } + }); + } +}); |