• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (C) 2007, 2008 Apple Inc.  All rights reserved.
3 * Copyright (C) 2008, 2009 Anthony Ricaud <rik@webkit.org>
4 * Copyright (C) 2009 Google Inc. All rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
8 * are met:
9 *
10 * 1.  Redistributions of source code must retain the above copyright
11 *     notice, this list of conditions and the following disclaimer.
12 * 2.  Redistributions in binary form must reproduce the above copyright
13 *     notice, this list of conditions and the following disclaimer in the
14 *     documentation and/or other materials provided with the distribution.
15 * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
16 *     its contributors may be used to endorse or promote products derived
17 *     from this software without specific prior written permission.
18 *
19 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
20 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
23 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
26 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31WebInspector.AbstractTimelinePanel = function()
32{
33    WebInspector.Panel.call(this);
34    this._items = [];
35    this._staleItems = [];
36}
37
38WebInspector.AbstractTimelinePanel.prototype = {
39    get categories()
40    {
41        // Should be implemented by the concrete subclasses.
42        return {};
43    },
44
45    populateSidebar: function()
46    {
47        // Should be implemented by the concrete subclasses.
48    },
49
50    createItemTreeElement: function(item)
51    {
52        // Should be implemented by the concrete subclasses.
53    },
54
55    createItemGraph: function(item)
56    {
57        // Should be implemented by the concrete subclasses.
58    },
59
60    get items()
61    {
62        return this._items;
63    },
64
65    createInterface: function()
66    {
67        this.containerElement = document.createElement("div");
68        this.containerElement.id = "resources-container";
69        this.containerElement.addEventListener("scroll", this._updateDividersLabelBarPosition.bind(this), false);
70        this.element.appendChild(this.containerElement);
71
72        this.createSidebar(this.containerElement, this.element);
73        this.sidebarElement.id = "resources-sidebar";
74        this.populateSidebar();
75
76        this._containerContentElement = document.createElement("div");
77        this._containerContentElement.id = "resources-container-content";
78        this.containerElement.appendChild(this._containerContentElement);
79
80        this.summaryBar = new WebInspector.SummaryBar(this.categories);
81        this.summaryBar.element.id = "resources-summary";
82        this._containerContentElement.appendChild(this.summaryBar.element);
83
84        this._timelineGrid = new WebInspector.TimelineGrid();
85        this._containerContentElement.appendChild(this._timelineGrid.element);
86        this.itemsGraphsElement = this._timelineGrid.itemsGraphsElement;
87    },
88
89    createFilterPanel: function()
90    {
91        this.filterBarElement = document.createElement("div");
92        this.filterBarElement.id = "resources-filter";
93        this.filterBarElement.className = "scope-bar";
94        this.element.appendChild(this.filterBarElement);
95
96        function createFilterElement(category)
97        {
98            if (category === "all")
99                var label = WebInspector.UIString("All");
100            else if (this.categories[category])
101                var label = this.categories[category].title;
102
103            var categoryElement = document.createElement("li");
104            categoryElement.category = category;
105            categoryElement.addStyleClass(category);
106            categoryElement.appendChild(document.createTextNode(label));
107            categoryElement.addEventListener("click", this._updateFilter.bind(this), false);
108            this.filterBarElement.appendChild(categoryElement);
109
110            return categoryElement;
111        }
112
113        this.filterAllElement = createFilterElement.call(this, "all");
114
115        // Add a divider
116        var dividerElement = document.createElement("div");
117        dividerElement.addStyleClass("divider");
118        this.filterBarElement.appendChild(dividerElement);
119
120        for (var category in this.categories)
121            createFilterElement.call(this, category);
122    },
123
124    showCategory: function(category)
125    {
126        var filterClass = "filter-" + category.toLowerCase();
127        this.itemsGraphsElement.addStyleClass(filterClass);
128        this.itemsTreeElement.childrenListElement.addStyleClass(filterClass);
129    },
130
131    hideCategory: function(category)
132    {
133        var filterClass = "filter-" + category.toLowerCase();
134        this.itemsGraphsElement.removeStyleClass(filterClass);
135        this.itemsTreeElement.childrenListElement.removeStyleClass(filterClass);
136    },
137
138    filter: function(target, selectMultiple)
139    {
140        function unselectAll()
141        {
142            for (var i = 0; i < this.filterBarElement.childNodes.length; ++i) {
143                var child = this.filterBarElement.childNodes[i];
144                if (!child.category)
145                    continue;
146
147                child.removeStyleClass("selected");
148                this.hideCategory(child.category);
149            }
150        }
151
152        if (target === this.filterAllElement) {
153            if (target.hasStyleClass("selected")) {
154                // We can't unselect All, so we break early here
155                return;
156            }
157
158            // If All wasn't selected, and now is, unselect everything else.
159            unselectAll.call(this);
160        } else {
161            // Something other than All is being selected, so we want to unselect All.
162            if (this.filterAllElement.hasStyleClass("selected")) {
163                this.filterAllElement.removeStyleClass("selected");
164                this.hideCategory("all");
165            }
166        }
167
168        if (!selectMultiple) {
169            // If multiple selection is off, we want to unselect everything else
170            // and just select ourselves.
171            unselectAll.call(this);
172
173            target.addStyleClass("selected");
174            this.showCategory(target.category);
175            return;
176        }
177
178        if (target.hasStyleClass("selected")) {
179            // If selectMultiple is turned on, and we were selected, we just
180            // want to unselect ourselves.
181            target.removeStyleClass("selected");
182            this.hideCategory(target.category);
183        } else {
184            // If selectMultiple is turned on, and we weren't selected, we just
185            // want to select ourselves.
186            target.addStyleClass("selected");
187            this.showCategory(target.category);
188        }
189    },
190
191    _updateFilter: function(e)
192    {
193        var isMac = WebInspector.isMac();
194        var selectMultiple = false;
195        if (isMac && e.metaKey && !e.ctrlKey && !e.altKey && !e.shiftKey)
196            selectMultiple = true;
197        if (!isMac && e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey)
198            selectMultiple = true;
199
200        this.filter(e.target, selectMultiple);
201
202        // When we are updating our filtering, scroll to the top so we don't end up
203        // in blank graph under all the resources.
204        this.containerElement.scrollTop = 0;
205    },
206
207    updateGraphDividersIfNeeded: function(force)
208    {
209        if (!this.visible) {
210            this.needsRefresh = true;
211            return false;
212        }
213        return this._timelineGrid.updateDividers(force, this.calculator);
214    },
215
216    _updateDividersLabelBarPosition: function()
217    {
218        const scrollTop = this.containerElement.scrollTop;
219        const offsetHeight = this.summaryBar.element.offsetHeight;
220        const dividersTop = (scrollTop < offsetHeight ? offsetHeight : scrollTop);
221        this._timelineGrid.setScrollAndDividerTop(scrollTop, dividersTop);
222    },
223
224    get needsRefresh()
225    {
226        return this._needsRefresh;
227    },
228
229    set needsRefresh(x)
230    {
231        if (this._needsRefresh === x)
232            return;
233
234        this._needsRefresh = x;
235
236        if (x) {
237            if (this.visible && !("_refreshTimeout" in this))
238                this._refreshTimeout = setTimeout(this.refresh.bind(this), 500);
239        } else {
240            if ("_refreshTimeout" in this) {
241                clearTimeout(this._refreshTimeout);
242                delete this._refreshTimeout;
243            }
244        }
245    },
246
247    refreshIfNeeded: function()
248    {
249        if (this.needsRefresh)
250            this.refresh();
251    },
252
253    show: function()
254    {
255        WebInspector.Panel.prototype.show.call(this);
256
257        this._updateDividersLabelBarPosition();
258        this.refreshIfNeeded();
259    },
260
261    resize: function()
262    {
263        WebInspector.Panel.prototype.resize.call(this);
264
265        this.updateGraphDividersIfNeeded();
266    },
267
268    updateMainViewWidth: function(width)
269    {
270        this._containerContentElement.style.left = width + "px";
271        this.resize();
272    },
273
274    invalidateAllItems: function()
275    {
276        this._staleItems = this._items.slice();
277    },
278
279    refresh: function()
280    {
281        this.needsRefresh = false;
282
283        var staleItemsLength = this._staleItems.length;
284
285        var boundariesChanged = false;
286
287        for (var i = 0; i < staleItemsLength; ++i) {
288            var item = this._staleItems[i];
289            if (!item._itemsTreeElement) {
290                // Create the timeline tree element and graph.
291                item._itemsTreeElement = this.createItemTreeElement(item);
292                item._itemsTreeElement._itemGraph = this.createItemGraph(item);
293
294                this.itemsTreeElement.appendChild(item._itemsTreeElement);
295                this.itemsGraphsElement.appendChild(item._itemsTreeElement._itemGraph.graphElement);
296            }
297
298            if (item._itemsTreeElement.refresh)
299                item._itemsTreeElement.refresh();
300
301            if (this.calculator.updateBoundaries(item))
302                boundariesChanged = true;
303        }
304
305        if (boundariesChanged) {
306            // The boundaries changed, so all item graphs are stale.
307            this._staleItems = this._items.slice();
308            staleItemsLength = this._staleItems.length;
309        }
310
311        for (var i = 0; i < staleItemsLength; ++i)
312            this._staleItems[i]._itemsTreeElement._itemGraph.refresh(this.calculator);
313
314        this._staleItems = [];
315
316        this.updateGraphDividersIfNeeded();
317    },
318
319    reset: function()
320    {
321        this.containerElement.scrollTop = 0;
322
323        if (this._calculator)
324            this._calculator.reset();
325
326        if (this._items) {
327            var itemsLength = this._items.length;
328            for (var i = 0; i < itemsLength; ++i) {
329                var item = this._items[i];
330                delete item._itemsTreeElement;
331            }
332        }
333
334        this._items = [];
335        this._staleItems = [];
336
337        this.itemsTreeElement.removeChildren();
338        this.itemsGraphsElement.removeChildren();
339
340        this.updateGraphDividersIfNeeded(true);
341    },
342
343    get calculator()
344    {
345        return this._calculator;
346    },
347
348    set calculator(x)
349    {
350        if (!x || this._calculator === x)
351            return;
352
353        this._calculator = x;
354        this._calculator.reset();
355
356        this._staleItems = this._items.slice();
357        this.refresh();
358    },
359
360    addItem: function(item)
361    {
362        this._items.push(item);
363        this.refreshItem(item);
364    },
365
366    removeItem: function(item)
367    {
368        this._items.remove(item, true);
369
370        if (item._itemsTreeElement) {
371            this.itemsTreeElement.removeChild(item._itemsTreeElement);
372            this.itemsGraphsElement.removeChild(item._itemsTreeElement._itemGraph.graphElement);
373        }
374
375        delete item._itemsTreeElement;
376        this.adjustScrollPosition();
377    },
378
379    refreshItem: function(item)
380    {
381        this._staleItems.push(item);
382        this.needsRefresh = true;
383    },
384
385    revealAndSelectItem: function(item)
386    {
387        if (item._itemsTreeElement) {
388            item._itemsTreeElement.reveal();
389            item._itemsTreeElement.select(true);
390        }
391    },
392
393    sortItems: function(sortingFunction)
394    {
395        var sortedElements = [].concat(this.itemsTreeElement.children);
396        sortedElements.sort(sortingFunction);
397
398        var sortedElementsLength = sortedElements.length;
399        for (var i = 0; i < sortedElementsLength; ++i) {
400            var treeElement = sortedElements[i];
401            if (treeElement === this.itemsTreeElement.children[i])
402                continue;
403
404            var wasSelected = treeElement.selected;
405            this.itemsTreeElement.removeChild(treeElement);
406            this.itemsTreeElement.insertChild(treeElement, i);
407            if (wasSelected)
408                treeElement.select(true);
409
410            var graphElement = treeElement._itemGraph.graphElement;
411            this.itemsGraphsElement.insertBefore(graphElement, this.itemsGraphsElement.children[i]);
412        }
413    },
414
415    adjustScrollPosition: function()
416    {
417        // Prevent the container from being scrolled off the end.
418        if ((this.containerElement.scrollTop + this.containerElement.offsetHeight) > this.sidebarElement.offsetHeight)
419            this.containerElement.scrollTop = (this.sidebarElement.offsetHeight - this.containerElement.offsetHeight);
420    },
421
422    addEventDivider: function(divider)
423    {
424        this._timelineGrid.addEventDivider(divider);
425    }
426}
427
428WebInspector.AbstractTimelinePanel.prototype.__proto__ = WebInspector.Panel.prototype;
429
430WebInspector.AbstractTimelineCalculator = function()
431{
432}
433
434WebInspector.AbstractTimelineCalculator.prototype = {
435    computeSummaryValues: function(items)
436    {
437        var total = 0;
438        var categoryValues = {};
439
440        var itemsLength = items.length;
441        for (var i = 0; i < itemsLength; ++i) {
442            var item = items[i];
443            var value = this._value(item);
444            if (typeof value === "undefined")
445                continue;
446            if (!(item.category.name in categoryValues))
447                categoryValues[item.category.name] = 0;
448            categoryValues[item.category.name] += value;
449            total += value;
450        }
451
452        return {categoryValues: categoryValues, total: total};
453    },
454
455    computeBarGraphPercentages: function(item)
456    {
457        return {start: 0, middle: 0, end: (this._value(item) / this.boundarySpan) * 100};
458    },
459
460    computeBarGraphLabels: function(item)
461    {
462        const label = this.formatValue(this._value(item));
463        return {left: label, right: label, tooltip: label};
464    },
465
466    get boundarySpan()
467    {
468        return this.maximumBoundary - this.minimumBoundary;
469    },
470
471    updateBoundaries: function(item)
472    {
473        this.minimumBoundary = 0;
474
475        var value = this._value(item);
476        if (typeof this.maximumBoundary === "undefined" || value > this.maximumBoundary) {
477            this.maximumBoundary = value;
478            return true;
479        }
480        return false;
481    },
482
483    reset: function()
484    {
485        delete this.minimumBoundary;
486        delete this.maximumBoundary;
487    },
488
489    _value: function(item)
490    {
491        return 0;
492    },
493
494    formatValue: function(value)
495    {
496        return value.toString();
497    }
498}
499
500WebInspector.AbstractTimelineCategory = function(name, title, color)
501{
502    this.name = name;
503    this.title = title;
504    this.color = color;
505}
506
507WebInspector.AbstractTimelineCategory.prototype = {
508    toString: function()
509    {
510        return this.title;
511    }
512}
513