Home | History | Annotate | Download | only in front_end
      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