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