diff options
Diffstat (limited to 'chromium/third_party/WebKit/Source/devtools/front_end/profiler/CanvasProfileView.js')
-rw-r--r-- | chromium/third_party/WebKit/Source/devtools/front_end/profiler/CanvasProfileView.js | 1276 |
1 files changed, 1276 insertions, 0 deletions
diff --git a/chromium/third_party/WebKit/Source/devtools/front_end/profiler/CanvasProfileView.js b/chromium/third_party/WebKit/Source/devtools/front_end/profiler/CanvasProfileView.js new file mode 100644 index 00000000000..edaeedb3cde --- /dev/null +++ b/chromium/third_party/WebKit/Source/devtools/front_end/profiler/CanvasProfileView.js @@ -0,0 +1,1276 @@ +/* + * Copyright (C) 2013 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @constructor + * @extends {WebInspector.VBox} + * @param {!WebInspector.CanvasProfileHeader} profile + */ +WebInspector.CanvasProfileView = function(profile) +{ + WebInspector.VBox.call(this); + this.registerRequiredCSS("canvasProfiler.css"); + this.element.classList.add("canvas-profile-view"); + + this._profile = profile; + this._traceLogId = profile.traceLogId(); + this._traceLogPlayer = /** @type {!WebInspector.CanvasTraceLogPlayerProxy} */ (profile.traceLogPlayer()); + this._linkifier = new WebInspector.Linkifier(); + + this._replayInfoSplitView = new WebInspector.SplitView(true, true, "canvasProfileViewReplaySplitViewState", 0.34); + this._replayInfoSplitView.show(this.element); + + this._imageSplitView = new WebInspector.SplitView(false, true, "canvasProfileViewSplitViewState", 300); + this._imageSplitView.show(this._replayInfoSplitView.mainElement()); + + var replayImageContainerView = new WebInspector.VBox(); + replayImageContainerView.setMinimumSize(50, 28); + replayImageContainerView.show(this._imageSplitView.mainElement()); + + // NOTE: The replayImageContainer can NOT be a flex div (e.g. VBox or SplitView elements)! + var replayImageContainer = replayImageContainerView.element.createChild("div"); + replayImageContainer.id = "canvas-replay-image-container"; + this._replayImageElement = replayImageContainer.createChild("img", "canvas-replay-image"); + this._debugInfoElement = replayImageContainer.createChild("div", "canvas-debug-info hidden"); + this._spinnerIcon = replayImageContainer.createChild("div", "spinner-icon small hidden"); + + var replayLogContainerView = new WebInspector.VBox(); + replayLogContainerView.setMinimumSize(22, 22); + replayLogContainerView.show(this._imageSplitView.sidebarElement()); + + var replayLogContainer = replayLogContainerView.element; + var controlsContainer = replayLogContainer.createChild("div", "status-bar"); + var logGridContainer = replayLogContainer.createChild("div", "canvas-replay-log"); + + this._createControlButton(controlsContainer, "canvas-replay-first-step", WebInspector.UIString("First call."), this._onReplayFirstStepClick.bind(this)); + this._createControlButton(controlsContainer, "canvas-replay-prev-step", WebInspector.UIString("Previous call."), this._onReplayStepClick.bind(this, false)); + this._createControlButton(controlsContainer, "canvas-replay-next-step", WebInspector.UIString("Next call."), this._onReplayStepClick.bind(this, true)); + this._createControlButton(controlsContainer, "canvas-replay-prev-draw", WebInspector.UIString("Previous drawing call."), this._onReplayDrawingCallClick.bind(this, false)); + this._createControlButton(controlsContainer, "canvas-replay-next-draw", WebInspector.UIString("Next drawing call."), this._onReplayDrawingCallClick.bind(this, true)); + this._createControlButton(controlsContainer, "canvas-replay-last-step", WebInspector.UIString("Last call."), this._onReplayLastStepClick.bind(this)); + + this._replayContextSelector = new WebInspector.StatusBarComboBox(this._onReplayContextChanged.bind(this)); + this._replayContextSelector.createOption(WebInspector.UIString("<screenshot auto>"), WebInspector.UIString("Show screenshot of the last replayed resource."), ""); + controlsContainer.appendChild(this._replayContextSelector.element); + + this._installReplayInfoSidebarWidgets(controlsContainer); + + this._replayStateView = new WebInspector.CanvasReplayStateView(this._traceLogPlayer); + this._replayStateView.show(this._replayInfoSplitView.sidebarElement()); + + /** @type {!Object.<string, boolean>} */ + this._replayContexts = {}; + + var columns = [ + {title: "#", sortable: false, width: "5%"}, + {title: WebInspector.UIString("Call"), sortable: false, width: "75%", disclosure: true}, + {title: WebInspector.UIString("Location"), sortable: false, width: "20%"} + ]; + + this._logGrid = new WebInspector.DataGrid(columns); + this._logGrid.element.classList.add("fill"); + this._logGrid.show(logGridContainer); + this._logGrid.addEventListener(WebInspector.DataGrid.Events.SelectedNode, this._replayTraceLog, this); + + this.element.addEventListener("mousedown", this._onMouseClick.bind(this), true); + + this._popoverHelper = new WebInspector.ObjectPopoverHelper(this.element, this._popoverAnchor.bind(this), this._resolveObjectForPopover.bind(this), this._onHidePopover.bind(this), true); + this._popoverHelper.setRemoteObjectFormatter(this._hexNumbersFormatter.bind(this)); + + this._requestTraceLog(0); +} + +/** + * @const + * @type {number} + */ +WebInspector.CanvasProfileView.TraceLogPollingInterval = 500; + +WebInspector.CanvasProfileView.prototype = { + dispose: function() + { + this._linkifier.reset(); + }, + + get statusBarItems() + { + return []; + }, + + get profile() + { + return this._profile; + }, + + /** + * @override + * @return {!Array.<!Element>} + */ + elementsToRestoreScrollPositionsFor: function() + { + return [this._logGrid.scrollContainer]; + }, + + /** + * @param {!Element} controlsContainer + */ + _installReplayInfoSidebarWidgets: function(controlsContainer) + { + this._replayInfoResizeWidgetElement = controlsContainer.createChild("div", "resizer-widget"); + this._replayInfoSplitView.addEventListener(WebInspector.SplitView.Events.ShowModeChanged, this._updateReplayInfoResizeWidget, this); + this._updateReplayInfoResizeWidget(); + this._replayInfoSplitView.installResizer(this._replayInfoResizeWidgetElement); + + this._toggleReplayStateSidebarButton = this._replayInfoSplitView.createShowHideSidebarButton("sidebar", "canvas-sidebar-show-hide-button"); + + controlsContainer.appendChild(this._toggleReplayStateSidebarButton.element); + this._replayInfoSplitView.hideSidebar(); + }, + + _updateReplayInfoResizeWidget: function() + { + this._replayInfoResizeWidgetElement.classList.toggle("hidden", this._replayInfoSplitView.showMode() !== WebInspector.SplitView.ShowMode.Both); + }, + + /** + * @param {?Event} event + */ + _onMouseClick: function(event) + { + var resourceLinkElement = event.target.enclosingNodeOrSelfWithClass("canvas-formatted-resource"); + if (resourceLinkElement) { + this._replayInfoSplitView.showBoth(); + this._replayStateView.selectResource(resourceLinkElement.__resourceId); + event.consume(true); + return; + } + if (event.target.enclosingNodeOrSelfWithClass("webkit-html-resource-link")) + event.consume(false); + }, + + /** + * @param {!Element} parent + * @param {string} className + * @param {string} title + * @param {function(this:WebInspector.CanvasProfileView)} clickCallback + */ + _createControlButton: function(parent, className, title, clickCallback) + { + var button = new WebInspector.StatusBarButton(title, className + " canvas-replay-button"); + parent.appendChild(button.element); + + button.makeLongClickEnabled(); + button.addEventListener("click", clickCallback, this); + button.addEventListener("longClickDown", clickCallback, this); + button.addEventListener("longClickPress", clickCallback, this); + }, + + _onReplayContextChanged: function() + { + var selectedContextId = this._replayContextSelector.selectedOption().value; + + /** + * @param {?CanvasAgent.ResourceState} resourceState + * @this {WebInspector.CanvasProfileView} + */ + function didReceiveResourceState(resourceState) + { + this._enableWaitIcon(false); + if (selectedContextId !== this._replayContextSelector.selectedOption().value) + return; + var imageURL = (resourceState && resourceState.imageURL) || ""; + this._replayImageElement.src = imageURL; + this._replayImageElement.style.visibility = imageURL ? "" : "hidden"; + } + + this._enableWaitIcon(true); + this._traceLogPlayer.getResourceState(selectedContextId, didReceiveResourceState.bind(this)); + }, + + /** + * @param {boolean} forward + */ + _onReplayStepClick: function(forward) + { + var selectedNode = this._logGrid.selectedNode; + if (!selectedNode) + return; + var nextNode = selectedNode; + do { + nextNode = forward ? nextNode.traverseNextNode(false) : nextNode.traversePreviousNode(false); + } while (nextNode && typeof nextNode.index !== "number"); + (nextNode || selectedNode).revealAndSelect(); + }, + + /** + * @param {boolean} forward + */ + _onReplayDrawingCallClick: function(forward) + { + var selectedNode = this._logGrid.selectedNode; + if (!selectedNode) + return; + var nextNode = selectedNode; + while (nextNode) { + var sibling = forward ? nextNode.nextSibling : nextNode.previousSibling; + if (sibling) { + nextNode = sibling; + if (nextNode.hasChildren || nextNode.call.isDrawingCall) + break; + } else { + nextNode = nextNode.parent; + if (!forward) + break; + } + } + if (!nextNode && forward) + this._onReplayLastStepClick(); + else + (nextNode || selectedNode).revealAndSelect(); + }, + + _onReplayFirstStepClick: function() + { + var firstNode = this._logGrid.rootNode().children[0]; + if (firstNode) + firstNode.revealAndSelect(); + }, + + _onReplayLastStepClick: function() + { + var lastNode = this._logGrid.rootNode().children.peekLast(); + if (!lastNode) + return; + while (lastNode.expanded) { + var lastChild = lastNode.children.peekLast(); + if (!lastChild) + break; + lastNode = lastChild; + } + lastNode.revealAndSelect(); + }, + + /** + * @param {boolean} enable + */ + _enableWaitIcon: function(enable) + { + this._spinnerIcon.classList.toggle("hidden", !enable); + this._debugInfoElement.classList.toggle("hidden", enable); + }, + + _replayTraceLog: function() + { + if (this._pendingReplayTraceLogEvent) + return; + var index = this._selectedCallIndex(); + if (index === -1 || index === this._lastReplayCallIndex) + return; + this._lastReplayCallIndex = index; + this._pendingReplayTraceLogEvent = true; + + /** + * @param {?CanvasAgent.ResourceState} resourceState + * @param {number} replayTime + * @this {WebInspector.CanvasProfileView} + */ + function didReplayTraceLog(resourceState, replayTime) + { + delete this._pendingReplayTraceLogEvent; + this._enableWaitIcon(false); + + this._debugInfoElement.textContent = WebInspector.UIString("Replay time: %s", Number.secondsToString(replayTime / 1000, true)); + this._onReplayContextChanged(); + + if (index !== this._selectedCallIndex()) + this._replayTraceLog(); + } + this._enableWaitIcon(true); + this._traceLogPlayer.replayTraceLog(index, didReplayTraceLog.bind(this)); + }, + + /** + * @param {number} offset + */ + _requestTraceLog: function(offset) + { + /** + * @param {?CanvasAgent.TraceLog} traceLog + * @this {WebInspector.CanvasProfileView} + */ + function didReceiveTraceLog(traceLog) + { + this._enableWaitIcon(false); + if (!traceLog) + return; + var callNodes = []; + var calls = traceLog.calls; + var index = traceLog.startOffset; + for (var i = 0, n = calls.length; i < n; ++i) + callNodes.push(this._createCallNode(index++, calls[i])); + var contexts = traceLog.contexts; + for (var i = 0, n = contexts.length; i < n; ++i) { + var contextId = contexts[i].resourceId || ""; + var description = contexts[i].description || ""; + if (this._replayContexts[contextId]) + continue; + this._replayContexts[contextId] = true; + this._replayContextSelector.createOption(description, WebInspector.UIString("Show screenshot of this context's canvas."), contextId); + } + this._appendCallNodes(callNodes); + if (traceLog.alive) + setTimeout(this._requestTraceLog.bind(this, index), WebInspector.CanvasProfileView.TraceLogPollingInterval); + else + this._flattenSingleFrameNode(); + this._profile._updateCapturingStatus(traceLog); + this._onReplayLastStepClick(); // Automatically replay the last step. + } + this._enableWaitIcon(true); + this._traceLogPlayer.getTraceLog(offset, undefined, didReceiveTraceLog.bind(this)); + }, + + /** + * @return {number} + */ + _selectedCallIndex: function() + { + var node = this._logGrid.selectedNode; + return node ? this._peekLastRecursively(node).index : -1; + }, + + /** + * @param {!WebInspector.DataGridNode} node + * @return {!WebInspector.DataGridNode} + */ + _peekLastRecursively: function(node) + { + var lastChild; + while ((lastChild = node.children.peekLast())) + node = lastChild; + return node; + }, + + /** + * @param {!Array.<!WebInspector.DataGridNode>} callNodes + */ + _appendCallNodes: function(callNodes) + { + var rootNode = this._logGrid.rootNode(); + var frameNode = rootNode.children.peekLast(); + if (frameNode && this._peekLastRecursively(frameNode).call.isFrameEndCall) + frameNode = null; + for (var i = 0, n = callNodes.length; i < n; ++i) { + if (!frameNode) { + var index = rootNode.children.length; + var data = {}; + data[0] = ""; + data[1] = WebInspector.UIString("Frame #%d", index + 1); + data[2] = ""; + frameNode = new WebInspector.DataGridNode(data); + frameNode.selectable = true; + rootNode.appendChild(frameNode); + } + var nextFrameCallIndex = i + 1; + while (nextFrameCallIndex < n && !callNodes[nextFrameCallIndex - 1].call.isFrameEndCall) + ++nextFrameCallIndex; + this._appendCallNodesToFrameNode(frameNode, callNodes, i, nextFrameCallIndex); + i = nextFrameCallIndex - 1; + frameNode = null; + } + }, + + /** + * @param {!WebInspector.DataGridNode} frameNode + * @param {!Array.<!WebInspector.DataGridNode>} callNodes + * @param {number} fromIndex + * @param {number} toIndex not inclusive + */ + _appendCallNodesToFrameNode: function(frameNode, callNodes, fromIndex, toIndex) + { + var self = this; + function appendDrawCallGroup() + { + var index = self._drawCallGroupsCount || 0; + var data = {}; + data[0] = ""; + data[1] = WebInspector.UIString("Draw call group #%d", index + 1); + data[2] = ""; + var node = new WebInspector.DataGridNode(data); + node.selectable = true; + self._drawCallGroupsCount = index + 1; + frameNode.appendChild(node); + return node; + } + + function splitDrawCallGroup(drawCallGroup) + { + var splitIndex = 0; + var splitNode; + while ((splitNode = drawCallGroup.children[splitIndex])) { + if (splitNode.call.isDrawingCall) + break; + ++splitIndex; + } + var newDrawCallGroup = appendDrawCallGroup(); + var lastNode; + while ((lastNode = drawCallGroup.children[splitIndex + 1])) + newDrawCallGroup.appendChild(lastNode); + return newDrawCallGroup; + } + + var drawCallGroup = frameNode.children.peekLast(); + var groupHasDrawCall = false; + if (drawCallGroup) { + for (var i = 0, n = drawCallGroup.children.length; i < n; ++i) { + if (drawCallGroup.children[i].call.isDrawingCall) { + groupHasDrawCall = true; + break; + } + } + } else + drawCallGroup = appendDrawCallGroup(); + + for (var i = fromIndex; i < toIndex; ++i) { + var node = callNodes[i]; + drawCallGroup.appendChild(node); + if (node.call.isDrawingCall) { + if (groupHasDrawCall) + drawCallGroup = splitDrawCallGroup(drawCallGroup); + else + groupHasDrawCall = true; + } + } + }, + + /** + * @param {number} index + * @param {!CanvasAgent.Call} call + * @return {!WebInspector.DataGridNode} + */ + _createCallNode: function(index, call) + { + var callViewElement = document.createElement("div"); + + var data = {}; + data[0] = index + 1; + data[1] = callViewElement; + data[2] = ""; + if (call.sourceURL) { + // FIXME(62725): stack trace line/column numbers are one-based. + var lineNumber = Math.max(0, call.lineNumber - 1) || 0; + var columnNumber = Math.max(0, call.columnNumber - 1) || 0; + data[2] = this._linkifier.linkifyLocation(this.profile.target(), call.sourceURL, lineNumber, columnNumber); + } + + callViewElement.createChild("span", "canvas-function-name").textContent = call.functionName || "context." + call.property; + + if (call.arguments) { + callViewElement.createTextChild("("); + for (var i = 0, n = call.arguments.length; i < n; ++i) { + var argument = /** @type {!CanvasAgent.CallArgument} */ (call.arguments[i]); + if (i) + callViewElement.createTextChild(", "); + var element = WebInspector.CanvasProfileDataGridHelper.createCallArgumentElement(argument); + element.__argumentIndex = i; + callViewElement.appendChild(element); + } + callViewElement.createTextChild(")"); + } else if (call.value) { + callViewElement.createTextChild(" = "); + callViewElement.appendChild(WebInspector.CanvasProfileDataGridHelper.createCallArgumentElement(call.value)); + } + + if (call.result) { + callViewElement.createTextChild(" => "); + callViewElement.appendChild(WebInspector.CanvasProfileDataGridHelper.createCallArgumentElement(call.result)); + } + + var node = new WebInspector.DataGridNode(data); + node.index = index; + node.selectable = true; + node.call = call; + return node; + }, + + _popoverAnchor: function(element, event) + { + var argumentElement = element.enclosingNodeOrSelfWithClass("canvas-call-argument"); + if (!argumentElement || argumentElement.__suppressPopover) + return null; + return argumentElement; + }, + + _resolveObjectForPopover: function(argumentElement, showCallback, objectGroupName) + { + /** + * @param {?Protocol.Error} error + * @param {!RuntimeAgent.RemoteObject=} result + * @param {!CanvasAgent.ResourceState=} resourceState + * @this {WebInspector.CanvasProfileView} + */ + function showObjectPopover(error, result, resourceState) + { + if (error) + return; + + // FIXME: handle resourceState also + if (!result) + return; + + this._popoverAnchorElement = argumentElement.cloneNode(true); + this._popoverAnchorElement.classList.add("canvas-popover-anchor"); + this._popoverAnchorElement.classList.add("source-frame-eval-expression"); + argumentElement.parentElement.appendChild(this._popoverAnchorElement); + + var diffLeft = this._popoverAnchorElement.boxInWindow().x - argumentElement.boxInWindow().x; + this._popoverAnchorElement.style.left = this._popoverAnchorElement.offsetLeft - diffLeft + "px"; + + showCallback(WebInspector.runtimeModel.createRemoteObject(result), false, this._popoverAnchorElement); + } + + var evalResult = argumentElement.__evalResult; + if (evalResult) + showObjectPopover.call(this, null, evalResult); + else { + var dataGridNode = this._logGrid.dataGridNodeFromNode(argumentElement); + if (!dataGridNode || typeof dataGridNode.index !== "number") { + this._popoverHelper.hidePopover(); + return; + } + var callIndex = dataGridNode.index; + var argumentIndex = argumentElement.__argumentIndex; + if (typeof argumentIndex !== "number") + argumentIndex = -1; + CanvasAgent.evaluateTraceLogCallArgument(this._traceLogId, callIndex, argumentIndex, objectGroupName, showObjectPopover.bind(this)); + } + }, + + /** + * @param {!WebInspector.RemoteObject} object + * @return {string} + */ + _hexNumbersFormatter: function(object) + { + if (object.type === "number") { + // Show enum values in hex with min length of 4 (e.g. 0x0012). + var str = "0000" + Number(object.description).toString(16).toUpperCase(); + str = str.replace(/^0+(.{4,})$/, "$1"); + return "0x" + str; + } + return object.description || ""; + }, + + _onHidePopover: function() + { + if (this._popoverAnchorElement) { + this._popoverAnchorElement.remove() + delete this._popoverAnchorElement; + } + }, + + _flattenSingleFrameNode: function() + { + var rootNode = this._logGrid.rootNode(); + if (rootNode.children.length !== 1) + return; + var frameNode = rootNode.children[0]; + while (frameNode.children[0]) + rootNode.appendChild(frameNode.children[0]); + rootNode.removeChild(frameNode); + }, + + __proto__: WebInspector.VBox.prototype +} + +/** + * @constructor + * @extends {WebInspector.ProfileType} + */ +WebInspector.CanvasProfileType = function() +{ + WebInspector.ProfileType.call(this, WebInspector.CanvasProfileType.TypeId, WebInspector.UIString("Capture Canvas Frame")); + this._recording = false; + this._lastProfileHeader = null; + + this._capturingModeSelector = new WebInspector.StatusBarComboBox(this._dispatchViewUpdatedEvent.bind(this)); + this._capturingModeSelector.element.title = WebInspector.UIString("Canvas capture mode."); + this._capturingModeSelector.createOption(WebInspector.UIString("Single Frame"), WebInspector.UIString("Capture a single canvas frame."), ""); + this._capturingModeSelector.createOption(WebInspector.UIString("Consecutive Frames"), WebInspector.UIString("Capture consecutive canvas frames."), "1"); + + /** @type {!Object.<string, !Element>} */ + this._frameOptions = {}; + + /** @type {!Object.<string, boolean>} */ + this._framesWithCanvases = {}; + + this._frameSelector = new WebInspector.StatusBarComboBox(this._dispatchViewUpdatedEvent.bind(this)); + this._frameSelector.element.title = WebInspector.UIString("Frame containing the canvases to capture."); + this._frameSelector.element.classList.add("hidden"); + + this._target = /** @type {!WebInspector.Target} */ (WebInspector.targetManager.activeTarget()); + this._target.resourceTreeModel.frames().forEach(this._addFrame, this); + this._target.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.FrameAdded, this._frameAdded, this); + this._target.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.FrameDetached, this._frameRemoved, this); + + this._dispatcher = new WebInspector.CanvasDispatcher(this); + this._canvasAgentEnabled = false; + + this._decorationElement = document.createElement("div"); + this._decorationElement.className = "profile-canvas-decoration"; + this._updateDecorationElement(); +} + +WebInspector.CanvasProfileType.TypeId = "CANVAS_PROFILE"; + +WebInspector.CanvasProfileType.prototype = { + get statusBarItems() + { + return [this._capturingModeSelector.element, this._frameSelector.element]; + }, + + get buttonTooltip() + { + if (this._isSingleFrameMode()) + return WebInspector.UIString("Capture next canvas frame."); + else + return this._recording ? WebInspector.UIString("Stop capturing canvas frames.") : WebInspector.UIString("Start capturing canvas frames."); + }, + + /** + * @override + * @return {boolean} + */ + buttonClicked: function() + { + if (!this._canvasAgentEnabled) + return false; + if (this._recording) { + this._recording = false; + this._stopFrameCapturing(); + } else if (this._isSingleFrameMode()) { + this._recording = false; + this._runSingleFrameCapturing(); + } else { + this._recording = true; + this._startFrameCapturing(); + } + return this._recording; + }, + + _runSingleFrameCapturing: function() + { + var frameId = this._selectedFrameId(); + this._target.profilingLock.acquire(); + CanvasAgent.captureFrame(frameId, this._didStartCapturingFrame.bind(this, frameId)); + this._target.profilingLock.release(); + }, + + _startFrameCapturing: function() + { + var frameId = this._selectedFrameId(); + this._target.profilingLock.acquire(); + CanvasAgent.startCapturing(frameId, this._didStartCapturingFrame.bind(this, frameId)); + }, + + _stopFrameCapturing: function() + { + if (!this._lastProfileHeader) { + this._target.profilingLock.release(); + return; + } + var profileHeader = this._lastProfileHeader; + var traceLogId = profileHeader.traceLogId(); + this._lastProfileHeader = null; + function didStopCapturing() + { + profileHeader._updateCapturingStatus(); + } + CanvasAgent.stopCapturing(traceLogId, didStopCapturing); + this._target.profilingLock.release(); + }, + + /** + * @param {string|undefined} frameId + * @param {?Protocol.Error} error + * @param {!CanvasAgent.TraceLogId} traceLogId + */ + _didStartCapturingFrame: function(frameId, error, traceLogId) + { + if (error || this._lastProfileHeader && this._lastProfileHeader.traceLogId() === traceLogId) + return; + var profileHeader = new WebInspector.CanvasProfileHeader(this._target, this, traceLogId, frameId); + this._lastProfileHeader = profileHeader; + this.addProfile(profileHeader); + profileHeader._updateCapturingStatus(); + }, + + get treeItemTitle() + { + return WebInspector.UIString("CANVAS PROFILE"); + }, + + get description() + { + return WebInspector.UIString("Canvas calls instrumentation"); + }, + + /** + * @override + * @return {!Element} + */ + decorationElement: function() + { + return this._decorationElement; + }, + + /** + * @override + * @param {!WebInspector.ProfileHeader} profile + */ + removeProfile: function(profile) + { + WebInspector.ProfileType.prototype.removeProfile.call(this, profile); + if (this._recording && profile === this._lastProfileHeader) + this._recording = false; + }, + + /** + * @param {boolean=} forcePageReload + */ + _updateDecorationElement: function(forcePageReload) + { + this._decorationElement.removeChildren(); + this._decorationElement.createChild("div", "warning-icon-small"); + this._decorationElement.appendChild(document.createTextNode(this._canvasAgentEnabled ? WebInspector.UIString("Canvas Profiler is enabled.") : WebInspector.UIString("Canvas Profiler is disabled."))); + var button = this._decorationElement.createChild("button"); + button.type = "button"; + button.textContent = this._canvasAgentEnabled ? WebInspector.UIString("Disable") : WebInspector.UIString("Enable"); + button.addEventListener("click", this._onProfilerEnableButtonClick.bind(this, !this._canvasAgentEnabled), false); + + var target = this._target; + /** + * @param {?Protocol.Error} error + * @param {boolean} result + */ + function hasUninstrumentedCanvasesCallback(error, result) + { + if (error || result) + target.resourceTreeModel.reloadPage(); + } + + if (forcePageReload) { + if (this._canvasAgentEnabled) { + CanvasAgent.hasUninstrumentedCanvases(hasUninstrumentedCanvasesCallback); + } else { + for (var frameId in this._framesWithCanvases) { + if (this._framesWithCanvases.hasOwnProperty(frameId)) { + target.resourceTreeModel.reloadPage(); + break; + } + } + } + } + }, + + /** + * @param {boolean} enable + */ + _onProfilerEnableButtonClick: function(enable) + { + if (this._canvasAgentEnabled === enable) + return; + + /** + * @param {?Protocol.Error} error + * @this {WebInspector.CanvasProfileType} + */ + function callback(error) + { + if (error) + return; + this._canvasAgentEnabled = enable; + this._updateDecorationElement(true); + this._dispatchViewUpdatedEvent(); + } + if (enable) + CanvasAgent.enable(callback.bind(this)); + else + CanvasAgent.disable(callback.bind(this)); + }, + + /** + * @return {boolean} + */ + _isSingleFrameMode: function() + { + return !this._capturingModeSelector.selectedOption().value; + }, + + /** + * @param {!WebInspector.Event} event + */ + _frameAdded: function(event) + { + var frame = /** @type {!WebInspector.ResourceTreeFrame} */ (event.data); + this._addFrame(frame); + }, + + /** + * @param {!WebInspector.ResourceTreeFrame} frame + */ + _addFrame: function(frame) + { + var frameId = frame.id; + var option = document.createElement("option"); + option.text = frame.displayName(); + option.title = frame.url; + option.value = frameId; + + this._frameOptions[frameId] = option; + + if (this._framesWithCanvases[frameId]) { + this._frameSelector.addOption(option); + this._dispatchViewUpdatedEvent(); + } + }, + + /** + * @param {!WebInspector.Event} event + */ + _frameRemoved: function(event) + { + var frame = /** @type {!WebInspector.ResourceTreeFrame} */ (event.data); + var frameId = frame.id; + var option = this._frameOptions[frameId]; + if (option && this._framesWithCanvases[frameId]) { + this._frameSelector.removeOption(option); + this._dispatchViewUpdatedEvent(); + } + delete this._frameOptions[frameId]; + delete this._framesWithCanvases[frameId]; + }, + + /** + * @param {string} frameId + */ + _contextCreated: function(frameId) + { + if (this._framesWithCanvases[frameId]) + return; + this._framesWithCanvases[frameId] = true; + var option = this._frameOptions[frameId]; + if (option) { + this._frameSelector.addOption(option); + this._dispatchViewUpdatedEvent(); + } + }, + + /** + * @param {!PageAgent.FrameId=} frameId + * @param {!CanvasAgent.TraceLogId=} traceLogId + */ + _traceLogsRemoved: function(frameId, traceLogId) + { + var sidebarElementsToDelete = []; + var sidebarElements = /** @type {!Array.<!WebInspector.ProfileSidebarTreeElement>} */ ((this.treeElement && this.treeElement.children) || []); + for (var i = 0, n = sidebarElements.length; i < n; ++i) { + var header = /** @type {!WebInspector.CanvasProfileHeader} */ (sidebarElements[i].profile); + if (!header) + continue; + if (frameId && frameId !== header.frameId()) + continue; + if (traceLogId && traceLogId !== header.traceLogId()) + continue; + sidebarElementsToDelete.push(sidebarElements[i]); + } + for (var i = 0, n = sidebarElementsToDelete.length; i < n; ++i) + sidebarElementsToDelete[i].ondelete(); + }, + + /** + * @return {string|undefined} + */ + _selectedFrameId: function() + { + var option = this._frameSelector.selectedOption(); + return option ? option.value : undefined; + }, + + _dispatchViewUpdatedEvent: function() + { + this._frameSelector.element.classList.toggle("hidden", this._frameSelector.size() <= 1); + this.dispatchEventToListeners(WebInspector.ProfileType.Events.ViewUpdated); + }, + + /** + * @override + * @return {boolean} + */ + isInstantProfile: function() + { + return this._isSingleFrameMode(); + }, + + /** + * @override + * @return {boolean} + */ + isEnabled: function() + { + return this._canvasAgentEnabled; + }, + + __proto__: WebInspector.ProfileType.prototype +} + +/** + * @constructor + * @implements {CanvasAgent.Dispatcher} + * @param {!WebInspector.CanvasProfileType} profileType + */ +WebInspector.CanvasDispatcher = function(profileType) +{ + this._profileType = profileType; + InspectorBackend.registerCanvasDispatcher(this); +} + +WebInspector.CanvasDispatcher.prototype = { + /** + * @param {string} frameId + */ + contextCreated: function(frameId) + { + this._profileType._contextCreated(frameId); + }, + + /** + * @param {!PageAgent.FrameId=} frameId + * @param {!CanvasAgent.TraceLogId=} traceLogId + */ + traceLogsRemoved: function(frameId, traceLogId) + { + this._profileType._traceLogsRemoved(frameId, traceLogId); + } +} + +/** + * @constructor + * @extends {WebInspector.ProfileHeader} + * @param {!WebInspector.Target} target + * @param {!WebInspector.CanvasProfileType} type + * @param {!CanvasAgent.TraceLogId=} traceLogId + * @param {!PageAgent.FrameId=} frameId + */ +WebInspector.CanvasProfileHeader = function(target, type, traceLogId, frameId) +{ + WebInspector.ProfileHeader.call(this, target, type, WebInspector.UIString("Trace Log %d", type._nextProfileUid)); + /** @type {!CanvasAgent.TraceLogId} */ + this._traceLogId = traceLogId || ""; + this._frameId = frameId; + this._alive = true; + this._traceLogSize = 0; + this._traceLogPlayer = traceLogId ? new WebInspector.CanvasTraceLogPlayerProxy(traceLogId) : null; +} + +WebInspector.CanvasProfileHeader.prototype = { + /** + * @return {!CanvasAgent.TraceLogId} + */ + traceLogId: function() + { + return this._traceLogId; + }, + + /** + * @return {?WebInspector.CanvasTraceLogPlayerProxy} + */ + traceLogPlayer: function() + { + return this._traceLogPlayer; + }, + + /** + * @return {!PageAgent.FrameId|undefined} + */ + frameId: function() + { + return this._frameId; + }, + + /** + * @override + * @param {!WebInspector.ProfilesPanel} panel + * @return {!WebInspector.ProfileSidebarTreeElement} + */ + createSidebarTreeElement: function(panel) + { + return new WebInspector.ProfileSidebarTreeElement(panel, this, "profile-sidebar-tree-item"); + }, + + /** + * @override + * @return {!WebInspector.CanvasProfileView} + */ + createView: function() + { + return new WebInspector.CanvasProfileView(this); + }, + + /** + * @override + */ + dispose: function() + { + if (this._traceLogPlayer) + this._traceLogPlayer.dispose(); + clearTimeout(this._requestStatusTimer); + this._alive = false; + }, + + /** + * @param {!CanvasAgent.TraceLog=} traceLog + */ + _updateCapturingStatus: function(traceLog) + { + if (!this._traceLogId) + return; + + if (traceLog) { + this._alive = traceLog.alive; + this._traceLogSize = traceLog.totalAvailableCalls; + } + + var subtitle = this._alive ? WebInspector.UIString("Capturing\u2026 %d calls", this._traceLogSize) : WebInspector.UIString("Captured %d calls", this._traceLogSize); + this.updateStatus(subtitle, this._alive); + + if (this._alive) { + clearTimeout(this._requestStatusTimer); + this._requestStatusTimer = setTimeout(this._requestCapturingStatus.bind(this), WebInspector.CanvasProfileView.TraceLogPollingInterval); + } + }, + + _requestCapturingStatus: function() + { + /** + * @param {?CanvasAgent.TraceLog} traceLog + * @this {WebInspector.CanvasProfileHeader} + */ + function didReceiveTraceLog(traceLog) + { + if (!traceLog) + return; + this._alive = traceLog.alive; + this._traceLogSize = traceLog.totalAvailableCalls; + this._updateCapturingStatus(); + } + this._traceLogPlayer.getTraceLog(0, 0, didReceiveTraceLog.bind(this)); + }, + + __proto__: WebInspector.ProfileHeader.prototype +} + +WebInspector.CanvasProfileDataGridHelper = { + /** + * @param {!CanvasAgent.CallArgument} callArgument + * @return {!Element} + */ + createCallArgumentElement: function(callArgument) + { + if (callArgument.enumName) + return WebInspector.CanvasProfileDataGridHelper.createEnumValueElement(callArgument.enumName, +callArgument.description); + var element = document.createElement("span"); + element.className = "canvas-call-argument"; + var description = callArgument.description; + if (callArgument.type === "string") { + const maxStringLength = 150; + element.createTextChild("\""); + element.createChild("span", "canvas-formatted-string").textContent = description.trimMiddle(maxStringLength); + element.createTextChild("\""); + element.__suppressPopover = (description.length <= maxStringLength && !/[\r\n]/.test(description)); + if (!element.__suppressPopover) + element.__evalResult = WebInspector.runtimeModel.createRemoteObjectFromPrimitiveValue(description); + } else { + var type = callArgument.subtype || callArgument.type; + if (type) { + element.classList.add("canvas-formatted-" + type); + if (["null", "undefined", "boolean", "number"].indexOf(type) >= 0) + element.__suppressPopover = true; + } + element.textContent = description; + if (callArgument.remoteObject) + element.__evalResult = WebInspector.runtimeModel.createRemoteObject(callArgument.remoteObject); + } + if (callArgument.resourceId) { + element.classList.add("canvas-formatted-resource"); + element.__resourceId = callArgument.resourceId; + } + return element; + }, + + /** + * @param {string} enumName + * @param {number} enumValue + * @return {!Element} + */ + createEnumValueElement: function(enumName, enumValue) + { + var element = document.createElement("span"); + element.className = "canvas-call-argument canvas-formatted-number"; + element.textContent = enumName; + element.__evalResult = WebInspector.runtimeModel.createRemoteObjectFromPrimitiveValue(enumValue); + return element; + } +} + +/** + * @extends {WebInspector.Object} + * @constructor + * @param {!CanvasAgent.TraceLogId} traceLogId + */ +WebInspector.CanvasTraceLogPlayerProxy = function(traceLogId) +{ + this._traceLogId = traceLogId; + /** @type {!Object.<string, !CanvasAgent.ResourceState>} */ + this._currentResourceStates = {}; + /** @type {?CanvasAgent.ResourceId} */ + this._defaultResourceId = null; +} + +/** @enum {string} */ +WebInspector.CanvasTraceLogPlayerProxy.Events = { + CanvasTraceLogReceived: "CanvasTraceLogReceived", + CanvasReplayStateChanged: "CanvasReplayStateChanged", + CanvasResourceStateReceived: "CanvasResourceStateReceived", +} + +WebInspector.CanvasTraceLogPlayerProxy.prototype = { + /** + * @param {number|undefined} startOffset + * @param {number|undefined} maxLength + * @param {function(?CanvasAgent.TraceLog):void} userCallback + */ + getTraceLog: function(startOffset, maxLength, userCallback) + { + /** + * @param {?Protocol.Error} error + * @param {!CanvasAgent.TraceLog} traceLog + * @this {WebInspector.CanvasTraceLogPlayerProxy} + */ + function callback(error, traceLog) + { + if (error || !traceLog) { + userCallback(null); + return; + } + userCallback(traceLog); + this.dispatchEventToListeners(WebInspector.CanvasTraceLogPlayerProxy.Events.CanvasTraceLogReceived, traceLog); + } + CanvasAgent.getTraceLog(this._traceLogId, startOffset, maxLength, callback.bind(this)); + }, + + dispose: function() + { + this._currentResourceStates = {}; + CanvasAgent.dropTraceLog(this._traceLogId); + this.dispatchEventToListeners(WebInspector.CanvasTraceLogPlayerProxy.Events.CanvasReplayStateChanged); + }, + + /** + * @param {?CanvasAgent.ResourceId} resourceId + * @param {function(?CanvasAgent.ResourceState):void} userCallback + */ + getResourceState: function(resourceId, userCallback) + { + resourceId = resourceId || this._defaultResourceId; + if (!resourceId) { + userCallback(null); // Has not been replayed yet. + return; + } + var effectiveResourceId = /** @type {!CanvasAgent.ResourceId} */ (resourceId); + if (this._currentResourceStates[effectiveResourceId]) { + userCallback(this._currentResourceStates[effectiveResourceId]); + return; + } + + /** + * @param {?Protocol.Error} error + * @param {!CanvasAgent.ResourceState} resourceState + * @this {WebInspector.CanvasTraceLogPlayerProxy} + */ + function callback(error, resourceState) + { + if (error || !resourceState) { + userCallback(null); + return; + } + this._currentResourceStates[effectiveResourceId] = resourceState; + userCallback(resourceState); + this.dispatchEventToListeners(WebInspector.CanvasTraceLogPlayerProxy.Events.CanvasResourceStateReceived, resourceState); + } + CanvasAgent.getResourceState(this._traceLogId, effectiveResourceId, callback.bind(this)); + }, + + /** + * @param {number} index + * @param {function(?CanvasAgent.ResourceState, number):void} userCallback + */ + replayTraceLog: function(index, userCallback) + { + /** + * @param {?Protocol.Error} error + * @param {!CanvasAgent.ResourceState} resourceState + * @param {number} replayTime + * @this {WebInspector.CanvasTraceLogPlayerProxy} + */ + function callback(error, resourceState, replayTime) + { + this._currentResourceStates = {}; + if (error) { + userCallback(null, replayTime); + } else { + this._defaultResourceId = resourceState.id; + this._currentResourceStates[resourceState.id] = resourceState; + userCallback(resourceState, replayTime); + } + this.dispatchEventToListeners(WebInspector.CanvasTraceLogPlayerProxy.Events.CanvasReplayStateChanged); + if (!error) + this.dispatchEventToListeners(WebInspector.CanvasTraceLogPlayerProxy.Events.CanvasResourceStateReceived, resourceState); + } + CanvasAgent.replayTraceLog(this._traceLogId, index, callback.bind(this)); + }, + + clearResourceStates: function() + { + this._currentResourceStates = {}; + this.dispatchEventToListeners(WebInspector.CanvasTraceLogPlayerProxy.Events.CanvasReplayStateChanged); + }, + + __proto__: WebInspector.Object.prototype +} |