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