• 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
31/**
32 * @constructor
33 */
34WebInspector.TimelineGrid = function()
35{
36    this.element = document.createElement("div");
37
38    this._dividersElement = this.element.createChild("div", "resources-dividers");
39
40    this._gridHeaderElement = document.createElement("div");
41    this._gridHeaderElement.id = "timeline-grid-header";
42    this._eventDividersElement = this._gridHeaderElement.createChild("div", "resources-event-dividers");
43    this._dividersLabelBarElement = this._gridHeaderElement.createChild("div", "resources-dividers-label-bar");
44    this.element.appendChild(this._gridHeaderElement);
45
46    this._leftCurtainElement = this.element.createChild("div", "timeline-cpu-curtain-left");
47    this._rightCurtainElement = this.element.createChild("div", "timeline-cpu-curtain-right");
48}
49
50/**
51 * @param {!WebInspector.TimelineGrid.Calculator} calculator
52 * @param {number} clientWidth
53 * @return {!{offsets: !Array.<number>, precision: number}}
54 */
55WebInspector.TimelineGrid.calculateDividerOffsets = function(calculator, clientWidth)
56{
57    const minGridSlicePx = 64; // minimal distance between grid lines.
58    const gridFreeZoneAtLeftPx = 50;
59
60    var dividersCount = clientWidth / minGridSlicePx;
61    var gridSliceTime = calculator.boundarySpan() / dividersCount;
62    var pixelsPerTime = clientWidth / calculator.boundarySpan();
63
64    // Align gridSliceTime to a nearest round value.
65    // We allow spans that fit into the formula: span = (1|2|5)x10^n,
66    // e.g.: ...  .1  .2  .5  1  2  5  10  20  50  ...
67    // After a span has been chosen make grid lines at multiples of the span.
68
69    var logGridSliceTime = Math.ceil(Math.log(gridSliceTime) / Math.LN10);
70    gridSliceTime = Math.pow(10, logGridSliceTime);
71    if (gridSliceTime * pixelsPerTime >= 5 * minGridSlicePx)
72        gridSliceTime = gridSliceTime / 5;
73    if (gridSliceTime * pixelsPerTime >= 2 * minGridSlicePx)
74        gridSliceTime = gridSliceTime / 2;
75
76    var firstDividerTime = Math.ceil((calculator.minimumBoundary() - calculator.zeroTime()) / gridSliceTime) * gridSliceTime + calculator.zeroTime();
77    var lastDividerTime = calculator.maximumBoundary();
78    // Add some extra space past the right boundary as the rightmost divider label text
79    // may be partially shown rather than just pop up when a new rightmost divider gets into the view.
80    if (calculator.paddingLeft() > 0)
81        lastDividerTime = lastDividerTime + minGridSlicePx / pixelsPerTime;
82    dividersCount = Math.ceil((lastDividerTime - firstDividerTime) / gridSliceTime);
83
84    var skipLeftmostDividers = calculator.paddingLeft() === 0;
85
86    if (!gridSliceTime)
87        dividersCount = 0;
88
89    var offsets = [];
90    for (var i = 0; i < dividersCount; ++i) {
91        var left = calculator.computePosition(firstDividerTime + gridSliceTime * i);
92        if (skipLeftmostDividers && left < gridFreeZoneAtLeftPx)
93            continue;
94        offsets.push(firstDividerTime + gridSliceTime * i);
95    }
96
97    return {offsets: offsets, precision: Math.max(0, -Math.floor(Math.log(gridSliceTime * 1.01) / Math.LN10))};
98}
99
100/**
101 * @param {!Object} canvas
102 * @param {!WebInspector.TimelineGrid.Calculator} calculator
103 * @param {?Array.<number>=} dividerOffsets
104 */
105WebInspector.TimelineGrid.drawCanvasGrid = function(canvas, calculator, dividerOffsets)
106{
107    var context = canvas.getContext("2d");
108    context.save();
109    var ratio = window.devicePixelRatio;
110    context.scale(ratio, ratio);
111    var printDeltas = !!dividerOffsets;
112    var width = canvas.width / window.devicePixelRatio;
113    var height = canvas.height / window.devicePixelRatio;
114    var precision = 0;
115    if (!dividerOffsets) {
116        var dividersData = WebInspector.TimelineGrid.calculateDividerOffsets(calculator, width);
117        dividerOffsets = dividersData.offsets;
118        precision = dividersData.precision;
119    }
120
121    context.fillStyle = "rgba(255, 255, 255, 0.5)";
122    context.fillRect(0, 0, width, 15);
123
124    context.fillStyle = "#333";
125    context.strokeStyle = "rgba(0, 0, 0, 0.1)";
126    context.textBaseline = "hanging";
127    context.font = (printDeltas ? "italic bold 11px " : " 11px ") + WebInspector.fontFamily();
128    context.lineWidth = 1;
129
130    context.translate(0.5, 0.5);
131    const minWidthForTitle = 60;
132    var lastPosition = 0;
133    var time = 0;
134    var lastTime = 0;
135    var paddingRight = 4;
136    var paddingTop = 3;
137    for (var i = 0; i < dividerOffsets.length; ++i) {
138        time = dividerOffsets[i];
139        var position = calculator.computePosition(time);
140        context.beginPath();
141        if (position - lastPosition > minWidthForTitle) {
142            if (!printDeltas || i !== 0) {
143                var text = printDeltas ? calculator.formatTime(calculator.zeroTime() + time - lastTime) : calculator.formatTime(time, precision);
144                var textWidth = context.measureText(text).width;
145                var textPosition = printDeltas ? (position + lastPosition - textWidth) / 2 : position - textWidth - paddingRight;
146                context.fillText(text, textPosition, paddingTop);
147            }
148        }
149        context.moveTo(position, 0);
150        context.lineTo(position, height);
151        context.stroke();
152        lastTime = time;
153        lastPosition = position;
154    }
155    context.restore();
156},
157
158WebInspector.TimelineGrid.prototype = {
159    get dividersElement()
160    {
161        return this._dividersElement;
162    },
163
164    get dividersLabelBarElement()
165    {
166        return this._dividersLabelBarElement;
167    },
168
169    removeDividers: function()
170    {
171        this._dividersElement.removeChildren();
172        this._dividersLabelBarElement.removeChildren();
173    },
174
175    /**
176     * @param {!WebInspector.TimelineGrid.Calculator} calculator
177     * @param {?Array.<number>=} dividerOffsets
178     * @param {boolean=} printDeltas
179     * @return {boolean}
180     */
181    updateDividers: function(calculator, dividerOffsets, printDeltas)
182    {
183        var precision = 0;
184        if (!dividerOffsets) {
185            var dividersData = WebInspector.TimelineGrid.calculateDividerOffsets(calculator, this._dividersElement.clientWidth);
186            dividerOffsets = dividersData.offsets;
187            precision = dividersData.precision;
188            printDeltas = false;
189        }
190
191        var dividersElementClientWidth = this._dividersElement.clientWidth;
192
193        // Reuse divider elements and labels.
194        var divider = /** @type {?Element} */ (this._dividersElement.firstChild);
195        var dividerLabelBar = /** @type {?Element} */ (this._dividersLabelBarElement.firstChild);
196
197        const minWidthForTitle = 60;
198        var lastPosition = 0;
199        var lastTime = 0;
200        for (var i = 0; i < dividerOffsets.length; ++i) {
201            if (!divider) {
202                divider = document.createElement("div");
203                divider.className = "resources-divider";
204                this._dividersElement.appendChild(divider);
205
206                dividerLabelBar = document.createElement("div");
207                dividerLabelBar.className = "resources-divider";
208                var label = document.createElement("div");
209                label.className = "resources-divider-label";
210                dividerLabelBar._labelElement = label;
211                dividerLabelBar.appendChild(label);
212                this._dividersLabelBarElement.appendChild(dividerLabelBar);
213            }
214
215            var time = dividerOffsets[i];
216            var position = calculator.computePosition(time);
217            if (position - lastPosition > minWidthForTitle)
218                dividerLabelBar._labelElement.textContent = printDeltas ? calculator.formatTime(time - lastTime) : calculator.formatTime(time, precision);
219            else
220                dividerLabelBar._labelElement.textContent = "";
221
222            if (printDeltas)
223                dividerLabelBar._labelElement.style.width = Math.ceil(position - lastPosition) + "px";
224            else
225                dividerLabelBar._labelElement.style.removeProperty("width");
226
227            lastPosition = position;
228            lastTime = time;
229            var percentLeft = 100 * position / dividersElementClientWidth;
230            divider.style.left = percentLeft + "%";
231            dividerLabelBar.style.left = percentLeft + "%";
232
233            divider = divider.nextSibling;
234            dividerLabelBar = dividerLabelBar.nextSibling;
235        }
236
237        // Remove extras.
238        while (divider) {
239            var nextDivider = divider.nextSibling;
240            this._dividersElement.removeChild(divider);
241            divider = nextDivider;
242        }
243        while (dividerLabelBar) {
244            var nextDivider = dividerLabelBar.nextSibling;
245            this._dividersLabelBarElement.removeChild(dividerLabelBar);
246            dividerLabelBar = nextDivider;
247        }
248        return true;
249    },
250
251    addEventDivider: function(divider)
252    {
253        this._eventDividersElement.appendChild(divider);
254    },
255
256    addEventDividers: function(dividers)
257    {
258        this._gridHeaderElement.removeChild(this._eventDividersElement);
259        for (var i = 0; i < dividers.length; ++i) {
260            if (dividers[i])
261                this._eventDividersElement.appendChild(dividers[i]);
262        }
263        this._gridHeaderElement.appendChild(this._eventDividersElement);
264    },
265
266    removeEventDividers: function()
267    {
268        this._eventDividersElement.removeChildren();
269    },
270
271    hideEventDividers: function()
272    {
273        this._eventDividersElement.classList.add("hidden");
274    },
275
276    showEventDividers: function()
277    {
278        this._eventDividersElement.classList.remove("hidden");
279    },
280
281    hideDividers: function()
282    {
283        this._dividersElement.classList.add("hidden");
284    },
285
286    showDividers: function()
287    {
288        this._dividersElement.classList.remove("hidden");
289    },
290
291    hideCurtains: function()
292    {
293        this._leftCurtainElement.classList.add("hidden");
294        this._rightCurtainElement.classList.add("hidden");
295    },
296
297    /**
298     * @param {number} gapOffset
299     * @param {number} gapWidth
300     */
301    showCurtains: function(gapOffset, gapWidth)
302    {
303        this._leftCurtainElement.style.width = gapOffset + "px";
304        this._leftCurtainElement.classList.remove("hidden");
305        this._rightCurtainElement.style.left = (gapOffset + gapWidth) + "px";
306        this._rightCurtainElement.classList.remove("hidden");
307    },
308
309    setScrollAndDividerTop: function(scrollTop, dividersTop)
310    {
311        this._dividersLabelBarElement.style.top = scrollTop + "px";
312        this._eventDividersElement.style.top = scrollTop + "px";
313        this._leftCurtainElement.style.top = scrollTop + "px";
314        this._rightCurtainElement.style.top = scrollTop + "px";
315    }
316}
317
318/**
319 * @interface
320 */
321WebInspector.TimelineGrid.Calculator = function() { }
322
323WebInspector.TimelineGrid.Calculator.prototype = {
324    /**
325     * @return {number}
326     */
327    paddingLeft: function() { },
328
329    /**
330     * @param {number} time
331     * @return {number}
332     */
333    computePosition: function(time) { },
334
335    /**
336     * @param {number} time
337     * @param {number=} precision
338     * @return {string}
339     */
340    formatTime: function(time, precision) { },
341
342    /** @return {number} */
343    minimumBoundary: function() { },
344
345    /** @return {number} */
346    zeroTime: function() { },
347
348    /** @return {number} */
349    maximumBoundary: function() { },
350
351    /** @return {number} */
352    boundarySpan: function() { }
353}
354