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