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.CanvasTraceLogPlayerProxy} traceLogPlayer 35 */ 36 WebInspector.CanvasReplayStateView = function(traceLogPlayer) 37 { 38 WebInspector.View.call(this); 39 this.registerRequiredCSS("canvasProfiler.css"); 40 this.element.classList.add("canvas-replay-state-view"); 41 this._traceLogPlayer = traceLogPlayer; 42 43 var controlsContainer = this.element.createChild("div", "status-bar"); 44 this._prevButton = this._createControlButton(controlsContainer, "canvas-replay-state-prev", WebInspector.UIString("Previous resource."), this._onResourceNavigationClick.bind(this, false)); 45 this._nextButton = this._createControlButton(controlsContainer, "canvas-replay-state-next", WebInspector.UIString("Next resource."), this._onResourceNavigationClick.bind(this, true)); 46 this._createControlButton(controlsContainer, "canvas-replay-state-refresh", WebInspector.UIString("Refresh."), this._onStateRefreshClick.bind(this)); 47 48 this._resourceSelector = new WebInspector.StatusBarComboBox(this._onReplayResourceChanged.bind(this)); 49 this._currentOption = this._resourceSelector.createOption(WebInspector.UIString("<auto>"), WebInspector.UIString("Show state of the last replayed resource."), ""); 50 controlsContainer.appendChild(this._resourceSelector.element); 51 52 /** @type {!Object.<string, string>} */ 53 this._resourceIdToDescription = {}; 54 55 /** @type {!Object.<string, !Object.<string, boolean>>} */ 56 this._gridNodesExpandedState = {}; 57 /** @type {!Object.<string, {scrollTop:number, scrollLeft:number}>} */ 58 this._gridScrollPositions = {}; 59 60 /** @type {?CanvasAgent.ResourceId} */ 61 this._currentResourceId = null; 62 /** @type {!Array.<!Element>} */ 63 this._prevOptionsStack = []; 64 /** @type {!Array.<!Element>} */ 65 this._nextOptionsStack = []; 66 67 /** @type {!Array.<!WebInspector.DataGridNode>} */ 68 this._highlightedGridNodes = []; 69 70 var columns = [ 71 {title: WebInspector.UIString("Name"), sortable: false, width: "50%", disclosure: true}, 72 {title: WebInspector.UIString("Value"), sortable: false, width: "50%"} 73 ]; 74 75 this._stateGrid = new WebInspector.DataGrid(columns); 76 this._stateGrid.element.classList.add("fill"); 77 this._stateGrid.show(this.element); 78 79 this._traceLogPlayer.addEventListener(WebInspector.CanvasTraceLogPlayerProxy.Events.CanvasReplayStateChanged, this._onReplayResourceChanged, this); 80 this._traceLogPlayer.addEventListener(WebInspector.CanvasTraceLogPlayerProxy.Events.CanvasTraceLogReceived, this._onCanvasTraceLogReceived, this); 81 this._traceLogPlayer.addEventListener(WebInspector.CanvasTraceLogPlayerProxy.Events.CanvasResourceStateReceived, this._onCanvasResourceStateReceived, this); 82 83 this._updateButtonsEnabledState(); 84 } 85 86 WebInspector.CanvasReplayStateView.prototype = { 87 /** 88 * @param {string} resourceId 89 */ 90 selectResource: function(resourceId) 91 { 92 if (resourceId === this._resourceSelector.selectedOption().value) 93 return; 94 var option = this._resourceSelector.selectElement().firstChild; 95 for (var index = 0; option; ++index, option = option.nextSibling) { 96 if (resourceId === option.value) { 97 this._resourceSelector.setSelectedIndex(index); 98 this._onReplayResourceChanged(); 99 break; 100 } 101 } 102 }, 103 104 /** 105 * @param {!Element} parent 106 * @param {string} className 107 * @param {string} title 108 * @param {function(this:WebInspector.CanvasProfileView)} clickCallback 109 * @return {!WebInspector.StatusBarButton} 110 */ 111 _createControlButton: function(parent, className, title, clickCallback) 112 { 113 var button = new WebInspector.StatusBarButton(title, className + " canvas-replay-button"); 114 parent.appendChild(button.element); 115 116 button.makeLongClickEnabled(); 117 button.addEventListener("click", clickCallback, this); 118 button.addEventListener("longClickDown", clickCallback, this); 119 button.addEventListener("longClickPress", clickCallback, this); 120 return button; 121 }, 122 123 /** 124 * @param {boolean} forward 125 */ 126 _onResourceNavigationClick: function(forward) 127 { 128 var newOption = forward ? this._nextOptionsStack.pop() : this._prevOptionsStack.pop(); 129 if (!newOption) 130 return; 131 (forward ? this._prevOptionsStack : this._nextOptionsStack).push(this._currentOption); 132 this._isNavigationButton = true; 133 this.selectResource(newOption.value); 134 delete this._isNavigationButton; 135 this._updateButtonsEnabledState(); 136 }, 137 138 _onStateRefreshClick: function() 139 { 140 this._traceLogPlayer.clearResourceStates(); 141 }, 142 143 _updateButtonsEnabledState: function() 144 { 145 this._prevButton.setEnabled(this._prevOptionsStack.length > 0); 146 this._nextButton.setEnabled(this._nextOptionsStack.length > 0); 147 }, 148 149 _updateCurrentOption: function() 150 { 151 const maxStackSize = 256; 152 var selectedOption = this._resourceSelector.selectedOption(); 153 if (this._currentOption === selectedOption) 154 return; 155 if (!this._isNavigationButton) { 156 this._prevOptionsStack.push(this._currentOption); 157 this._nextOptionsStack = []; 158 if (this._prevOptionsStack.length > maxStackSize) 159 this._prevOptionsStack.shift(); 160 this._updateButtonsEnabledState(); 161 } 162 this._currentOption = selectedOption; 163 }, 164 165 /** 166 * @param {!CanvasAgent.TraceLog} traceLog 167 */ 168 _collectResourcesFromTraceLog: function(traceLog) 169 { 170 /** @type {!Array.<!CanvasAgent.CallArgument>} */ 171 var collectedResources = []; 172 var calls = traceLog.calls; 173 for (var i = 0, n = calls.length; i < n; ++i) { 174 var call = calls[i]; 175 var args = call.arguments || []; 176 for (var j = 0; j < args.length; ++j) 177 this._collectResourceFromCallArgument(args[j], collectedResources); 178 this._collectResourceFromCallArgument(call.result, collectedResources); 179 this._collectResourceFromCallArgument(call.value, collectedResources); 180 } 181 var contexts = traceLog.contexts; 182 for (var i = 0, n = contexts.length; i < n; ++i) 183 this._collectResourceFromCallArgument(contexts[i], collectedResources); 184 this._addCollectedResourcesToSelector(collectedResources); 185 }, 186 187 /** 188 * @param {!CanvasAgent.ResourceState} resourceState 189 */ 190 _collectResourcesFromResourceState: function(resourceState) 191 { 192 /** @type {!Array.<!CanvasAgent.CallArgument>} */ 193 var collectedResources = []; 194 this._collectResourceFromResourceStateDescriptors(resourceState.descriptors, collectedResources); 195 this._addCollectedResourcesToSelector(collectedResources); 196 }, 197 198 /** 199 * @param {!Array.<!CanvasAgent.ResourceStateDescriptor>|undefined} descriptors 200 * @param {!Array.<!CanvasAgent.CallArgument>} output 201 */ 202 _collectResourceFromResourceStateDescriptors: function(descriptors, output) 203 { 204 if (!descriptors) 205 return; 206 for (var i = 0, n = descriptors.length; i < n; ++i) { 207 var descriptor = descriptors[i]; 208 this._collectResourceFromCallArgument(descriptor.value, output); 209 this._collectResourceFromResourceStateDescriptors(descriptor.values, output); 210 } 211 }, 212 213 /** 214 * @param {!CanvasAgent.CallArgument|undefined} argument 215 * @param {!Array.<!CanvasAgent.CallArgument>} output 216 */ 217 _collectResourceFromCallArgument: function(argument, output) 218 { 219 if (!argument) 220 return; 221 var resourceId = argument.resourceId; 222 if (!resourceId || this._resourceIdToDescription[resourceId]) 223 return; 224 this._resourceIdToDescription[resourceId] = argument.description; 225 output.push(argument); 226 }, 227 228 /** 229 * @param {!Array.<!CanvasAgent.CallArgument>} collectedResources 230 */ 231 _addCollectedResourcesToSelector: function(collectedResources) 232 { 233 if (!collectedResources.length) 234 return; 235 /** 236 * @param {!CanvasAgent.CallArgument} arg1 237 * @param {!CanvasAgent.CallArgument} arg2 238 * @return {number} 239 */ 240 function comparator(arg1, arg2) 241 { 242 var a = arg1.description; 243 var b = arg2.description; 244 return String.naturalOrderComparator(a, b); 245 } 246 collectedResources.sort(comparator); 247 248 var selectElement = this._resourceSelector.selectElement(); 249 var currentOption = selectElement.firstChild; 250 currentOption = currentOption.nextSibling; // Skip the "<auto>" option. 251 for (var i = 0, n = collectedResources.length; i < n; ++i) { 252 var argument = collectedResources[i]; 253 while (currentOption && String.naturalOrderComparator(currentOption.text, argument.description) < 0) 254 currentOption = currentOption.nextSibling; 255 var option = this._resourceSelector.createOption(argument.description, WebInspector.UIString("Show state of this resource."), argument.resourceId); 256 if (currentOption) 257 selectElement.insertBefore(option, currentOption); 258 } 259 }, 260 261 _onReplayResourceChanged: function() 262 { 263 this._updateCurrentOption(); 264 var selectedResourceId = this._resourceSelector.selectedOption().value; 265 266 /** 267 * @param {?CanvasAgent.ResourceState} resourceState 268 * @this {WebInspector.CanvasReplayStateView} 269 */ 270 function didReceiveResourceState(resourceState) 271 { 272 if (selectedResourceId !== this._resourceSelector.selectedOption().value) 273 return; 274 this._showResourceState(resourceState); 275 } 276 this._traceLogPlayer.getResourceState(selectedResourceId, didReceiveResourceState.bind(this)); 277 }, 278 279 /** 280 * @param {!WebInspector.Event} event 281 */ 282 _onCanvasTraceLogReceived: function(event) 283 { 284 var traceLog = /** @type {!CanvasAgent.TraceLog} */ (event.data); 285 console.assert(traceLog); 286 this._collectResourcesFromTraceLog(traceLog); 287 }, 288 289 /** 290 * @param {!WebInspector.Event} event 291 */ 292 _onCanvasResourceStateReceived: function(event) 293 { 294 var resourceState = /** @type {!CanvasAgent.ResourceState} */ (event.data); 295 console.assert(resourceState); 296 this._collectResourcesFromResourceState(resourceState); 297 }, 298 299 /** 300 * @param {?CanvasAgent.ResourceState} resourceState 301 */ 302 _showResourceState: function(resourceState) 303 { 304 this._saveExpandedState(); 305 this._saveScrollState(); 306 307 var rootNode = this._stateGrid.rootNode(); 308 if (!resourceState) { 309 this._currentResourceId = null; 310 this._updateDataGridHighlights([]); 311 rootNode.removeChildren(); 312 return; 313 } 314 315 var nodesToHighlight = []; 316 var nameToOldGridNodes = {}; 317 318 /** 319 * @param {!Object} map 320 * @param {!WebInspector.DataGridNode=} node 321 */ 322 function populateNameToNodesMap(map, node) 323 { 324 if (!node) 325 return; 326 for (var i = 0, child; child = node.children[i]; ++i) { 327 var item = { 328 node: child, 329 children: {} 330 }; 331 map[child.name] = item; 332 populateNameToNodesMap(item.children, child); 333 } 334 } 335 populateNameToNodesMap(nameToOldGridNodes, rootNode); 336 rootNode.removeChildren(); 337 338 /** 339 * @param {!CanvasAgent.ResourceStateDescriptor} d1 340 * @param {!CanvasAgent.ResourceStateDescriptor} d2 341 * @return {number} 342 */ 343 function comparator(d1, d2) 344 { 345 var hasChildren1 = !!d1.values; 346 var hasChildren2 = !!d2.values; 347 if (hasChildren1 !== hasChildren2) 348 return hasChildren1 ? 1 : -1; 349 return String.naturalOrderComparator(d1.name, d2.name); 350 } 351 /** 352 * @param {!Array.<!CanvasAgent.ResourceStateDescriptor>|undefined} descriptors 353 * @param {!WebInspector.DataGridNode} parent 354 * @param {!Object=} nameToOldChildren 355 * @this {WebInspector.CanvasReplayStateView} 356 */ 357 function appendResourceStateDescriptors(descriptors, parent, nameToOldChildren) 358 { 359 descriptors = descriptors || []; 360 descriptors.sort(comparator); 361 var oldChildren = nameToOldChildren || {}; 362 for (var i = 0, n = descriptors.length; i < n; ++i) { 363 var descriptor = descriptors[i]; 364 var childNode = this._createDataGridNode(descriptor); 365 parent.appendChild(childNode); 366 var oldChildrenItem = oldChildren[childNode.name] || {}; 367 var oldChildNode = oldChildrenItem.node; 368 if (!oldChildNode || oldChildNode.element.textContent !== childNode.element.textContent) 369 nodesToHighlight.push(childNode); 370 appendResourceStateDescriptors.call(this, descriptor.values, childNode, oldChildrenItem.children); 371 } 372 } 373 appendResourceStateDescriptors.call(this, resourceState.descriptors, rootNode, nameToOldGridNodes); 374 375 var shouldHighlightChanges = (this._resourceKindId(this._currentResourceId) === this._resourceKindId(resourceState.id)); 376 this._currentResourceId = resourceState.id; 377 this._restoreExpandedState(); 378 this._updateDataGridHighlights(shouldHighlightChanges ? nodesToHighlight : []); 379 this._restoreScrollState(); 380 }, 381 382 /** 383 * @param {!Array.<!WebInspector.DataGridNode>} nodes 384 */ 385 _updateDataGridHighlights: function(nodes) 386 { 387 for (var i = 0, n = this._highlightedGridNodes.length; i < n; ++i) { 388 var node = this._highlightedGridNodes[i]; 389 node.element.classList.remove("canvas-grid-node-highlighted"); 390 } 391 392 this._highlightedGridNodes = nodes; 393 394 for (var i = 0, n = this._highlightedGridNodes.length; i < n; ++i) { 395 var node = this._highlightedGridNodes[i]; 396 node.element.classList.add("canvas-grid-node-highlighted"); 397 node.reveal(); 398 } 399 }, 400 401 /** 402 * @param {?CanvasAgent.ResourceId} resourceId 403 * @return {string} 404 */ 405 _resourceKindId: function(resourceId) 406 { 407 var description = (resourceId && this._resourceIdToDescription[resourceId]) || ""; 408 return description.replace(/\d+/g, ""); 409 }, 410 411 /** 412 * @param {function(!WebInspector.DataGridNode, string):void} callback 413 */ 414 _forEachGridNode: function(callback) 415 { 416 /** 417 * @param {!WebInspector.DataGridNode} node 418 * @param {string} key 419 */ 420 function processRecursively(node, key) 421 { 422 for (var i = 0, child; child = node.children[i]; ++i) { 423 var childKey = key + "#" + child.name; 424 callback(child, childKey); 425 processRecursively(child, childKey); 426 } 427 } 428 processRecursively(this._stateGrid.rootNode(), ""); 429 }, 430 431 _saveExpandedState: function() 432 { 433 if (!this._currentResourceId) 434 return; 435 var expandedState = {}; 436 var key = this._resourceKindId(this._currentResourceId); 437 this._gridNodesExpandedState[key] = expandedState; 438 /** 439 * @param {!WebInspector.DataGridNode} node 440 * @param {string} key 441 */ 442 function callback(node, key) 443 { 444 if (node.expanded) 445 expandedState[key] = true; 446 } 447 this._forEachGridNode(callback); 448 }, 449 450 _restoreExpandedState: function() 451 { 452 if (!this._currentResourceId) 453 return; 454 var key = this._resourceKindId(this._currentResourceId); 455 var expandedState = this._gridNodesExpandedState[key]; 456 if (!expandedState) 457 return; 458 /** 459 * @param {!WebInspector.DataGridNode} node 460 * @param {string} key 461 */ 462 function callback(node, key) 463 { 464 if (expandedState[key]) 465 node.expand(); 466 } 467 this._forEachGridNode(callback); 468 }, 469 470 _saveScrollState: function() 471 { 472 if (!this._currentResourceId) 473 return; 474 var key = this._resourceKindId(this._currentResourceId); 475 this._gridScrollPositions[key] = { 476 scrollTop: this._stateGrid.scrollContainer.scrollTop, 477 scrollLeft: this._stateGrid.scrollContainer.scrollLeft 478 }; 479 }, 480 481 _restoreScrollState: function() 482 { 483 if (!this._currentResourceId) 484 return; 485 var key = this._resourceKindId(this._currentResourceId); 486 var scrollState = this._gridScrollPositions[key]; 487 if (!scrollState) 488 return; 489 this._stateGrid.scrollContainer.scrollTop = scrollState.scrollTop; 490 this._stateGrid.scrollContainer.scrollLeft = scrollState.scrollLeft; 491 }, 492 493 /** 494 * @param {!CanvasAgent.ResourceStateDescriptor} descriptor 495 * @return {!WebInspector.DataGridNode} 496 */ 497 _createDataGridNode: function(descriptor) 498 { 499 var name = descriptor.name; 500 var callArgument = descriptor.value; 501 502 /** @type {!Element|string} */ 503 var valueElement = callArgument ? WebInspector.CanvasProfileDataGridHelper.createCallArgumentElement(callArgument) : ""; 504 505 /** @type {!Element|string} */ 506 var nameElement = name; 507 if (typeof descriptor.enumValueForName !== "undefined") 508 nameElement = WebInspector.CanvasProfileDataGridHelper.createEnumValueElement(name, +descriptor.enumValueForName); 509 510 if (descriptor.isArray && descriptor.values) { 511 if (typeof nameElement === "string") 512 nameElement += "[" + descriptor.values.length + "]"; 513 else { 514 var element = document.createElement("span"); 515 element.appendChild(nameElement); 516 element.createTextChild("[" + descriptor.values.length + "]"); 517 nameElement = element; 518 } 519 } 520 521 var data = {}; 522 data[0] = nameElement; 523 data[1] = valueElement; 524 var node = new WebInspector.DataGridNode(data); 525 node.selectable = false; 526 node.name = name; 527 return node; 528 }, 529 530 __proto__: WebInspector.View.prototype 531 } 532