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