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