1 /* 2 * Copyright (C) 2013 Google Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * * Neither the name of Google Inc. nor the names of its 15 * contributors may be used to endorse or promote products derived from 16 * this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 31 /** 32 * @constructor 33 * @extends {WebInspector.VBox} 34 * @param {!WebInspector.CanvasProfileHeader} profile 35 */ 36 WebInspector.CanvasProfileView = function(profile) 37 { 38 WebInspector.VBox.call(this); 39 this.registerRequiredCSS("canvasProfiler.css"); 40 this.element.classList.add("canvas-profile-view"); 41 42 this._profile = profile; 43 this._traceLogId = profile.traceLogId(); 44 this._traceLogPlayer = /** @type {!WebInspector.CanvasTraceLogPlayerProxy} */ (profile.traceLogPlayer()); 45 this._linkifier = new WebInspector.Linkifier(); 46 47 this._replayInfoSplitView = new WebInspector.SplitView(true, true, "canvasProfileViewReplaySplitViewState", 0.34); 48 this._replayInfoSplitView.show(this.element); 49 50 this._imageSplitView = new WebInspector.SplitView(false, true, "canvasProfileViewSplitViewState", 300); 51 this._imageSplitView.show(this._replayInfoSplitView.mainElement()); 52 53 var replayImageContainerView = new WebInspector.VBox(); 54 replayImageContainerView.setMinimumSize(50, 28); 55 replayImageContainerView.show(this._imageSplitView.mainElement()); 56 57 // NOTE: The replayImageContainer can NOT be a flex div (e.g. VBox or SplitView elements)! 58 var replayImageContainer = replayImageContainerView.element.createChild("div"); 59 replayImageContainer.id = "canvas-replay-image-container"; 60 this._replayImageElement = replayImageContainer.createChild("img", "canvas-replay-image"); 61 this._debugInfoElement = replayImageContainer.createChild("div", "canvas-debug-info hidden"); 62 this._spinnerIcon = replayImageContainer.createChild("div", "spinner-icon small hidden"); 63 64 var replayLogContainerView = new WebInspector.VBox(); 65 replayLogContainerView.setMinimumSize(22, 22); 66 replayLogContainerView.show(this._imageSplitView.sidebarElement()); 67 68 var replayLogContainer = replayLogContainerView.element; 69 var controlsContainer = replayLogContainer.createChild("div", "status-bar"); 70 var logGridContainer = replayLogContainer.createChild("div", "canvas-replay-log"); 71 72 this._createControlButton(controlsContainer, "canvas-replay-first-step", WebInspector.UIString("First call."), this._onReplayFirstStepClick.bind(this)); 73 this._createControlButton(controlsContainer, "canvas-replay-prev-step", WebInspector.UIString("Previous call."), this._onReplayStepClick.bind(this, false)); 74 this._createControlButton(controlsContainer, "canvas-replay-next-step", WebInspector.UIString("Next call."), this._onReplayStepClick.bind(this, true)); 75 this._createControlButton(controlsContainer, "canvas-replay-prev-draw", WebInspector.UIString("Previous drawing call."), this._onReplayDrawingCallClick.bind(this, false)); 76 this._createControlButton(controlsContainer, "canvas-replay-next-draw", WebInspector.UIString("Next drawing call."), this._onReplayDrawingCallClick.bind(this, true)); 77 this._createControlButton(controlsContainer, "canvas-replay-last-step", WebInspector.UIString("Last call."), this._onReplayLastStepClick.bind(this)); 78 79 this._replayContextSelector = new WebInspector.StatusBarComboBox(this._onReplayContextChanged.bind(this)); 80 this._replayContextSelector.createOption(WebInspector.UIString("<screenshot auto>"), WebInspector.UIString("Show screenshot of the last replayed resource."), ""); 81 controlsContainer.appendChild(this._replayContextSelector.element); 82 83 this._installReplayInfoSidebarWidgets(controlsContainer); 84 85 this._replayStateView = new WebInspector.CanvasReplayStateView(this._traceLogPlayer); 86 this._replayStateView.show(this._replayInfoSplitView.sidebarElement()); 87 88 /** @type {!Object.<string, boolean>} */ 89 this._replayContexts = {}; 90 91 var columns = [ 92 {title: "#", sortable: false, width: "5%"}, 93 {title: WebInspector.UIString("Call"), sortable: false, width: "75%", disclosure: true}, 94 {title: WebInspector.UIString("Location"), sortable: false, width: "20%"} 95 ]; 96 97 this._logGrid = new WebInspector.DataGrid(columns); 98 this._logGrid.element.classList.add("fill"); 99 this._logGrid.show(logGridContainer); 100 this._logGrid.addEventListener(WebInspector.DataGrid.Events.SelectedNode, this._replayTraceLog, this); 101 102 this.element.addEventListener("mousedown", this._onMouseClick.bind(this), true); 103 104 this._popoverHelper = new WebInspector.ObjectPopoverHelper(this.element, this._popoverAnchor.bind(this), this._resolveObjectForPopover.bind(this), this._onHidePopover.bind(this), true); 105 this._popoverHelper.setRemoteObjectFormatter(this._hexNumbersFormatter.bind(this)); 106 107 this._requestTraceLog(0); 108 } 109 110 /** 111 * @const 112 * @type {number} 113 */ 114 WebInspector.CanvasProfileView.TraceLogPollingInterval = 500; 115 116 WebInspector.CanvasProfileView.prototype = { 117 dispose: function() 118 { 119 this._linkifier.reset(); 120 }, 121 122 get statusBarItems() 123 { 124 return []; 125 }, 126 127 get profile() 128 { 129 return this._profile; 130 }, 131 132 /** 133 * @override 134 * @return {!Array.<!Element>} 135 */ 136 elementsToRestoreScrollPositionsFor: function() 137 { 138 return [this._logGrid.scrollContainer]; 139 }, 140 141 /** 142 * @param {!Element} controlsContainer 143 */ 144 _installReplayInfoSidebarWidgets: function(controlsContainer) 145 { 146 this._replayInfoResizeWidgetElement = controlsContainer.createChild("div", "resizer-widget"); 147 this._replayInfoSplitView.addEventListener(WebInspector.SplitView.Events.ShowModeChanged, this._updateReplayInfoResizeWidget, this); 148 this._updateReplayInfoResizeWidget(); 149 this._replayInfoSplitView.installResizer(this._replayInfoResizeWidgetElement); 150 151 this._toggleReplayStateSidebarButton = this._replayInfoSplitView.createShowHideSidebarButton("sidebar", "canvas-sidebar-show-hide-button"); 152 153 controlsContainer.appendChild(this._toggleReplayStateSidebarButton.element); 154 this._replayInfoSplitView.hideSidebar(); 155 }, 156 157 _updateReplayInfoResizeWidget: function() 158 { 159 this._replayInfoResizeWidgetElement.classList.toggle("hidden", this._replayInfoSplitView.showMode() !== WebInspector.SplitView.ShowMode.Both); 160 }, 161 162 /** 163 * @param {?Event} event 164 */ 165 _onMouseClick: function(event) 166 { 167 var resourceLinkElement = event.target.enclosingNodeOrSelfWithClass("canvas-formatted-resource"); 168 if (resourceLinkElement) { 169 this._replayInfoSplitView.showBoth(); 170 this._replayStateView.selectResource(resourceLinkElement.__resourceId); 171 event.consume(true); 172 return; 173 } 174 if (event.target.enclosingNodeOrSelfWithClass("webkit-html-resource-link")) 175 event.consume(false); 176 }, 177 178 /** 179 * @param {!Element} parent 180 * @param {string} className 181 * @param {string} title 182 * @param {function(this:WebInspector.CanvasProfileView)} clickCallback 183 */ 184 _createControlButton: function(parent, className, title, clickCallback) 185 { 186 var button = new WebInspector.StatusBarButton(title, className + " canvas-replay-button"); 187 parent.appendChild(button.element); 188 189 button.makeLongClickEnabled(); 190 button.addEventListener("click", clickCallback, this); 191 button.addEventListener("longClickDown", clickCallback, this); 192 button.addEventListener("longClickPress", clickCallback, this); 193 }, 194 195 _onReplayContextChanged: function() 196 { 197 var selectedContextId = this._replayContextSelector.selectedOption().value; 198 199 /** 200 * @param {?CanvasAgent.ResourceState} resourceState 201 * @this {WebInspector.CanvasProfileView} 202 */ 203 function didReceiveResourceState(resourceState) 204 { 205 this._enableWaitIcon(false); 206 if (selectedContextId !== this._replayContextSelector.selectedOption().value) 207 return; 208 var imageURL = (resourceState && resourceState.imageURL) || ""; 209 this._replayImageElement.src = imageURL; 210 this._replayImageElement.style.visibility = imageURL ? "" : "hidden"; 211 } 212 213 this._enableWaitIcon(true); 214 this._traceLogPlayer.getResourceState(selectedContextId, didReceiveResourceState.bind(this)); 215 }, 216 217 /** 218 * @param {boolean} forward 219 */ 220 _onReplayStepClick: function(forward) 221 { 222 var selectedNode = this._logGrid.selectedNode; 223 if (!selectedNode) 224 return; 225 var nextNode = selectedNode; 226 do { 227 nextNode = forward ? nextNode.traverseNextNode(false) : nextNode.traversePreviousNode(false); 228 } while (nextNode && typeof nextNode.index !== "number"); 229 (nextNode || selectedNode).revealAndSelect(); 230 }, 231 232 /** 233 * @param {boolean} forward 234 */ 235 _onReplayDrawingCallClick: function(forward) 236 { 237 var selectedNode = this._logGrid.selectedNode; 238 if (!selectedNode) 239 return; 240 var nextNode = selectedNode; 241 while (nextNode) { 242 var sibling = forward ? nextNode.nextSibling : nextNode.previousSibling; 243 if (sibling) { 244 nextNode = sibling; 245 if (nextNode.hasChildren || nextNode.call.isDrawingCall) 246 break; 247 } else { 248 nextNode = nextNode.parent; 249 if (!forward) 250 break; 251 } 252 } 253 if (!nextNode && forward) 254 this._onReplayLastStepClick(); 255 else 256 (nextNode || selectedNode).revealAndSelect(); 257 }, 258 259 _onReplayFirstStepClick: function() 260 { 261 var firstNode = this._logGrid.rootNode().children[0]; 262 if (firstNode) 263 firstNode.revealAndSelect(); 264 }, 265 266 _onReplayLastStepClick: function() 267 { 268 var lastNode = this._logGrid.rootNode().children.peekLast(); 269 if (!lastNode) 270 return; 271 while (lastNode.expanded) { 272 var lastChild = lastNode.children.peekLast(); 273 if (!lastChild) 274 break; 275 lastNode = lastChild; 276 } 277 lastNode.revealAndSelect(); 278 }, 279 280 /** 281 * @param {boolean} enable 282 */ 283 _enableWaitIcon: function(enable) 284 { 285 this._spinnerIcon.classList.toggle("hidden", !enable); 286 this._debugInfoElement.classList.toggle("hidden", enable); 287 }, 288 289 _replayTraceLog: function() 290 { 291 if (this._pendingReplayTraceLogEvent) 292 return; 293 var index = this._selectedCallIndex(); 294 if (index === -1 || index === this._lastReplayCallIndex) 295 return; 296 this._lastReplayCallIndex = index; 297 this._pendingReplayTraceLogEvent = true; 298 299 /** 300 * @param {?CanvasAgent.ResourceState} resourceState 301 * @param {number} replayTime 302 * @this {WebInspector.CanvasProfileView} 303 */ 304 function didReplayTraceLog(resourceState, replayTime) 305 { 306 delete this._pendingReplayTraceLogEvent; 307 this._enableWaitIcon(false); 308 309 this._debugInfoElement.textContent = WebInspector.UIString("Replay time: %s", Number.secondsToString(replayTime / 1000, true)); 310 this._onReplayContextChanged(); 311 312 if (index !== this._selectedCallIndex()) 313 this._replayTraceLog(); 314 } 315 this._enableWaitIcon(true); 316 this._traceLogPlayer.replayTraceLog(index, didReplayTraceLog.bind(this)); 317 }, 318 319 /** 320 * @param {number} offset 321 */ 322 _requestTraceLog: function(offset) 323 { 324 /** 325 * @param {?CanvasAgent.TraceLog} traceLog 326 * @this {WebInspector.CanvasProfileView} 327 */ 328 function didReceiveTraceLog(traceLog) 329 { 330 this._enableWaitIcon(false); 331 if (!traceLog) 332 return; 333 var callNodes = []; 334 var calls = traceLog.calls; 335 var index = traceLog.startOffset; 336 for (var i = 0, n = calls.length; i < n; ++i) 337 callNodes.push(this._createCallNode(index++, calls[i])); 338 var contexts = traceLog.contexts; 339 for (var i = 0, n = contexts.length; i < n; ++i) { 340 var contextId = contexts[i].resourceId || ""; 341 var description = contexts[i].description || ""; 342 if (this._replayContexts[contextId]) 343 continue; 344 this._replayContexts[contextId] = true; 345 this._replayContextSelector.createOption(description, WebInspector.UIString("Show screenshot of this context's canvas."), contextId); 346 } 347 this._appendCallNodes(callNodes); 348 if (traceLog.alive) 349 setTimeout(this._requestTraceLog.bind(this, index), WebInspector.CanvasProfileView.TraceLogPollingInterval); 350 else 351 this._flattenSingleFrameNode(); 352 this._profile._updateCapturingStatus(traceLog); 353 this._onReplayLastStepClick(); // Automatically replay the last step. 354 } 355 this._enableWaitIcon(true); 356 this._traceLogPlayer.getTraceLog(offset, undefined, didReceiveTraceLog.bind(this)); 357 }, 358 359 /** 360 * @return {number} 361 */ 362 _selectedCallIndex: function() 363 { 364 var node = this._logGrid.selectedNode; 365 return node ? this._peekLastRecursively(node).index : -1; 366 }, 367 368 /** 369 * @param {!WebInspector.DataGridNode} node 370 * @return {!WebInspector.DataGridNode} 371 */ 372 _peekLastRecursively: function(node) 373 { 374 var lastChild; 375 while ((lastChild = node.children.peekLast())) 376 node = lastChild; 377 return node; 378 }, 379 380 /** 381 * @param {!Array.<!WebInspector.DataGridNode>} callNodes 382 */ 383 _appendCallNodes: function(callNodes) 384 { 385 var rootNode = this._logGrid.rootNode(); 386 var frameNode = rootNode.children.peekLast(); 387 if (frameNode && this._peekLastRecursively(frameNode).call.isFrameEndCall) 388 frameNode = null; 389 for (var i = 0, n = callNodes.length; i < n; ++i) { 390 if (!frameNode) { 391 var index = rootNode.children.length; 392 var data = {}; 393 data[0] = ""; 394 data[1] = WebInspector.UIString("Frame #%d", index + 1); 395 data[2] = ""; 396 frameNode = new WebInspector.DataGridNode(data); 397 frameNode.selectable = true; 398 rootNode.appendChild(frameNode); 399 } 400 var nextFrameCallIndex = i + 1; 401 while (nextFrameCallIndex < n && !callNodes[nextFrameCallIndex - 1].call.isFrameEndCall) 402 ++nextFrameCallIndex; 403 this._appendCallNodesToFrameNode(frameNode, callNodes, i, nextFrameCallIndex); 404 i = nextFrameCallIndex - 1; 405 frameNode = null; 406 } 407 }, 408 409 /** 410 * @param {!WebInspector.DataGridNode} frameNode 411 * @param {!Array.<!WebInspector.DataGridNode>} callNodes 412 * @param {number} fromIndex 413 * @param {number} toIndex not inclusive 414 */ 415 _appendCallNodesToFrameNode: function(frameNode, callNodes, fromIndex, toIndex) 416 { 417 var self = this; 418 function appendDrawCallGroup() 419 { 420 var index = self._drawCallGroupsCount || 0; 421 var data = {}; 422 data[0] = ""; 423 data[1] = WebInspector.UIString("Draw call group #%d", index + 1); 424 data[2] = ""; 425 var node = new WebInspector.DataGridNode(data); 426 node.selectable = true; 427 self._drawCallGroupsCount = index + 1; 428 frameNode.appendChild(node); 429 return node; 430 } 431 432 function splitDrawCallGroup(drawCallGroup) 433 { 434 var splitIndex = 0; 435 var splitNode; 436 while ((splitNode = drawCallGroup.children[splitIndex])) { 437 if (splitNode.call.isDrawingCall) 438 break; 439 ++splitIndex; 440 } 441 var newDrawCallGroup = appendDrawCallGroup(); 442 var lastNode; 443 while ((lastNode = drawCallGroup.children[splitIndex + 1])) 444 newDrawCallGroup.appendChild(lastNode); 445 return newDrawCallGroup; 446 } 447 448 var drawCallGroup = frameNode.children.peekLast(); 449 var groupHasDrawCall = false; 450 if (drawCallGroup) { 451 for (var i = 0, n = drawCallGroup.children.length; i < n; ++i) { 452 if (drawCallGroup.children[i].call.isDrawingCall) { 453 groupHasDrawCall = true; 454 break; 455 } 456 } 457 } else 458 drawCallGroup = appendDrawCallGroup(); 459 460 for (var i = fromIndex; i < toIndex; ++i) { 461 var node = callNodes[i]; 462 drawCallGroup.appendChild(node); 463 if (node.call.isDrawingCall) { 464 if (groupHasDrawCall) 465 drawCallGroup = splitDrawCallGroup(drawCallGroup); 466 else 467 groupHasDrawCall = true; 468 } 469 } 470 }, 471 472 /** 473 * @param {number} index 474 * @param {!CanvasAgent.Call} call 475 * @return {!WebInspector.DataGridNode} 476 */ 477 _createCallNode: function(index, call) 478 { 479 var callViewElement = document.createElement("div"); 480 481 var data = {}; 482 data[0] = index + 1; 483 data[1] = callViewElement; 484 data[2] = ""; 485 if (call.sourceURL) { 486 // FIXME(62725): stack trace line/column numbers are one-based. 487 var lineNumber = Math.max(0, call.lineNumber - 1) || 0; 488 var columnNumber = Math.max(0, call.columnNumber - 1) || 0; 489 data[2] = this._linkifier.linkifyLocation(this.profile.target(), call.sourceURL, lineNumber, columnNumber); 490 } 491 492 callViewElement.createChild("span", "canvas-function-name").textContent = call.functionName || "context." + call.property; 493 494 if (call.arguments) { 495 callViewElement.createTextChild("("); 496 for (var i = 0, n = call.arguments.length; i < n; ++i) { 497 var argument = /** @type {!CanvasAgent.CallArgument} */ (call.arguments[i]); 498 if (i) 499 callViewElement.createTextChild(", "); 500 var element = WebInspector.CanvasProfileDataGridHelper.createCallArgumentElement(argument); 501 element.__argumentIndex = i; 502 callViewElement.appendChild(element); 503 } 504 callViewElement.createTextChild(")"); 505 } else if (call.value) { 506 callViewElement.createTextChild(" = "); 507 callViewElement.appendChild(WebInspector.CanvasProfileDataGridHelper.createCallArgumentElement(call.value)); 508 } 509 510 if (call.result) { 511 callViewElement.createTextChild(" => "); 512 callViewElement.appendChild(WebInspector.CanvasProfileDataGridHelper.createCallArgumentElement(call.result)); 513 } 514 515 var node = new WebInspector.DataGridNode(data); 516 node.index = index; 517 node.selectable = true; 518 node.call = call; 519 return node; 520 }, 521 522 _popoverAnchor: function(element, event) 523 { 524 var argumentElement = element.enclosingNodeOrSelfWithClass("canvas-call-argument"); 525 if (!argumentElement || argumentElement.__suppressPopover) 526 return null; 527 return argumentElement; 528 }, 529 530 _resolveObjectForPopover: function(argumentElement, showCallback, objectGroupName) 531 { 532 /** 533 * @param {?Protocol.Error} error 534 * @param {!RuntimeAgent.RemoteObject=} result 535 * @param {!CanvasAgent.ResourceState=} resourceState 536 * @this {WebInspector.CanvasProfileView} 537 */ 538 function showObjectPopover(error, result, resourceState) 539 { 540 if (error) 541 return; 542 543 // FIXME: handle resourceState also 544 if (!result) 545 return; 546 547 this._popoverAnchorElement = argumentElement.cloneNode(true); 548 this._popoverAnchorElement.classList.add("canvas-popover-anchor"); 549 this._popoverAnchorElement.classList.add("source-frame-eval-expression"); 550 argumentElement.parentElement.appendChild(this._popoverAnchorElement); 551 552 var diffLeft = this._popoverAnchorElement.boxInWindow().x - argumentElement.boxInWindow().x; 553 this._popoverAnchorElement.style.left = this._popoverAnchorElement.offsetLeft - diffLeft + "px"; 554 555 showCallback(WebInspector.runtimeModel.createRemoteObject(result), false, this._popoverAnchorElement); 556 } 557 558 var evalResult = argumentElement.__evalResult; 559 if (evalResult) 560 showObjectPopover.call(this, null, evalResult); 561 else { 562 var dataGridNode = this._logGrid.dataGridNodeFromNode(argumentElement); 563 if (!dataGridNode || typeof dataGridNode.index !== "number") { 564 this._popoverHelper.hidePopover(); 565 return; 566 } 567 var callIndex = dataGridNode.index; 568 var argumentIndex = argumentElement.__argumentIndex; 569 if (typeof argumentIndex !== "number") 570 argumentIndex = -1; 571 CanvasAgent.evaluateTraceLogCallArgument(this._traceLogId, callIndex, argumentIndex, objectGroupName, showObjectPopover.bind(this)); 572 } 573 }, 574 575 /** 576 * @param {!WebInspector.RemoteObject} object 577 * @return {string} 578 */ 579 _hexNumbersFormatter: function(object) 580 { 581 if (object.type === "number") { 582 // Show enum values in hex with min length of 4 (e.g. 0x0012). 583 var str = "0000" + Number(object.description).toString(16).toUpperCase(); 584 str = str.replace(/^0+(.{4,})$/, "$1"); 585 return "0x" + str; 586 } 587 return object.description || ""; 588 }, 589 590 _onHidePopover: function() 591 { 592 if (this._popoverAnchorElement) { 593 this._popoverAnchorElement.remove() 594 delete this._popoverAnchorElement; 595 } 596 }, 597 598 _flattenSingleFrameNode: function() 599 { 600 var rootNode = this._logGrid.rootNode(); 601 if (rootNode.children.length !== 1) 602 return; 603 var frameNode = rootNode.children[0]; 604 while (frameNode.children[0]) 605 rootNode.appendChild(frameNode.children[0]); 606 rootNode.removeChild(frameNode); 607 }, 608 609 __proto__: WebInspector.VBox.prototype 610 } 611 612 /** 613 * @constructor 614 * @extends {WebInspector.ProfileType} 615 */ 616 WebInspector.CanvasProfileType = function() 617 { 618 WebInspector.ProfileType.call(this, WebInspector.CanvasProfileType.TypeId, WebInspector.UIString("Capture Canvas Frame")); 619 this._recording = false; 620 this._lastProfileHeader = null; 621 622 this._capturingModeSelector = new WebInspector.StatusBarComboBox(this._dispatchViewUpdatedEvent.bind(this)); 623 this._capturingModeSelector.element.title = WebInspector.UIString("Canvas capture mode."); 624 this._capturingModeSelector.createOption(WebInspector.UIString("Single Frame"), WebInspector.UIString("Capture a single canvas frame."), ""); 625 this._capturingModeSelector.createOption(WebInspector.UIString("Consecutive Frames"), WebInspector.UIString("Capture consecutive canvas frames."), "1"); 626 627 /** @type {!Object.<string, !Element>} */ 628 this._frameOptions = {}; 629 630 /** @type {!Object.<string, boolean>} */ 631 this._framesWithCanvases = {}; 632 633 this._frameSelector = new WebInspector.StatusBarComboBox(this._dispatchViewUpdatedEvent.bind(this)); 634 this._frameSelector.element.title = WebInspector.UIString("Frame containing the canvases to capture."); 635 this._frameSelector.element.classList.add("hidden"); 636 637 this._target = /** @type {!WebInspector.Target} */ (WebInspector.targetManager.activeTarget()); 638 this._target.resourceTreeModel.frames().forEach(this._addFrame, this); 639 this._target.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.FrameAdded, this._frameAdded, this); 640 this._target.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.FrameDetached, this._frameRemoved, this); 641 642 this._dispatcher = new WebInspector.CanvasDispatcher(this); 643 this._canvasAgentEnabled = false; 644 645 this._decorationElement = document.createElement("div"); 646 this._decorationElement.className = "profile-canvas-decoration"; 647 this._updateDecorationElement(); 648 } 649 650 WebInspector.CanvasProfileType.TypeId = "CANVAS_PROFILE"; 651 652 WebInspector.CanvasProfileType.prototype = { 653 get statusBarItems() 654 { 655 return [this._capturingModeSelector.element, this._frameSelector.element]; 656 }, 657 658 get buttonTooltip() 659 { 660 if (this._isSingleFrameMode()) 661 return WebInspector.UIString("Capture next canvas frame."); 662 else 663 return this._recording ? WebInspector.UIString("Stop capturing canvas frames.") : WebInspector.UIString("Start capturing canvas frames."); 664 }, 665 666 /** 667 * @override 668 * @return {boolean} 669 */ 670 buttonClicked: function() 671 { 672 if (!this._canvasAgentEnabled) 673 return false; 674 if (this._recording) { 675 this._recording = false; 676 this._stopFrameCapturing(); 677 } else if (this._isSingleFrameMode()) { 678 this._recording = false; 679 this._runSingleFrameCapturing(); 680 } else { 681 this._recording = true; 682 this._startFrameCapturing(); 683 } 684 return this._recording; 685 }, 686 687 _runSingleFrameCapturing: function() 688 { 689 var frameId = this._selectedFrameId(); 690 this._target.profilingLock.acquire(); 691 CanvasAgent.captureFrame(frameId, this._didStartCapturingFrame.bind(this, frameId)); 692 this._target.profilingLock.release(); 693 }, 694 695 _startFrameCapturing: function() 696 { 697 var frameId = this._selectedFrameId(); 698 this._target.profilingLock.acquire(); 699 CanvasAgent.startCapturing(frameId, this._didStartCapturingFrame.bind(this, frameId)); 700 }, 701 702 _stopFrameCapturing: function() 703 { 704 if (!this._lastProfileHeader) { 705 this._target.profilingLock.release(); 706 return; 707 } 708 var profileHeader = this._lastProfileHeader; 709 var traceLogId = profileHeader.traceLogId(); 710 this._lastProfileHeader = null; 711 function didStopCapturing() 712 { 713 profileHeader._updateCapturingStatus(); 714 } 715 CanvasAgent.stopCapturing(traceLogId, didStopCapturing); 716 this._target.profilingLock.release(); 717 }, 718 719 /** 720 * @param {string|undefined} frameId 721 * @param {?Protocol.Error} error 722 * @param {!CanvasAgent.TraceLogId} traceLogId 723 */ 724 _didStartCapturingFrame: function(frameId, error, traceLogId) 725 { 726 if (error || this._lastProfileHeader && this._lastProfileHeader.traceLogId() === traceLogId) 727 return; 728 var profileHeader = new WebInspector.CanvasProfileHeader(this._target, this, traceLogId, frameId); 729 this._lastProfileHeader = profileHeader; 730 this.addProfile(profileHeader); 731 profileHeader._updateCapturingStatus(); 732 }, 733 734 get treeItemTitle() 735 { 736 return WebInspector.UIString("CANVAS PROFILE"); 737 }, 738 739 get description() 740 { 741 return WebInspector.UIString("Canvas calls instrumentation"); 742 }, 743 744 /** 745 * @override 746 * @return {!Element} 747 */ 748 decorationElement: function() 749 { 750 return this._decorationElement; 751 }, 752 753 /** 754 * @override 755 * @param {!WebInspector.ProfileHeader} profile 756 */ 757 removeProfile: function(profile) 758 { 759 WebInspector.ProfileType.prototype.removeProfile.call(this, profile); 760 if (this._recording && profile === this._lastProfileHeader) 761 this._recording = false; 762 }, 763 764 /** 765 * @param {boolean=} forcePageReload 766 */ 767 _updateDecorationElement: function(forcePageReload) 768 { 769 this._decorationElement.removeChildren(); 770 this._decorationElement.createChild("div", "warning-icon-small"); 771 this._decorationElement.appendChild(document.createTextNode(this._canvasAgentEnabled ? WebInspector.UIString("Canvas Profiler is enabled.") : WebInspector.UIString("Canvas Profiler is disabled."))); 772 var button = this._decorationElement.createChild("button"); 773 button.type = "button"; 774 button.textContent = this._canvasAgentEnabled ? WebInspector.UIString("Disable") : WebInspector.UIString("Enable"); 775 button.addEventListener("click", this._onProfilerEnableButtonClick.bind(this, !this._canvasAgentEnabled), false); 776 777 var target = this._target; 778 /** 779 * @param {?Protocol.Error} error 780 * @param {boolean} result 781 */ 782 function hasUninstrumentedCanvasesCallback(error, result) 783 { 784 if (error || result) 785 target.resourceTreeModel.reloadPage(); 786 } 787 788 if (forcePageReload) { 789 if (this._canvasAgentEnabled) { 790 CanvasAgent.hasUninstrumentedCanvases(hasUninstrumentedCanvasesCallback); 791 } else { 792 for (var frameId in this._framesWithCanvases) { 793 if (this._framesWithCanvases.hasOwnProperty(frameId)) { 794 target.resourceTreeModel.reloadPage(); 795 break; 796 } 797 } 798 } 799 } 800 }, 801 802 /** 803 * @param {boolean} enable 804 */ 805 _onProfilerEnableButtonClick: function(enable) 806 { 807 if (this._canvasAgentEnabled === enable) 808 return; 809 810 /** 811 * @param {?Protocol.Error} error 812 * @this {WebInspector.CanvasProfileType} 813 */ 814 function callback(error) 815 { 816 if (error) 817 return; 818 this._canvasAgentEnabled = enable; 819 this._updateDecorationElement(true); 820 this._dispatchViewUpdatedEvent(); 821 } 822 if (enable) 823 CanvasAgent.enable(callback.bind(this)); 824 else 825 CanvasAgent.disable(callback.bind(this)); 826 }, 827 828 /** 829 * @return {boolean} 830 */ 831 _isSingleFrameMode: function() 832 { 833 return !this._capturingModeSelector.selectedOption().value; 834 }, 835 836 /** 837 * @param {!WebInspector.Event} event 838 */ 839 _frameAdded: function(event) 840 { 841 var frame = /** @type {!WebInspector.ResourceTreeFrame} */ (event.data); 842 this._addFrame(frame); 843 }, 844 845 /** 846 * @param {!WebInspector.ResourceTreeFrame} frame 847 */ 848 _addFrame: function(frame) 849 { 850 var frameId = frame.id; 851 var option = document.createElement("option"); 852 option.text = frame.displayName(); 853 option.title = frame.url; 854 option.value = frameId; 855 856 this._frameOptions[frameId] = option; 857 858 if (this._framesWithCanvases[frameId]) { 859 this._frameSelector.addOption(option); 860 this._dispatchViewUpdatedEvent(); 861 } 862 }, 863 864 /** 865 * @param {!WebInspector.Event} event 866 */ 867 _frameRemoved: function(event) 868 { 869 var frame = /** @type {!WebInspector.ResourceTreeFrame} */ (event.data); 870 var frameId = frame.id; 871 var option = this._frameOptions[frameId]; 872 if (option && this._framesWithCanvases[frameId]) { 873 this._frameSelector.removeOption(option); 874 this._dispatchViewUpdatedEvent(); 875 } 876 delete this._frameOptions[frameId]; 877 delete this._framesWithCanvases[frameId]; 878 }, 879 880 /** 881 * @param {string} frameId 882 */ 883 _contextCreated: function(frameId) 884 { 885 if (this._framesWithCanvases[frameId]) 886 return; 887 this._framesWithCanvases[frameId] = true; 888 var option = this._frameOptions[frameId]; 889 if (option) { 890 this._frameSelector.addOption(option); 891 this._dispatchViewUpdatedEvent(); 892 } 893 }, 894 895 /** 896 * @param {!PageAgent.FrameId=} frameId 897 * @param {!CanvasAgent.TraceLogId=} traceLogId 898 */ 899 _traceLogsRemoved: function(frameId, traceLogId) 900 { 901 var sidebarElementsToDelete = []; 902 var sidebarElements = /** @type {!Array.<!WebInspector.ProfileSidebarTreeElement>} */ ((this.treeElement && this.treeElement.children) || []); 903 for (var i = 0, n = sidebarElements.length; i < n; ++i) { 904 var header = /** @type {!WebInspector.CanvasProfileHeader} */ (sidebarElements[i].profile); 905 if (!header) 906 continue; 907 if (frameId && frameId !== header.frameId()) 908 continue; 909 if (traceLogId && traceLogId !== header.traceLogId()) 910 continue; 911 sidebarElementsToDelete.push(sidebarElements[i]); 912 } 913 for (var i = 0, n = sidebarElementsToDelete.length; i < n; ++i) 914 sidebarElementsToDelete[i].ondelete(); 915 }, 916 917 /** 918 * @return {string|undefined} 919 */ 920 _selectedFrameId: function() 921 { 922 var option = this._frameSelector.selectedOption(); 923 return option ? option.value : undefined; 924 }, 925 926 _dispatchViewUpdatedEvent: function() 927 { 928 this._frameSelector.element.classList.toggle("hidden", this._frameSelector.size() <= 1); 929 this.dispatchEventToListeners(WebInspector.ProfileType.Events.ViewUpdated); 930 }, 931 932 /** 933 * @override 934 * @return {boolean} 935 */ 936 isInstantProfile: function() 937 { 938 return this._isSingleFrameMode(); 939 }, 940 941 /** 942 * @override 943 * @return {boolean} 944 */ 945 isEnabled: function() 946 { 947 return this._canvasAgentEnabled; 948 }, 949 950 __proto__: WebInspector.ProfileType.prototype 951 } 952 953 /** 954 * @constructor 955 * @implements {CanvasAgent.Dispatcher} 956 * @param {!WebInspector.CanvasProfileType} profileType 957 */ 958 WebInspector.CanvasDispatcher = function(profileType) 959 { 960 this._profileType = profileType; 961 InspectorBackend.registerCanvasDispatcher(this); 962 } 963 964 WebInspector.CanvasDispatcher.prototype = { 965 /** 966 * @param {string} frameId 967 */ 968 contextCreated: function(frameId) 969 { 970 this._profileType._contextCreated(frameId); 971 }, 972 973 /** 974 * @param {!PageAgent.FrameId=} frameId 975 * @param {!CanvasAgent.TraceLogId=} traceLogId 976 */ 977 traceLogsRemoved: function(frameId, traceLogId) 978 { 979 this._profileType._traceLogsRemoved(frameId, traceLogId); 980 } 981 } 982 983 /** 984 * @constructor 985 * @extends {WebInspector.ProfileHeader} 986 * @param {!WebInspector.Target} target 987 * @param {!WebInspector.CanvasProfileType} type 988 * @param {!CanvasAgent.TraceLogId=} traceLogId 989 * @param {!PageAgent.FrameId=} frameId 990 */ 991 WebInspector.CanvasProfileHeader = function(target, type, traceLogId, frameId) 992 { 993 WebInspector.ProfileHeader.call(this, target, type, WebInspector.UIString("Trace Log %d", type._nextProfileUid)); 994 /** @type {!CanvasAgent.TraceLogId} */ 995 this._traceLogId = traceLogId || ""; 996 this._frameId = frameId; 997 this._alive = true; 998 this._traceLogSize = 0; 999 this._traceLogPlayer = traceLogId ? new WebInspector.CanvasTraceLogPlayerProxy(traceLogId) : null; 1000 } 1001 1002 WebInspector.CanvasProfileHeader.prototype = { 1003 /** 1004 * @return {!CanvasAgent.TraceLogId} 1005 */ 1006 traceLogId: function() 1007 { 1008 return this._traceLogId; 1009 }, 1010 1011 /** 1012 * @return {?WebInspector.CanvasTraceLogPlayerProxy} 1013 */ 1014 traceLogPlayer: function() 1015 { 1016 return this._traceLogPlayer; 1017 }, 1018 1019 /** 1020 * @return {!PageAgent.FrameId|undefined} 1021 */ 1022 frameId: function() 1023 { 1024 return this._frameId; 1025 }, 1026 1027 /** 1028 * @override 1029 * @param {!WebInspector.ProfilesPanel} panel 1030 * @return {!WebInspector.ProfileSidebarTreeElement} 1031 */ 1032 createSidebarTreeElement: function(panel) 1033 { 1034 return new WebInspector.ProfileSidebarTreeElement(panel, this, "profile-sidebar-tree-item"); 1035 }, 1036 1037 /** 1038 * @override 1039 * @return {!WebInspector.CanvasProfileView} 1040 */ 1041 createView: function() 1042 { 1043 return new WebInspector.CanvasProfileView(this); 1044 }, 1045 1046 /** 1047 * @override 1048 */ 1049 dispose: function() 1050 { 1051 if (this._traceLogPlayer) 1052 this._traceLogPlayer.dispose(); 1053 clearTimeout(this._requestStatusTimer); 1054 this._alive = false; 1055 }, 1056 1057 /** 1058 * @param {!CanvasAgent.TraceLog=} traceLog 1059 */ 1060 _updateCapturingStatus: function(traceLog) 1061 { 1062 if (!this._traceLogId) 1063 return; 1064 1065 if (traceLog) { 1066 this._alive = traceLog.alive; 1067 this._traceLogSize = traceLog.totalAvailableCalls; 1068 } 1069 1070 var subtitle = this._alive ? WebInspector.UIString("Capturing\u2026 %d calls", this._traceLogSize) : WebInspector.UIString("Captured %d calls", this._traceLogSize); 1071 this.updateStatus(subtitle, this._alive); 1072 1073 if (this._alive) { 1074 clearTimeout(this._requestStatusTimer); 1075 this._requestStatusTimer = setTimeout(this._requestCapturingStatus.bind(this), WebInspector.CanvasProfileView.TraceLogPollingInterval); 1076 } 1077 }, 1078 1079 _requestCapturingStatus: function() 1080 { 1081 /** 1082 * @param {?CanvasAgent.TraceLog} traceLog 1083 * @this {WebInspector.CanvasProfileHeader} 1084 */ 1085 function didReceiveTraceLog(traceLog) 1086 { 1087 if (!traceLog) 1088 return; 1089 this._alive = traceLog.alive; 1090 this._traceLogSize = traceLog.totalAvailableCalls; 1091 this._updateCapturingStatus(); 1092 } 1093 this._traceLogPlayer.getTraceLog(0, 0, didReceiveTraceLog.bind(this)); 1094 }, 1095 1096 __proto__: WebInspector.ProfileHeader.prototype 1097 } 1098 1099 WebInspector.CanvasProfileDataGridHelper = { 1100 /** 1101 * @param {!CanvasAgent.CallArgument} callArgument 1102 * @return {!Element} 1103 */ 1104 createCallArgumentElement: function(callArgument) 1105 { 1106 if (callArgument.enumName) 1107 return WebInspector.CanvasProfileDataGridHelper.createEnumValueElement(callArgument.enumName, +callArgument.description); 1108 var element = document.createElement("span"); 1109 element.className = "canvas-call-argument"; 1110 var description = callArgument.description; 1111 if (callArgument.type === "string") { 1112 const maxStringLength = 150; 1113 element.createTextChild("\""); 1114 element.createChild("span", "canvas-formatted-string").textContent = description.trimMiddle(maxStringLength); 1115 element.createTextChild("\""); 1116 element.__suppressPopover = (description.length <= maxStringLength && !/[\r\n]/.test(description)); 1117 if (!element.__suppressPopover) 1118 element.__evalResult = WebInspector.runtimeModel.createRemoteObjectFromPrimitiveValue(description); 1119 } else { 1120 var type = callArgument.subtype || callArgument.type; 1121 if (type) { 1122 element.classList.add("canvas-formatted-" + type); 1123 if (["null", "undefined", "boolean", "number"].indexOf(type) >= 0) 1124 element.__suppressPopover = true; 1125 } 1126 element.textContent = description; 1127 if (callArgument.remoteObject) 1128 element.__evalResult = WebInspector.runtimeModel.createRemoteObject(callArgument.remoteObject); 1129 } 1130 if (callArgument.resourceId) { 1131 element.classList.add("canvas-formatted-resource"); 1132 element.__resourceId = callArgument.resourceId; 1133 } 1134 return element; 1135 }, 1136 1137 /** 1138 * @param {string} enumName 1139 * @param {number} enumValue 1140 * @return {!Element} 1141 */ 1142 createEnumValueElement: function(enumName, enumValue) 1143 { 1144 var element = document.createElement("span"); 1145 element.className = "canvas-call-argument canvas-formatted-number"; 1146 element.textContent = enumName; 1147 element.__evalResult = WebInspector.runtimeModel.createRemoteObjectFromPrimitiveValue(enumValue); 1148 return element; 1149 } 1150 } 1151 1152 /** 1153 * @extends {WebInspector.Object} 1154 * @constructor 1155 * @param {!CanvasAgent.TraceLogId} traceLogId 1156 */ 1157 WebInspector.CanvasTraceLogPlayerProxy = function(traceLogId) 1158 { 1159 this._traceLogId = traceLogId; 1160 /** @type {!Object.<string, !CanvasAgent.ResourceState>} */ 1161 this._currentResourceStates = {}; 1162 /** @type {?CanvasAgent.ResourceId} */ 1163 this._defaultResourceId = null; 1164 } 1165 1166 /** @enum {string} */ 1167 WebInspector.CanvasTraceLogPlayerProxy.Events = { 1168 CanvasTraceLogReceived: "CanvasTraceLogReceived", 1169 CanvasReplayStateChanged: "CanvasReplayStateChanged", 1170 CanvasResourceStateReceived: "CanvasResourceStateReceived", 1171 } 1172 1173 WebInspector.CanvasTraceLogPlayerProxy.prototype = { 1174 /** 1175 * @param {number|undefined} startOffset 1176 * @param {number|undefined} maxLength 1177 * @param {function(?CanvasAgent.TraceLog):void} userCallback 1178 */ 1179 getTraceLog: function(startOffset, maxLength, userCallback) 1180 { 1181 /** 1182 * @param {?Protocol.Error} error 1183 * @param {!CanvasAgent.TraceLog} traceLog 1184 * @this {WebInspector.CanvasTraceLogPlayerProxy} 1185 */ 1186 function callback(error, traceLog) 1187 { 1188 if (error || !traceLog) { 1189 userCallback(null); 1190 return; 1191 } 1192 userCallback(traceLog); 1193 this.dispatchEventToListeners(WebInspector.CanvasTraceLogPlayerProxy.Events.CanvasTraceLogReceived, traceLog); 1194 } 1195 CanvasAgent.getTraceLog(this._traceLogId, startOffset, maxLength, callback.bind(this)); 1196 }, 1197 1198 dispose: function() 1199 { 1200 this._currentResourceStates = {}; 1201 CanvasAgent.dropTraceLog(this._traceLogId); 1202 this.dispatchEventToListeners(WebInspector.CanvasTraceLogPlayerProxy.Events.CanvasReplayStateChanged); 1203 }, 1204 1205 /** 1206 * @param {?CanvasAgent.ResourceId} resourceId 1207 * @param {function(?CanvasAgent.ResourceState):void} userCallback 1208 */ 1209 getResourceState: function(resourceId, userCallback) 1210 { 1211 resourceId = resourceId || this._defaultResourceId; 1212 if (!resourceId) { 1213 userCallback(null); // Has not been replayed yet. 1214 return; 1215 } 1216 var effectiveResourceId = /** @type {!CanvasAgent.ResourceId} */ (resourceId); 1217 if (this._currentResourceStates[effectiveResourceId]) { 1218 userCallback(this._currentResourceStates[effectiveResourceId]); 1219 return; 1220 } 1221 1222 /** 1223 * @param {?Protocol.Error} error 1224 * @param {!CanvasAgent.ResourceState} resourceState 1225 * @this {WebInspector.CanvasTraceLogPlayerProxy} 1226 */ 1227 function callback(error, resourceState) 1228 { 1229 if (error || !resourceState) { 1230 userCallback(null); 1231 return; 1232 } 1233 this._currentResourceStates[effectiveResourceId] = resourceState; 1234 userCallback(resourceState); 1235 this.dispatchEventToListeners(WebInspector.CanvasTraceLogPlayerProxy.Events.CanvasResourceStateReceived, resourceState); 1236 } 1237 CanvasAgent.getResourceState(this._traceLogId, effectiveResourceId, callback.bind(this)); 1238 }, 1239 1240 /** 1241 * @param {number} index 1242 * @param {function(?CanvasAgent.ResourceState, number):void} userCallback 1243 */ 1244 replayTraceLog: function(index, userCallback) 1245 { 1246 /** 1247 * @param {?Protocol.Error} error 1248 * @param {!CanvasAgent.ResourceState} resourceState 1249 * @param {number} replayTime 1250 * @this {WebInspector.CanvasTraceLogPlayerProxy} 1251 */ 1252 function callback(error, resourceState, replayTime) 1253 { 1254 this._currentResourceStates = {}; 1255 if (error) { 1256 userCallback(null, replayTime); 1257 } else { 1258 this._defaultResourceId = resourceState.id; 1259 this._currentResourceStates[resourceState.id] = resourceState; 1260 userCallback(resourceState, replayTime); 1261 } 1262 this.dispatchEventToListeners(WebInspector.CanvasTraceLogPlayerProxy.Events.CanvasReplayStateChanged); 1263 if (!error) 1264 this.dispatchEventToListeners(WebInspector.CanvasTraceLogPlayerProxy.Events.CanvasResourceStateReceived, resourceState); 1265 } 1266 CanvasAgent.replayTraceLog(this._traceLogId, index, callback.bind(this)); 1267 }, 1268 1269 clearResourceStates: function() 1270 { 1271 this._currentResourceStates = {}; 1272 this.dispatchEventToListeners(WebInspector.CanvasTraceLogPlayerProxy.Events.CanvasReplayStateChanged); 1273 }, 1274 1275 __proto__: WebInspector.Object.prototype 1276 } 1277