123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527 |
- /* 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);
- }
- });
- }
- });
|