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 --- devtools/client/canvasdebugger/callslist.js | 526 +++++++++++++++++++++ devtools/client/canvasdebugger/canvasdebugger.js | 341 +++++++++++++ devtools/client/canvasdebugger/canvasdebugger.xul | 135 ++++++ devtools/client/canvasdebugger/moz.build | 10 + devtools/client/canvasdebugger/panel.js | 76 +++ devtools/client/canvasdebugger/snapshotslist.js | 495 +++++++++++++++++++ devtools/client/canvasdebugger/test/.eslintrc.js | 6 + devtools/client/canvasdebugger/test/browser.ini | 61 +++ .../test/browser_canvas-actor-test-01.js | 17 + .../test/browser_canvas-actor-test-02.js | 78 +++ .../test/browser_canvas-actor-test-03.js | 75 +++ .../test/browser_canvas-actor-test-04.js | 85 ++++ .../test/browser_canvas-actor-test-05.js | 50 ++ .../test/browser_canvas-actor-test-06.js | 100 ++++ .../test/browser_canvas-actor-test-07.js | 94 ++++ .../test/browser_canvas-actor-test-08.js | 36 ++ .../test/browser_canvas-actor-test-09.js | 36 ++ .../test/browser_canvas-actor-test-10.js | 107 +++++ .../test/browser_canvas-actor-test-11.js | 138 ++++++ .../test/browser_canvas-actor-test-12.js | 29 ++ .../test/browser_canvas-frontend-call-highlight.js | 41 ++ .../test/browser_canvas-frontend-call-list.js | 70 +++ .../test/browser_canvas-frontend-call-search.js | 72 +++ .../test/browser_canvas-frontend-call-stack-01.js | 82 ++++ .../test/browser_canvas-frontend-call-stack-02.js | 57 +++ .../test/browser_canvas-frontend-call-stack-03.js | 65 +++ .../test/browser_canvas-frontend-clear.js | 43 ++ .../browser_canvas-frontend-img-screenshots.js | 34 ++ .../browser_canvas-frontend-img-thumbnails-01.js | 65 +++ .../browser_canvas-frontend-img-thumbnails-02.js | 67 +++ .../test/browser_canvas-frontend-open.js | 41 ++ .../test/browser_canvas-frontend-record-01.js | 60 +++ .../test/browser_canvas-frontend-record-02.js | 73 +++ .../test/browser_canvas-frontend-record-03.js | 37 ++ .../test/browser_canvas-frontend-record-04.js | 34 ++ .../test/browser_canvas-frontend-reload-01.js | 55 +++ .../test/browser_canvas-frontend-reload-02.js | 70 +++ .../test/browser_canvas-frontend-slider-01.js | 39 ++ .../test/browser_canvas-frontend-slider-02.js | 97 ++++ .../browser_canvas-frontend-snapshot-select-01.js | 93 ++++ .../browser_canvas-frontend-snapshot-select-02.js | 30 ++ .../test/browser_canvas-frontend-stepping.js | 76 +++ .../test/browser_canvas-frontend-stop-01.js | 36 ++ .../test/browser_canvas-frontend-stop-02.js | 35 ++ .../test/browser_canvas-frontend-stop-03.js | 36 ++ .../test/browser_profiling-canvas.js | 45 ++ .../canvasdebugger/test/browser_profiling-webgl.js | 91 ++++ .../client/canvasdebugger/test/doc_no-canvas.html | 14 + .../client/canvasdebugger/test/doc_raf-begin.html | 36 ++ .../canvasdebugger/test/doc_raf-no-canvas.html | 18 + .../client/canvasdebugger/test/doc_settimeout.html | 37 ++ .../test/doc_simple-canvas-bitmasks.html | 34 ++ .../test/doc_simple-canvas-deep-stack.html | 46 ++ .../test/doc_simple-canvas-transparent.html | 37 ++ .../canvasdebugger/test/doc_simple-canvas.html | 37 ++ .../canvasdebugger/test/doc_webgl-bindings.html | 61 +++ .../canvasdebugger/test/doc_webgl-drawArrays.html | 187 ++++++++ .../test/doc_webgl-drawElements.html | 225 +++++++++ .../client/canvasdebugger/test/doc_webgl-enum.html | 34 ++ devtools/client/canvasdebugger/test/head.js | 305 ++++++++++++ 60 files changed, 5110 insertions(+) create mode 100644 devtools/client/canvasdebugger/callslist.js create mode 100644 devtools/client/canvasdebugger/canvasdebugger.js create mode 100644 devtools/client/canvasdebugger/canvasdebugger.xul create mode 100644 devtools/client/canvasdebugger/moz.build create mode 100644 devtools/client/canvasdebugger/panel.js create mode 100644 devtools/client/canvasdebugger/snapshotslist.js create mode 100644 devtools/client/canvasdebugger/test/.eslintrc.js create mode 100644 devtools/client/canvasdebugger/test/browser.ini create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-actor-test-01.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-actor-test-02.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-actor-test-03.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-actor-test-04.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-actor-test-05.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-actor-test-06.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-actor-test-07.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-actor-test-08.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-actor-test-09.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-actor-test-10.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-actor-test-11.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-actor-test-12.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-frontend-call-highlight.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-frontend-call-list.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-frontend-call-search.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-frontend-call-stack-01.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-frontend-call-stack-02.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-frontend-call-stack-03.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-frontend-clear.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-frontend-img-screenshots.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-frontend-img-thumbnails-01.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-frontend-img-thumbnails-02.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-frontend-open.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-frontend-record-01.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-frontend-record-02.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-frontend-record-03.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-frontend-record-04.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-frontend-reload-01.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-frontend-reload-02.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-frontend-slider-01.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-frontend-slider-02.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-frontend-snapshot-select-01.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-frontend-snapshot-select-02.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-frontend-stepping.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-frontend-stop-01.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-frontend-stop-02.js create mode 100644 devtools/client/canvasdebugger/test/browser_canvas-frontend-stop-03.js create mode 100644 devtools/client/canvasdebugger/test/browser_profiling-canvas.js create mode 100644 devtools/client/canvasdebugger/test/browser_profiling-webgl.js create mode 100644 devtools/client/canvasdebugger/test/doc_no-canvas.html create mode 100644 devtools/client/canvasdebugger/test/doc_raf-begin.html create mode 100644 devtools/client/canvasdebugger/test/doc_raf-no-canvas.html create mode 100644 devtools/client/canvasdebugger/test/doc_settimeout.html create mode 100644 devtools/client/canvasdebugger/test/doc_simple-canvas-bitmasks.html create mode 100644 devtools/client/canvasdebugger/test/doc_simple-canvas-deep-stack.html create mode 100644 devtools/client/canvasdebugger/test/doc_simple-canvas-transparent.html create mode 100644 devtools/client/canvasdebugger/test/doc_simple-canvas.html create mode 100644 devtools/client/canvasdebugger/test/doc_webgl-bindings.html create mode 100644 devtools/client/canvasdebugger/test/doc_webgl-drawArrays.html create mode 100644 devtools/client/canvasdebugger/test/doc_webgl-drawElements.html create mode 100644 devtools/client/canvasdebugger/test/doc_webgl-enum.html create mode 100644 devtools/client/canvasdebugger/test/head.js (limited to 'devtools/client/canvasdebugger') 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); + } + }); + } +}); diff --git a/devtools/client/canvasdebugger/canvasdebugger.js b/devtools/client/canvasdebugger/canvasdebugger.js new file mode 100644 index 000000000..c46cc6d0c --- /dev/null +++ b/devtools/client/canvasdebugger/canvasdebugger.js @@ -0,0 +1,341 @@ +/* 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"; + +var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm"); +const { SideMenuWidget } = require("resource://devtools/client/shared/widgets/SideMenuWidget.jsm"); +const promise = require("promise"); +const Services = require("Services"); +const EventEmitter = require("devtools/shared/event-emitter"); +const { CallWatcherFront } = require("devtools/shared/fronts/call-watcher"); +const { CanvasFront } = require("devtools/shared/fronts/canvas"); +const DevToolsUtils = require("devtools/shared/DevToolsUtils"); +const flags = require("devtools/shared/flags"); +const { LocalizationHelper } = require("devtools/shared/l10n"); +const { PluralForm } = require("devtools/shared/plural-form"); +const { Heritage, WidgetMethods, setNamedTimeout, clearNamedTimeout, + setConditionalTimeout } = require("devtools/client/shared/widgets/view-helpers"); + +const CANVAS_ACTOR_RECORDING_ATTEMPT = flags.testing ? 500 : 5000; + +const { Task } = require("devtools/shared/task"); + +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", + "resource://gre/modules/FileUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", + "resource://gre/modules/NetUtil.jsm"); + +XPCOMUtils.defineLazyGetter(this, "NetworkHelper", function () { + return require("devtools/shared/webconsole/network-helper"); +}); + +// The panel's window global is an EventEmitter firing the following events: +const EVENTS = { + // When the UI is reset from tab navigation. + UI_RESET: "CanvasDebugger:UIReset", + + // When all the animation frame snapshots are removed by the user. + SNAPSHOTS_LIST_CLEARED: "CanvasDebugger:SnapshotsListCleared", + + // When an animation frame snapshot starts/finishes being recorded, and + // whether it was completed succesfully or cancelled. + SNAPSHOT_RECORDING_STARTED: "CanvasDebugger:SnapshotRecordingStarted", + SNAPSHOT_RECORDING_FINISHED: "CanvasDebugger:SnapshotRecordingFinished", + SNAPSHOT_RECORDING_COMPLETED: "CanvasDebugger:SnapshotRecordingCompleted", + SNAPSHOT_RECORDING_CANCELLED: "CanvasDebugger:SnapshotRecordingCancelled", + + // When an animation frame snapshot was selected and all its data displayed. + SNAPSHOT_RECORDING_SELECTED: "CanvasDebugger:SnapshotRecordingSelected", + + // After all the function calls associated with an animation frame snapshot + // are displayed in the UI. + CALL_LIST_POPULATED: "CanvasDebugger:CallListPopulated", + + // After the stack associated with a call in an animation frame snapshot + // is displayed in the UI. + CALL_STACK_DISPLAYED: "CanvasDebugger:CallStackDisplayed", + + // After a screenshot associated with a call in an animation frame snapshot + // is displayed in the UI. + CALL_SCREENSHOT_DISPLAYED: "CanvasDebugger:ScreenshotDisplayed", + + // After all the thumbnails associated with an animation frame snapshot + // are displayed in the UI. + THUMBNAILS_DISPLAYED: "CanvasDebugger:ThumbnailsDisplayed", + + // When a source is shown in the JavaScript Debugger at a specific location. + SOURCE_SHOWN_IN_JS_DEBUGGER: "CanvasDebugger:SourceShownInJsDebugger", + SOURCE_NOT_FOUND_IN_JS_DEBUGGER: "CanvasDebugger:SourceNotFoundInJsDebugger" +}; +XPCOMUtils.defineConstant(this, "EVENTS", EVENTS); + +const HTML_NS = "http://www.w3.org/1999/xhtml"; +const STRINGS_URI = "devtools/client/locales/canvasdebugger.properties"; +const SHARED_STRINGS_URI = "devtools/client/locales/shared.properties"; + +const SNAPSHOT_START_RECORDING_DELAY = 10; // ms +const SNAPSHOT_DATA_EXPORT_MAX_BLOCK = 1000; // ms +const SNAPSHOT_DATA_DISPLAY_DELAY = 10; // ms +const SCREENSHOT_DISPLAY_DELAY = 100; // ms +const STACK_FUNC_INDENTATION = 14; // px + +// This identifier string is simply used to tentatively ascertain whether or not +// a JSON loaded from disk is actually something generated by this tool or not. +// It isn't, of course, a definitive verification, but a Good Enough™ +// approximation before continuing the import. Don't localize this. +const CALLS_LIST_SERIALIZER_IDENTIFIER = "Recorded Animation Frame Snapshot"; +const CALLS_LIST_SERIALIZER_VERSION = 1; +const CALLS_LIST_SLOW_SAVE_DELAY = 100; // ms + +/** + * The current target and the Canvas front, set by this tool's host. + */ +var gToolbox, gTarget, gFront; + +/** + * Initializes the canvas debugger controller and views. + */ +function startupCanvasDebugger() { + return promise.all([ + EventsHandler.initialize(), + SnapshotsListView.initialize(), + CallsListView.initialize() + ]); +} + +/** + * Destroys the canvas debugger controller and views. + */ +function shutdownCanvasDebugger() { + return promise.all([ + EventsHandler.destroy(), + SnapshotsListView.destroy(), + CallsListView.destroy() + ]); +} + +/** + * Functions handling target-related lifetime events. + */ +var EventsHandler = { + /** + * Listen for events emitted by the current tab target. + */ + initialize: function () { + // Make sure the backend is prepared to handle contexts. + // Since actors are created lazily on the first request to them, we need to send an + // early request to ensure the CallWatcherActor is running and watching for new window + // globals. + gFront.setup({ reload: false }); + + this._onTabNavigated = this._onTabNavigated.bind(this); + gTarget.on("will-navigate", this._onTabNavigated); + gTarget.on("navigate", this._onTabNavigated); + }, + + /** + * Remove events emitted by the current tab target. + */ + destroy: function () { + gTarget.off("will-navigate", this._onTabNavigated); + gTarget.off("navigate", this._onTabNavigated); + }, + + /** + * Called for each location change in the debugged tab. + */ + _onTabNavigated: function (event) { + if (event != "will-navigate") { + return; + } + + // Reset UI. + SnapshotsListView.empty(); + CallsListView.empty(); + + $("#record-snapshot").removeAttribute("checked"); + $("#record-snapshot").removeAttribute("disabled"); + $("#record-snapshot").hidden = false; + + $("#reload-notice").hidden = true; + $("#empty-notice").hidden = false; + $("#waiting-notice").hidden = true; + + $("#debugging-pane-contents").hidden = true; + $("#screenshot-container").hidden = true; + $("#snapshot-filmstrip").hidden = true; + + window.emit(EVENTS.UI_RESET); + } +}; + +/** + * Localization convenience methods. + */ +var L10N = new LocalizationHelper(STRINGS_URI); +var SHARED_L10N = new LocalizationHelper(SHARED_STRINGS_URI); + +/** + * Convenient way of emitting events from the panel window. + */ +EventEmitter.decorate(this); + +/** + * DOM query helpers. + */ +var $ = (selector, target = document) => target.querySelector(selector); +var $all = (selector, target = document) => target.querySelectorAll(selector); + +/** + * Gets the fileName part of a string which happens to be an URL. + */ +function getFileName(url) { + try { + let { fileName } = NetworkHelper.nsIURL(url); + return fileName || "/"; + } catch (e) { + // This doesn't look like a url, or nsIURL can't handle it. + return ""; + } +} + +/** + * Gets an image data object containing a buffer large enough to hold + * width * height pixels. + * + * This method avoids allocating memory and tries to reuse a common buffer + * as much as possible. + * + * @param number w + * The desired image data storage width. + * @param number h + * The desired image data storage height. + * @return ImageData + * The requested image data buffer. + */ +function getImageDataStorage(ctx, w, h) { + let storage = getImageDataStorage.cache; + if (storage && storage.width == w && storage.height == h) { + return storage; + } + return getImageDataStorage.cache = ctx.createImageData(w, h); +} + +// The cache used in the `getImageDataStorage` function. +getImageDataStorage.cache = null; + +/** + * Draws image data into a canvas. + * + * This method makes absolutely no assumptions about the canvas element + * dimensions, or pre-existing rendering. It's a dumb proxy that copies pixels. + * + * @param HTMLCanvasElement canvas + * The canvas element to put the image data into. + * @param number width + * The image data width. + * @param number height + * The image data height. + * @param array pixels + * An array buffer view of the image data. + * @param object options + * Additional options supported by this operation: + * - centered: specifies whether the image data should be centered + * when copied in the canvas; this is useful when the + * supplied pixels don't completely cover the canvas. + */ +function drawImage(canvas, width, height, pixels, options = {}) { + let ctx = canvas.getContext("2d"); + + // FrameSnapshot actors return "snapshot-image" type instances with just an + // empty pixel array if the source image is completely transparent. + if (pixels.length <= 1) { + ctx.clearRect(0, 0, canvas.width, canvas.height); + return; + } + + let imageData = getImageDataStorage(ctx, width, height); + imageData.data.set(pixels); + + if (options.centered) { + let left = (canvas.width - width) / 2; + let top = (canvas.height - height) / 2; + ctx.putImageData(imageData, left, top); + } else { + ctx.putImageData(imageData, 0, 0); + } +} + +/** + * Draws image data into a canvas, and sets that as the rendering source for + * an element with the specified id as the -moz-element background image. + * + * @param string id + * The id of the -moz-element background image. + * @param number width + * The image data width. + * @param number height + * The image data height. + * @param array pixels + * An array buffer view of the image data. + */ +function drawBackground(id, width, height, pixels) { + let canvas = document.createElementNS(HTML_NS, "canvas"); + canvas.width = width; + canvas.height = height; + + drawImage(canvas, width, height, pixels); + document.mozSetImageElement(id, canvas); + + // Used in tests. Not emitting an event because this shouldn't be "interesting". + if (window._onMozSetImageElement) { + window._onMozSetImageElement(pixels); + } +} + +/** + * Iterates forward to find the next draw call in a snapshot. + */ +function getNextDrawCall(calls, call) { + for (let i = calls.indexOf(call) + 1, len = calls.length; i < len; i++) { + let nextCall = calls[i]; + let name = nextCall.attachment.actor.name; + if (CanvasFront.DRAW_CALLS.has(name)) { + return nextCall; + } + } + return null; +} + +/** + * Iterates backwards to find the most recent screenshot for a function call + * in a snapshot loaded from disk. + */ +function getScreenshotFromCallLoadedFromDisk(calls, call) { + for (let i = calls.indexOf(call); i >= 0; i--) { + let prevCall = calls[i]; + let screenshot = prevCall.screenshot; + if (screenshot) { + return screenshot; + } + } + return CanvasFront.INVALID_SNAPSHOT_IMAGE; +} + +/** + * Iterates backwards to find the most recent thumbnail for a function call. + */ +function getThumbnailForCall(thumbnails, index) { + for (let i = thumbnails.length - 1; i >= 0; i--) { + let thumbnail = thumbnails[i]; + if (thumbnail.index <= index) { + return thumbnail; + } + } + return CanvasFront.INVALID_SNAPSHOT_IMAGE; +} diff --git a/devtools/client/canvasdebugger/canvasdebugger.xul b/devtools/client/canvasdebugger/canvasdebugger.xul new file mode 100644 index 000000000..f3003cbbe --- /dev/null +++ b/devtools/client/canvasdebugger/canvasdebugger.xul @@ -0,0 +1,135 @@ + + + + + + + + %canvasDebuggerDTD; +]> + + + + + + diff --git a/devtools/client/canvasdebugger/test/doc_raf-no-canvas.html b/devtools/client/canvasdebugger/test/doc_raf-no-canvas.html new file mode 100644 index 000000000..fa937623c --- /dev/null +++ b/devtools/client/canvasdebugger/test/doc_raf-no-canvas.html @@ -0,0 +1,18 @@ + + + + + + + Canvas inspector test page + + + + + + + diff --git a/devtools/client/canvasdebugger/test/doc_settimeout.html b/devtools/client/canvasdebugger/test/doc_settimeout.html new file mode 100644 index 000000000..57cfbdab0 --- /dev/null +++ b/devtools/client/canvasdebugger/test/doc_settimeout.html @@ -0,0 +1,37 @@ + + + + + + + Canvas inspector test page + + + + + + + + + diff --git a/devtools/client/canvasdebugger/test/doc_simple-canvas-bitmasks.html b/devtools/client/canvasdebugger/test/doc_simple-canvas-bitmasks.html new file mode 100644 index 000000000..bd5f67a6a --- /dev/null +++ b/devtools/client/canvasdebugger/test/doc_simple-canvas-bitmasks.html @@ -0,0 +1,34 @@ + + + + + + + Canvas inspector test page + + + + + + + + + diff --git a/devtools/client/canvasdebugger/test/doc_simple-canvas-deep-stack.html b/devtools/client/canvasdebugger/test/doc_simple-canvas-deep-stack.html new file mode 100644 index 000000000..f5ecc45d6 --- /dev/null +++ b/devtools/client/canvasdebugger/test/doc_simple-canvas-deep-stack.html @@ -0,0 +1,46 @@ + + + + + + + Canvas inspector test page + + + + + + + + + diff --git a/devtools/client/canvasdebugger/test/doc_simple-canvas-transparent.html b/devtools/client/canvasdebugger/test/doc_simple-canvas-transparent.html new file mode 100644 index 000000000..f8daf1e24 --- /dev/null +++ b/devtools/client/canvasdebugger/test/doc_simple-canvas-transparent.html @@ -0,0 +1,37 @@ + + + + + + + Canvas inspector test page + + + + + + + + + diff --git a/devtools/client/canvasdebugger/test/doc_simple-canvas.html b/devtools/client/canvasdebugger/test/doc_simple-canvas.html new file mode 100644 index 000000000..4fe6b587a --- /dev/null +++ b/devtools/client/canvasdebugger/test/doc_simple-canvas.html @@ -0,0 +1,37 @@ + + + + + + + Canvas inspector test page + + + + + + + + + diff --git a/devtools/client/canvasdebugger/test/doc_webgl-bindings.html b/devtools/client/canvasdebugger/test/doc_webgl-bindings.html new file mode 100644 index 000000000..eb1405359 --- /dev/null +++ b/devtools/client/canvasdebugger/test/doc_webgl-bindings.html @@ -0,0 +1,61 @@ + + + + + + + WebGL editor test page + + + + + + + + + diff --git a/devtools/client/canvasdebugger/test/doc_webgl-drawArrays.html b/devtools/client/canvasdebugger/test/doc_webgl-drawArrays.html new file mode 100644 index 000000000..7a6aea907 --- /dev/null +++ b/devtools/client/canvasdebugger/test/doc_webgl-drawArrays.html @@ -0,0 +1,187 @@ + + + + + + + WebGL editor test page + + + + + + + + + + \ No newline at end of file diff --git a/devtools/client/canvasdebugger/test/doc_webgl-drawElements.html b/devtools/client/canvasdebugger/test/doc_webgl-drawElements.html new file mode 100644 index 000000000..a8ba4a3e8 --- /dev/null +++ b/devtools/client/canvasdebugger/test/doc_webgl-drawElements.html @@ -0,0 +1,225 @@ + + + + + + + WebGL editor test page + + + + + + + + + + \ No newline at end of file diff --git a/devtools/client/canvasdebugger/test/doc_webgl-enum.html b/devtools/client/canvasdebugger/test/doc_webgl-enum.html new file mode 100644 index 000000000..f7f4d6d1e --- /dev/null +++ b/devtools/client/canvasdebugger/test/doc_webgl-enum.html @@ -0,0 +1,34 @@ + + + + + + + WebGL editor test page + + + + + + + + + diff --git a/devtools/client/canvasdebugger/test/head.js b/devtools/client/canvasdebugger/test/head.js new file mode 100644 index 000000000..a718551ce --- /dev/null +++ b/devtools/client/canvasdebugger/test/head.js @@ -0,0 +1,305 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +var { generateUUID } = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); +var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {}); + +var Services = require("Services"); +var promise = require("promise"); +var { gDevTools } = require("devtools/client/framework/devtools"); +var { DebuggerClient } = require("devtools/shared/client/main"); +var { DebuggerServer } = require("devtools/server/main"); +var { CallWatcherFront } = require("devtools/shared/fronts/call-watcher"); +var { CanvasFront } = require("devtools/shared/fronts/canvas"); +var { setTimeout } = require("sdk/timers"); +var DevToolsUtils = require("devtools/shared/DevToolsUtils"); +var flags = require("devtools/shared/flags"); +var { TargetFactory } = require("devtools/client/framework/target"); +var { Toolbox } = require("devtools/client/framework/toolbox"); +var { isWebGLSupported } = require("devtools/client/shared/webgl-utils"); +var mm = null; + +const FRAME_SCRIPT_UTILS_URL = "chrome://devtools/content/shared/frame-script-utils.js"; +const EXAMPLE_URL = "http://example.com/browser/devtools/client/canvasdebugger/test/"; +const SET_TIMEOUT_URL = EXAMPLE_URL + "doc_settimeout.html"; +const NO_CANVAS_URL = EXAMPLE_URL + "doc_no-canvas.html"; +const RAF_NO_CANVAS_URL = EXAMPLE_URL + "doc_raf-no-canvas.html"; +const SIMPLE_CANVAS_URL = EXAMPLE_URL + "doc_simple-canvas.html"; +const SIMPLE_BITMASKS_URL = EXAMPLE_URL + "doc_simple-canvas-bitmasks.html"; +const SIMPLE_CANVAS_TRANSPARENT_URL = EXAMPLE_URL + "doc_simple-canvas-transparent.html"; +const SIMPLE_CANVAS_DEEP_STACK_URL = EXAMPLE_URL + "doc_simple-canvas-deep-stack.html"; +const WEBGL_ENUM_URL = EXAMPLE_URL + "doc_webgl-enum.html"; +const WEBGL_BINDINGS_URL = EXAMPLE_URL + "doc_webgl-bindings.html"; +const WEBGL_DRAW_ARRAYS = EXAMPLE_URL + "doc_webgl-drawArrays.html"; +const WEBGL_DRAW_ELEMENTS = EXAMPLE_URL + "doc_webgl-drawElements.html"; +const RAF_BEGIN_URL = EXAMPLE_URL + "doc_raf-begin.html"; + +// Disable logging for all the tests. Both the debugger server and frontend will +// be affected by this pref. +var gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log"); +Services.prefs.setBoolPref("devtools.debugger.log", false); + +// All tests are asynchronous. +waitForExplicitFinish(); + +var gToolEnabled = Services.prefs.getBoolPref("devtools.canvasdebugger.enabled"); + +flags.testing = true; + +registerCleanupFunction(() => { + info("finish() was called, cleaning up..."); + flags.testing = false; + Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging); + Services.prefs.setBoolPref("devtools.canvasdebugger.enabled", gToolEnabled); + + // Some of yhese tests use a lot of memory due to GL contexts, so force a GC + // to help fragmentation. + info("Forcing GC after canvas debugger test."); + Cu.forceGC(); +}); + +/** + * Call manually in tests that use frame script utils after initializing + * the shader editor. Call after init but before navigating to different pages. + */ +function loadFrameScripts() { + mm = gBrowser.selectedBrowser.messageManager; + mm.loadFrameScript(FRAME_SCRIPT_UTILS_URL, false); +} + +function addTab(aUrl, aWindow) { + info("Adding tab: " + aUrl); + + let deferred = promise.defer(); + let targetWindow = aWindow || window; + let targetBrowser = targetWindow.gBrowser; + + targetWindow.focus(); + let tab = targetBrowser.selectedTab = targetBrowser.addTab(aUrl); + let linkedBrowser = tab.linkedBrowser; + + BrowserTestUtils.browserLoaded(linkedBrowser) + .then(function () { + info("Tab added and finished loading: " + aUrl); + deferred.resolve(tab); + }); + + return deferred.promise; +} + +function removeTab(aTab, aWindow) { + info("Removing tab."); + + let deferred = promise.defer(); + let targetWindow = aWindow || window; + let targetBrowser = targetWindow.gBrowser; + let tabContainer = targetBrowser.tabContainer; + + tabContainer.addEventListener("TabClose", function onClose(aEvent) { + tabContainer.removeEventListener("TabClose", onClose, false); + info("Tab removed and finished closing."); + deferred.resolve(); + }, false); + + targetBrowser.removeTab(aTab); + return deferred.promise; +} + +function handleError(aError) { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + finish(); +} + +var gRequiresWebGL = false; + +function ifTestingSupported() { + ok(false, "You need to define a 'ifTestingSupported' function."); + finish(); +} + +function ifTestingUnsupported() { + todo(false, "Skipping test because some required functionality isn't supported."); + finish(); +} + +function test() { + let generator = isTestingSupported() ? ifTestingSupported : ifTestingUnsupported; + Task.spawn(generator).then(null, handleError); +} + +function createCanvas() { + return document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); +} + +function isTestingSupported() { + if (!gRequiresWebGL) { + info("This test does not require WebGL support."); + return true; + } + + let supported = isWebGLSupported(document); + + info("This test requires WebGL support."); + info("Apparently, WebGL is" + (supported ? "" : " not") + " supported."); + return supported; +} + +function once(aTarget, aEventName, aUseCapture = false) { + info("Waiting for event: '" + aEventName + "' on " + aTarget + "."); + + let deferred = promise.defer(); + + for (let [add, remove] of [ + ["on", "off"], // Use event emitter before DOM events for consistency + ["addEventListener", "removeEventListener"], + ["addListener", "removeListener"] + ]) { + if ((add in aTarget) && (remove in aTarget)) { + aTarget[add](aEventName, function onEvent(...aArgs) { + info("Got event: '" + aEventName + "' on " + aTarget + "."); + aTarget[remove](aEventName, onEvent, aUseCapture); + deferred.resolve(...aArgs); + }, aUseCapture); + break; + } + } + + return deferred.promise; +} + +function waitForTick() { + let deferred = promise.defer(); + executeSoon(deferred.resolve); + return deferred.promise; +} + +function navigateInHistory(aTarget, aDirection, aWaitForTargetEvent = "navigate") { + executeSoon(() => content.history[aDirection]()); + return once(aTarget, aWaitForTargetEvent); +} + +function navigate(aTarget, aUrl, aWaitForTargetEvent = "navigate") { + executeSoon(() => aTarget.activeTab.navigateTo(aUrl)); + return once(aTarget, aWaitForTargetEvent); +} + +function reload(aTarget, aWaitForTargetEvent = "navigate") { + executeSoon(() => aTarget.activeTab.reload()); + return once(aTarget, aWaitForTargetEvent); +} + +function initServer() { + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } +} + +function initCallWatcherBackend(aUrl) { + info("Initializing a call watcher front."); + initServer(); + + return Task.spawn(function* () { + let tab = yield addTab(aUrl); + let target = TargetFactory.forTab(tab); + + yield target.makeRemote(); + + let front = new CallWatcherFront(target.client, target.form); + return { target, front }; + }); +} + +function initCanvasDebuggerBackend(aUrl) { + info("Initializing a canvas debugger front."); + initServer(); + + return Task.spawn(function* () { + let tab = yield addTab(aUrl); + let target = TargetFactory.forTab(tab); + + yield target.makeRemote(); + + let front = new CanvasFront(target.client, target.form); + return { target, front }; + }); +} + +function initCanvasDebuggerFrontend(aUrl) { + info("Initializing a canvas debugger pane."); + + return Task.spawn(function* () { + let tab = yield addTab(aUrl); + let target = TargetFactory.forTab(tab); + + yield target.makeRemote(); + + Services.prefs.setBoolPref("devtools.canvasdebugger.enabled", true); + let toolbox = yield gDevTools.showToolbox(target, "canvasdebugger"); + let panel = toolbox.getCurrentPanel(); + return { target, panel }; + }); +} + +function teardown({target}) { + info("Destroying the specified canvas debugger."); + + let {tab} = target; + return gDevTools.closeToolbox(target).then(() => { + removeTab(tab); + }); +} + +/** + * Takes a string `script` and evaluates it directly in the content + * in potentially a different process. + */ +function evalInDebuggee(script) { + let deferred = promise.defer(); + + if (!mm) { + throw new Error("`loadFrameScripts()` must be called when using MessageManager."); + } + + let id = generateUUID().toString(); + mm.sendAsyncMessage("devtools:test:eval", { script: script, id: id }); + mm.addMessageListener("devtools:test:eval:response", handler); + + function handler({ data }) { + if (id !== data.id) { + return; + } + + mm.removeMessageListener("devtools:test:eval:response", handler); + deferred.resolve(data.value); + } + + return deferred.promise; +} + +function getSourceActor(aSources, aURL) { + let item = aSources.getItemForAttachment(a => a.source.url === aURL); + return item ? item.value : null; +} + +/** + * Waits until a predicate returns true. + * + * @param function predicate + * Invoked once in a while until it returns true. + * @param number interval [optional] + * How often the predicate is invoked, in milliseconds. + */ +function* waitUntil(predicate, interval = 10) { + if (yield predicate()) { + return Promise.resolve(true); + } + let deferred = Promise.defer(); + setTimeout(function () { + waitUntil(predicate).then(() => deferred.resolve(true)); + }, interval); + return deferred.promise; +} -- cgit v1.2.3