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 */
36WebInspector.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 */
94WebInspector.CanvasProfileView.TraceLogPollingInterval = 500;
95
96WebInspector.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 */
595WebInspector.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
628WebInspector.CanvasProfileType.TypeId = "CANVAS_PROFILE";
629
630WebInspector.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 */
962WebInspector.CanvasDispatcher = function(profileType)
963{
964    this._profileType = profileType;
965    InspectorBackend.registerCanvasDispatcher(this);
966}
967
968WebInspector.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 */
996WebInspector.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
1006WebInspector.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