• 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.CanvasTraceLogPlayerProxy} traceLogPlayer
35 */
36WebInspector.CanvasReplayStateView = function(traceLogPlayer)
37{
38    WebInspector.VBox.call(this);
39    this.registerRequiredCSS("canvasProfiler.css");
40    this.element.classList.add("canvas-replay-state-view");
41    this._traceLogPlayer = traceLogPlayer;
42
43    var controlsContainer = this.element.createChild("div", "status-bar");
44    this._prevButton = this._createControlButton(controlsContainer, "canvas-replay-state-prev", WebInspector.UIString("Previous resource."), this._onResourceNavigationClick.bind(this, false));
45    this._nextButton = this._createControlButton(controlsContainer, "canvas-replay-state-next", WebInspector.UIString("Next resource."), this._onResourceNavigationClick.bind(this, true));
46    this._createControlButton(controlsContainer, "canvas-replay-state-refresh", WebInspector.UIString("Refresh."), this._onStateRefreshClick.bind(this));
47
48    this._resourceSelector = new WebInspector.StatusBarComboBox(this._onReplayResourceChanged.bind(this));
49    this._currentOption = this._resourceSelector.createOption(WebInspector.UIString("<auto>"), WebInspector.UIString("Show state of the last replayed resource."), "");
50    controlsContainer.appendChild(this._resourceSelector.element);
51
52    /** @type {!Object.<string, string>} */
53    this._resourceIdToDescription = {};
54
55    /** @type {!Object.<string, !Object.<string, boolean>>} */
56    this._gridNodesExpandedState = {};
57    /** @type {!Object.<string, !{scrollTop: number, scrollLeft: number}>} */
58    this._gridScrollPositions = {};
59
60    /** @type {?CanvasAgent.ResourceId} */
61    this._currentResourceId = null;
62    /** @type {!Array.<!Element>} */
63    this._prevOptionsStack = [];
64    /** @type {!Array.<!Element>} */
65    this._nextOptionsStack = [];
66
67    /** @type {!Array.<!WebInspector.DataGridNode>} */
68    this._highlightedGridNodes = [];
69
70    var columns = [
71        {title: WebInspector.UIString("Name"), sortable: false, width: "50%", disclosure: true},
72        {title: WebInspector.UIString("Value"), sortable: false, width: "50%"}
73    ];
74
75    this._stateGrid = new WebInspector.DataGrid(columns);
76    this._stateGrid.element.classList.add("fill");
77    this._stateGrid.show(this.element);
78
79    this._traceLogPlayer.addEventListener(WebInspector.CanvasTraceLogPlayerProxy.Events.CanvasReplayStateChanged, this._onReplayResourceChanged, this);
80    this._traceLogPlayer.addEventListener(WebInspector.CanvasTraceLogPlayerProxy.Events.CanvasTraceLogReceived, this._onCanvasTraceLogReceived, this);
81    this._traceLogPlayer.addEventListener(WebInspector.CanvasTraceLogPlayerProxy.Events.CanvasResourceStateReceived, this._onCanvasResourceStateReceived, this);
82
83    this._updateButtonsEnabledState();
84}
85
86WebInspector.CanvasReplayStateView.prototype = {
87    /**
88     * @param {string} resourceId
89     */
90    selectResource: function(resourceId)
91    {
92        if (resourceId === this._resourceSelector.selectedOption().value)
93            return;
94        var option = this._resourceSelector.selectElement().firstChild;
95        for (var index = 0; option; ++index, option = option.nextSibling) {
96            if (resourceId === option.value) {
97                this._resourceSelector.setSelectedIndex(index);
98                this._onReplayResourceChanged();
99                break;
100            }
101        }
102    },
103
104    /**
105     * @param {!Element} parent
106     * @param {string} className
107     * @param {string} title
108     * @param {function(this:WebInspector.CanvasProfileView)} clickCallback
109     * @return {!WebInspector.StatusBarButton}
110     */
111    _createControlButton: function(parent, className, title, clickCallback)
112    {
113        var button = new WebInspector.StatusBarButton(title, className + " canvas-replay-button");
114        parent.appendChild(button.element);
115
116        button.makeLongClickEnabled();
117        button.addEventListener("click", clickCallback, this);
118        button.addEventListener("longClickDown", clickCallback, this);
119        button.addEventListener("longClickPress", clickCallback, this);
120        return button;
121    },
122
123    /**
124     * @param {boolean} forward
125     */
126    _onResourceNavigationClick: function(forward)
127    {
128        var newOption = forward ? this._nextOptionsStack.pop() : this._prevOptionsStack.pop();
129        if (!newOption)
130            return;
131        (forward ? this._prevOptionsStack : this._nextOptionsStack).push(this._currentOption);
132        this._isNavigationButton = true;
133        this.selectResource(newOption.value);
134        delete this._isNavigationButton;
135        this._updateButtonsEnabledState();
136    },
137
138    _onStateRefreshClick: function()
139    {
140        this._traceLogPlayer.clearResourceStates();
141    },
142
143    _updateButtonsEnabledState: function()
144    {
145        this._prevButton.setEnabled(this._prevOptionsStack.length > 0);
146        this._nextButton.setEnabled(this._nextOptionsStack.length > 0);
147    },
148
149    _updateCurrentOption: function()
150    {
151        const maxStackSize = 256;
152        var selectedOption = this._resourceSelector.selectedOption();
153        if (this._currentOption === selectedOption)
154            return;
155        if (!this._isNavigationButton) {
156            this._prevOptionsStack.push(this._currentOption);
157            this._nextOptionsStack = [];
158            if (this._prevOptionsStack.length > maxStackSize)
159                this._prevOptionsStack.shift();
160            this._updateButtonsEnabledState();
161        }
162        this._currentOption = selectedOption;
163    },
164
165    /**
166     * @param {!CanvasAgent.TraceLog} traceLog
167     */
168    _collectResourcesFromTraceLog: function(traceLog)
169    {
170        /** @type {!Array.<!CanvasAgent.CallArgument>} */
171        var collectedResources = [];
172        var calls = traceLog.calls;
173        for (var i = 0, n = calls.length; i < n; ++i) {
174            var call = calls[i];
175            var args = call.arguments || [];
176            for (var j = 0; j < args.length; ++j)
177                this._collectResourceFromCallArgument(args[j], collectedResources);
178            this._collectResourceFromCallArgument(call.result, collectedResources);
179            this._collectResourceFromCallArgument(call.value, collectedResources);
180        }
181        var contexts = traceLog.contexts;
182        for (var i = 0, n = contexts.length; i < n; ++i)
183            this._collectResourceFromCallArgument(contexts[i], collectedResources);
184        this._addCollectedResourcesToSelector(collectedResources);
185    },
186
187    /**
188     * @param {!CanvasAgent.ResourceState} resourceState
189     */
190    _collectResourcesFromResourceState: function(resourceState)
191    {
192        /** @type {!Array.<!CanvasAgent.CallArgument>} */
193        var collectedResources = [];
194        this._collectResourceFromResourceStateDescriptors(resourceState.descriptors, collectedResources);
195        this._addCollectedResourcesToSelector(collectedResources);
196    },
197
198    /**
199     * @param {!Array.<!CanvasAgent.ResourceStateDescriptor>|undefined} descriptors
200     * @param {!Array.<!CanvasAgent.CallArgument>} output
201     */
202    _collectResourceFromResourceStateDescriptors: function(descriptors, output)
203    {
204        if (!descriptors)
205            return;
206        for (var i = 0, n = descriptors.length; i < n; ++i) {
207            var descriptor = descriptors[i];
208            this._collectResourceFromCallArgument(descriptor.value, output);
209            this._collectResourceFromResourceStateDescriptors(descriptor.values, output);
210        }
211    },
212
213    /**
214     * @param {!CanvasAgent.CallArgument|undefined} argument
215     * @param {!Array.<!CanvasAgent.CallArgument>} output
216     */
217    _collectResourceFromCallArgument: function(argument, output)
218    {
219        if (!argument)
220            return;
221        var resourceId = argument.resourceId;
222        if (!resourceId || this._resourceIdToDescription[resourceId])
223            return;
224        this._resourceIdToDescription[resourceId] = argument.description;
225        output.push(argument);
226    },
227
228    /**
229     * @param {!Array.<!CanvasAgent.CallArgument>} collectedResources
230     */
231    _addCollectedResourcesToSelector: function(collectedResources)
232    {
233        if (!collectedResources.length)
234            return;
235        /**
236         * @param {!CanvasAgent.CallArgument} arg1
237         * @param {!CanvasAgent.CallArgument} arg2
238         * @return {number}
239         */
240        function comparator(arg1, arg2)
241        {
242            var a = arg1.description;
243            var b = arg2.description;
244            return String.naturalOrderComparator(a, b);
245        }
246        collectedResources.sort(comparator);
247
248        var selectElement = this._resourceSelector.selectElement();
249        var currentOption = selectElement.firstChild;
250        currentOption = currentOption.nextSibling; // Skip the "<auto>" option.
251        for (var i = 0, n = collectedResources.length; i < n; ++i) {
252            var argument = collectedResources[i];
253            while (currentOption && String.naturalOrderComparator(currentOption.text, argument.description) < 0)
254                currentOption = currentOption.nextSibling;
255            var option = this._resourceSelector.createOption(argument.description, WebInspector.UIString("Show state of this resource."), argument.resourceId);
256            if (currentOption)
257                selectElement.insertBefore(option, currentOption);
258        }
259    },
260
261    _onReplayResourceChanged: function()
262    {
263        this._updateCurrentOption();
264        var selectedResourceId = this._resourceSelector.selectedOption().value;
265
266        /**
267         * @param {?CanvasAgent.ResourceState} resourceState
268         * @this {WebInspector.CanvasReplayStateView}
269         */
270        function didReceiveResourceState(resourceState)
271        {
272            if (selectedResourceId !== this._resourceSelector.selectedOption().value)
273                return;
274            this._showResourceState(resourceState);
275        }
276        this._traceLogPlayer.getResourceState(selectedResourceId, didReceiveResourceState.bind(this));
277    },
278
279    /**
280     * @param {!WebInspector.Event} event
281     */
282    _onCanvasTraceLogReceived: function(event)
283    {
284        var traceLog = /** @type {!CanvasAgent.TraceLog} */ (event.data);
285        console.assert(traceLog);
286        this._collectResourcesFromTraceLog(traceLog);
287    },
288
289    /**
290     * @param {!WebInspector.Event} event
291     */
292    _onCanvasResourceStateReceived: function(event)
293    {
294        var resourceState = /** @type {!CanvasAgent.ResourceState} */ (event.data);
295        console.assert(resourceState);
296        this._collectResourcesFromResourceState(resourceState);
297    },
298
299    /**
300     * @param {?CanvasAgent.ResourceState} resourceState
301     */
302    _showResourceState: function(resourceState)
303    {
304        this._saveExpandedState();
305        this._saveScrollState();
306
307        var rootNode = this._stateGrid.rootNode();
308        if (!resourceState) {
309            this._currentResourceId = null;
310            this._updateDataGridHighlights([]);
311            rootNode.removeChildren();
312            return;
313        }
314
315        var nodesToHighlight = [];
316        var nameToOldGridNodes = {};
317
318        /**
319         * @param {!Object} map
320         * @param {!WebInspector.DataGridNode=} node
321         */
322        function populateNameToNodesMap(map, node)
323        {
324            if (!node)
325                return;
326            for (var i = 0, child; child = node.children[i]; ++i) {
327                var item = {
328                    node: child,
329                    children: {}
330                };
331                map[child.name] = item;
332                populateNameToNodesMap(item.children, child);
333            }
334        }
335        populateNameToNodesMap(nameToOldGridNodes, rootNode);
336        rootNode.removeChildren();
337
338        /**
339         * @param {!CanvasAgent.ResourceStateDescriptor} d1
340         * @param {!CanvasAgent.ResourceStateDescriptor} d2
341         * @return {number}
342         */
343        function comparator(d1, d2)
344        {
345            var hasChildren1 = !!d1.values;
346            var hasChildren2 = !!d2.values;
347            if (hasChildren1 !== hasChildren2)
348                return hasChildren1 ? 1 : -1;
349            return String.naturalOrderComparator(d1.name, d2.name);
350        }
351        /**
352         * @param {!Array.<!CanvasAgent.ResourceStateDescriptor>|undefined} descriptors
353         * @param {!WebInspector.DataGridNode} parent
354         * @param {!Object=} nameToOldChildren
355         * @this {WebInspector.CanvasReplayStateView}
356         */
357        function appendResourceStateDescriptors(descriptors, parent, nameToOldChildren)
358        {
359            descriptors = descriptors || [];
360            descriptors.sort(comparator);
361            var oldChildren = nameToOldChildren || {};
362            for (var i = 0, n = descriptors.length; i < n; ++i) {
363                var descriptor = descriptors[i];
364                var childNode = this._createDataGridNode(descriptor);
365                parent.appendChild(childNode);
366                var oldChildrenItem = oldChildren[childNode.name] || {};
367                var oldChildNode = oldChildrenItem.node;
368                if (!oldChildNode || oldChildNode.element.textContent !== childNode.element.textContent)
369                    nodesToHighlight.push(childNode);
370                appendResourceStateDescriptors.call(this, descriptor.values, childNode, oldChildrenItem.children);
371            }
372        }
373        appendResourceStateDescriptors.call(this, resourceState.descriptors, rootNode, nameToOldGridNodes);
374
375        var shouldHighlightChanges = (this._resourceKindId(this._currentResourceId) === this._resourceKindId(resourceState.id));
376        this._currentResourceId = resourceState.id;
377        this._restoreExpandedState();
378        this._updateDataGridHighlights(shouldHighlightChanges ? nodesToHighlight : []);
379        this._restoreScrollState();
380    },
381
382    /**
383     * @param {!Array.<!WebInspector.DataGridNode>} nodes
384     */
385    _updateDataGridHighlights: function(nodes)
386    {
387        for (var i = 0, n = this._highlightedGridNodes.length; i < n; ++i)
388            this._highlightedGridNodes[i].element.classList.remove("canvas-grid-node-highlighted");
389
390        this._highlightedGridNodes = nodes;
391
392        for (var i = 0, n = this._highlightedGridNodes.length; i < n; ++i) {
393            var node = this._highlightedGridNodes[i];
394            WebInspector.runCSSAnimationOnce(node.element, "canvas-grid-node-highlighted");
395            node.reveal();
396        }
397    },
398
399    /**
400     * @param {?CanvasAgent.ResourceId} resourceId
401     * @return {string}
402     */
403    _resourceKindId: function(resourceId)
404    {
405        var description = (resourceId && this._resourceIdToDescription[resourceId]) || "";
406        return description.replace(/\d+/g, "");
407    },
408
409    /**
410     * @param {function(!WebInspector.DataGridNode, string):void} callback
411     */
412    _forEachGridNode: function(callback)
413    {
414        /**
415         * @param {!WebInspector.DataGridNode} node
416         * @param {string} key
417         */
418        function processRecursively(node, key)
419        {
420            for (var i = 0, child; child = node.children[i]; ++i) {
421                var childKey = key + "#" + child.name;
422                callback(child, childKey);
423                processRecursively(child, childKey);
424            }
425        }
426        processRecursively(this._stateGrid.rootNode(), "");
427    },
428
429    _saveExpandedState: function()
430    {
431        if (!this._currentResourceId)
432            return;
433        var expandedState = {};
434        var key = this._resourceKindId(this._currentResourceId);
435        this._gridNodesExpandedState[key] = expandedState;
436        /**
437         * @param {!WebInspector.DataGridNode} node
438         * @param {string} key
439         */
440        function callback(node, key)
441        {
442            if (node.expanded)
443                expandedState[key] = true;
444        }
445        this._forEachGridNode(callback);
446    },
447
448    _restoreExpandedState: function()
449    {
450        if (!this._currentResourceId)
451            return;
452        var key = this._resourceKindId(this._currentResourceId);
453        var expandedState = this._gridNodesExpandedState[key];
454        if (!expandedState)
455            return;
456        /**
457         * @param {!WebInspector.DataGridNode} node
458         * @param {string} key
459         */
460        function callback(node, key)
461        {
462            if (expandedState[key])
463                node.expand();
464        }
465        this._forEachGridNode(callback);
466    },
467
468    _saveScrollState: function()
469    {
470        if (!this._currentResourceId)
471            return;
472        var key = this._resourceKindId(this._currentResourceId);
473        this._gridScrollPositions[key] = {
474            scrollTop: this._stateGrid.scrollContainer.scrollTop,
475            scrollLeft: this._stateGrid.scrollContainer.scrollLeft
476        };
477    },
478
479    _restoreScrollState: function()
480    {
481        if (!this._currentResourceId)
482            return;
483        var key = this._resourceKindId(this._currentResourceId);
484        var scrollState = this._gridScrollPositions[key];
485        if (!scrollState)
486            return;
487        this._stateGrid.scrollContainer.scrollTop = scrollState.scrollTop;
488        this._stateGrid.scrollContainer.scrollLeft = scrollState.scrollLeft;
489    },
490
491    /**
492     * @param {!CanvasAgent.ResourceStateDescriptor} descriptor
493     * @return {!WebInspector.DataGridNode}
494     */
495    _createDataGridNode: function(descriptor)
496    {
497        var name = descriptor.name;
498        var callArgument = descriptor.value;
499
500        /** @type {!Element|string} */
501        var valueElement = callArgument ? WebInspector.CanvasProfileDataGridHelper.createCallArgumentElement(callArgument) : "";
502
503        /** @type {!Element|string} */
504        var nameElement = name;
505        if (typeof descriptor.enumValueForName !== "undefined")
506            nameElement = WebInspector.CanvasProfileDataGridHelper.createEnumValueElement(name, +descriptor.enumValueForName);
507
508        if (descriptor.isArray && descriptor.values) {
509            if (typeof nameElement === "string")
510                nameElement += "[" + descriptor.values.length + "]";
511            else {
512                var element = document.createElement("span");
513                element.appendChild(nameElement);
514                element.createTextChild("[" + descriptor.values.length + "]");
515                nameElement = element;
516            }
517        }
518
519        var data = {};
520        data[0] = nameElement;
521        data[1] = valueElement;
522        var node = new WebInspector.DataGridNode(data);
523        node.selectable = false;
524        node.name = name;
525        return node;
526    },
527
528    __proto__: WebInspector.VBox.prototype
529}
530