• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (C) 2012 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.SplitView}
34 * @param {string} title
35 * @param {!WebInspector.TimelineModeViewDelegate} delegate
36 * @param {!WebInspector.TimelineModel} model
37 */
38WebInspector.CountersGraph = function(title, delegate, model)
39{
40    WebInspector.SplitView.call(this, true, false);
41
42    this.element.id = "memory-graphs-container";
43
44    this._delegate = delegate;
45    this._model = model;
46    this._calculator = new WebInspector.TimelineCalculator(this._model);
47
48    this._graphsContainer = this.mainElement();
49    this._createCurrentValuesBar();
50    this._canvasView = new WebInspector.VBoxWithResizeCallback(this._resize.bind(this));
51    this._canvasView.show(this._graphsContainer);
52    this._canvasContainer = this._canvasView.element;
53    this._canvasContainer.id = "memory-graphs-canvas-container";
54    this._canvas = this._canvasContainer.createChild("canvas");
55    this._canvas.id = "memory-counters-graph";
56
57    this._canvasContainer.addEventListener("mouseover", this._onMouseMove.bind(this), true);
58    this._canvasContainer.addEventListener("mousemove", this._onMouseMove.bind(this), true);
59    this._canvasContainer.addEventListener("mouseout", this._onMouseOut.bind(this), true);
60    this._canvasContainer.addEventListener("click", this._onClick.bind(this), true);
61    // We create extra timeline grid here to reuse its event dividers.
62    this._timelineGrid = new WebInspector.TimelineGrid();
63    this._canvasContainer.appendChild(this._timelineGrid.dividersElement);
64
65    // Populate sidebar
66    this.sidebarElement().createChild("div", "sidebar-tree sidebar-tree-section").textContent = title;
67    this._counters = [];
68    this._counterUI = [];
69}
70
71WebInspector.CountersGraph.prototype = {
72    _createCurrentValuesBar: function()
73    {
74        this._currentValuesBar = this._graphsContainer.createChild("div");
75        this._currentValuesBar.id = "counter-values-bar";
76    },
77
78    /**
79     * @param {string} uiName
80     * @param {string} uiValueTemplate
81     * @param {string} color
82     * @return {!WebInspector.CountersGraph.Counter}
83     */
84    createCounter: function(uiName, uiValueTemplate, color)
85    {
86        var counter = new WebInspector.CountersGraph.Counter();
87        this._counters.push(counter);
88        this._counterUI.push(new WebInspector.CountersGraph.CounterUI(this, uiName, uiValueTemplate, color, counter));
89        return counter;
90    },
91
92    /**
93     * @return {!WebInspector.View}
94     */
95    view: function()
96    {
97        return this;
98    },
99
100    dispose: function()
101    {
102    },
103
104    reset: function()
105    {
106        for (var i = 0; i < this._counters.length; ++i) {
107            this._counters[i].reset();
108            this._counterUI[i].reset();
109        }
110        this.refresh();
111    },
112
113    _resize: function()
114    {
115        var parentElement = this._canvas.parentElement;
116        this._canvas.width = parentElement.clientWidth  * window.devicePixelRatio;
117        this._canvas.height = parentElement.clientHeight * window.devicePixelRatio;
118        var timelinePaddingLeft = 15;
119        this._calculator.setDisplayWindow(timelinePaddingLeft, this._canvas.width);
120        this.refresh();
121    },
122
123    /**
124     * @param {number} startTime
125     * @param {number} endTime
126     */
127    setWindowTimes: function(startTime, endTime)
128    {
129        this._calculator.setWindow(startTime, endTime);
130        this.scheduleRefresh();
131    },
132
133    scheduleRefresh: function()
134    {
135        WebInspector.invokeOnceAfterBatchUpdate(this, this.refresh);
136    },
137
138    draw: function()
139    {
140        for (var i = 0; i < this._counters.length; ++i) {
141            this._counters[i]._calculateVisibleIndexes(this._calculator);
142            this._counters[i]._calculateXValues(this._canvas.width);
143        }
144        this._clear();
145
146        for (var i = 0; i < this._counterUI.length; i++)
147            this._counterUI[i]._drawGraph(this._canvas);
148    },
149
150    /**
151     * @param {?Event} event
152     */
153    _onClick: function(event)
154    {
155        var x = event.x - this._canvasContainer.totalOffsetLeft();
156        var minDistance = Infinity;
157        var bestTime;
158        for (var i = 0; i < this._counterUI.length; ++i) {
159            var counterUI = this._counterUI[i];
160            if (!counterUI.counter.times.length)
161                continue;
162            var index = counterUI._recordIndexAt(x);
163            var distance = Math.abs(x * window.devicePixelRatio - counterUI.counter.x[index]);
164            if (distance < minDistance) {
165                minDistance = distance;
166                bestTime = counterUI.counter.times[index];
167            }
168        }
169        if (bestTime !== undefined)
170            this._revealRecordAt(bestTime);
171    },
172
173    /**
174     * @param {number} time
175     */
176    _revealRecordAt: function(time)
177    {
178        var recordToReveal;
179        /**
180         * @param {!WebInspector.TimelineModel.Record} record
181         * @return {boolean}
182         * @this {WebInspector.CountersGraph}
183         */
184        function findRecordToReveal(record)
185        {
186            if (!this._model.isVisible(record))
187                return false;
188            if (record.startTime() <= time && time <= record.endTime()) {
189                recordToReveal = record;
190                return true;
191            }
192            // If there is no record containing the time than use the latest one before that time.
193            if (!recordToReveal || record.endTime() < time && recordToReveal.endTime() < record.endTime())
194                recordToReveal = record;
195            return false;
196        }
197        this._model.forAllRecords(null, findRecordToReveal.bind(this));
198        this._delegate.select(recordToReveal ? WebInspector.TimelineSelection.fromRecord(recordToReveal) : null);
199    },
200
201    /**
202     * @param {?Event} event
203     */
204    _onMouseOut: function(event)
205    {
206        delete this._markerXPosition;
207        this._clearCurrentValueAndMarker();
208    },
209
210    _clearCurrentValueAndMarker: function()
211    {
212        for (var i = 0; i < this._counterUI.length; i++)
213            this._counterUI[i]._clearCurrentValueAndMarker();
214    },
215
216    /**
217     * @param {?Event} event
218     */
219    _onMouseMove: function(event)
220    {
221        var x = event.x - this._canvasContainer.totalOffsetLeft();
222        this._markerXPosition = x;
223        this._refreshCurrentValues();
224    },
225
226    _refreshCurrentValues: function()
227    {
228        if (this._markerXPosition === undefined)
229            return;
230        for (var i = 0; i < this._counterUI.length; ++i)
231            this._counterUI[i].updateCurrentValue(this._markerXPosition);
232    },
233
234    refresh: function()
235    {
236        this._timelineGrid.updateDividers(this._calculator);
237        this.draw();
238        this._refreshCurrentValues();
239    },
240
241    refreshRecords: function()
242    {
243    },
244
245    _clear: function()
246    {
247        var ctx = this._canvas.getContext("2d");
248        ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
249    },
250
251    /**
252     * @param {?WebInspector.TimelineModel.Record} record
253     * @param {string=} regex
254     * @param {boolean=} selectRecord
255     */
256    highlightSearchResult: function(record, regex, selectRecord)
257    {
258    },
259
260    /**
261     * @param {?WebInspector.TimelineSelection} selection
262     */
263    setSelection: function(selection)
264    {
265    },
266
267    __proto__: WebInspector.SplitView.prototype
268}
269
270/**
271 * @constructor
272 */
273WebInspector.CountersGraph.Counter = function()
274{
275    this.times = [];
276    this.values = [];
277}
278
279WebInspector.CountersGraph.Counter.prototype = {
280    /**
281     * @param {number} time
282     * @param {number} value
283     */
284    appendSample: function(time, value)
285    {
286        if (this.values.length && this.values.peekLast() === value)
287            return;
288        this.times.push(time);
289        this.values.push(value);
290    },
291
292    reset: function()
293    {
294        this.times = [];
295        this.values = [];
296    },
297
298    /**
299     * @param {number} value
300     */
301    setLimit: function(value)
302    {
303        this._limitValue = value;
304    },
305
306    /**
307     * @return {!{min: number, max: number}}
308     */
309    _calculateBounds: function()
310    {
311        var maxValue;
312        var minValue;
313        for (var i = this._minimumIndex; i <= this._maximumIndex; i++) {
314            var value = this.values[i];
315            if (minValue === undefined || value < minValue)
316                minValue = value;
317            if (maxValue === undefined || value > maxValue)
318                maxValue = value;
319        }
320        minValue = minValue || 0;
321        maxValue = maxValue || 1;
322        if (this._limitValue) {
323            if (maxValue > this._limitValue * 0.5)
324                maxValue = Math.max(maxValue, this._limitValue);
325            minValue = Math.min(minValue, this._limitValue);
326        }
327        return { min: minValue, max: maxValue };
328    },
329
330    /**
331     * @param {!WebInspector.TimelineCalculator} calculator
332     */
333    _calculateVisibleIndexes: function(calculator)
334    {
335        var start = calculator.minimumBoundary();
336        var end = calculator.maximumBoundary();
337
338        // Maximum index of element whose time <= start.
339        this._minimumIndex = Number.constrain(this.times.upperBound(start) - 1, 0, this.times.length - 1);
340
341        // Minimum index of element whose time >= end.
342        this._maximumIndex = Number.constrain(this.times.lowerBound(end), 0, this.times.length - 1);
343
344        // Current window bounds.
345        this._minTime = start;
346        this._maxTime = end;
347    },
348
349    /**
350     * @param {number} width
351     */
352    _calculateXValues: function(width)
353    {
354        if (!this.values.length)
355            return;
356
357        var xFactor = width / (this._maxTime - this._minTime);
358
359        this.x = new Array(this.values.length);
360        for (var i = this._minimumIndex + 1; i <= this._maximumIndex; i++)
361             this.x[i] = xFactor * (this.times[i] - this._minTime);
362    }
363}
364
365/**
366 * @constructor
367 * @param {!WebInspector.CountersGraph} memoryCountersPane
368 * @param {string} title
369 * @param {string} currentValueLabel
370 * @param {string} graphColor
371 * @param {!WebInspector.CountersGraph.Counter} counter
372 */
373WebInspector.CountersGraph.CounterUI = function(memoryCountersPane, title, currentValueLabel, graphColor, counter)
374{
375    this._memoryCountersPane = memoryCountersPane;
376    this.counter = counter;
377    var container = memoryCountersPane.sidebarElement().createChild("div", "memory-counter-sidebar-info");
378    var swatchColor = graphColor;
379    this._swatch = new WebInspector.SwatchCheckbox(WebInspector.UIString(title), swatchColor);
380    this._swatch.addEventListener(WebInspector.SwatchCheckbox.Events.Changed, this._toggleCounterGraph.bind(this));
381    container.appendChild(this._swatch.element);
382    this._range = this._swatch.element.createChild("span");
383
384    this._value = memoryCountersPane._currentValuesBar.createChild("span", "memory-counter-value");
385    this._value.style.color = graphColor;
386    this.graphColor = graphColor;
387    this.limitColor = WebInspector.Color.parse(graphColor).setAlpha(0.3).toString(WebInspector.Color.Format.RGBA);
388    this.graphYValues = [];
389    this._verticalPadding = 10;
390
391    this._currentValueLabel = currentValueLabel;
392    this._marker = memoryCountersPane._canvasContainer.createChild("div", "memory-counter-marker");
393    this._marker.style.backgroundColor = graphColor;
394    this._clearCurrentValueAndMarker();
395}
396
397WebInspector.CountersGraph.CounterUI.prototype = {
398    reset: function()
399    {
400        this._range.textContent = "";
401    },
402
403    /**
404     * @param {number} minValue
405     * @param {number} maxValue
406     */
407    setRange: function(minValue, maxValue)
408    {
409        this._range.textContent = WebInspector.UIString("[%.0f:%.0f]", minValue, maxValue);
410    },
411
412    _toggleCounterGraph: function(event)
413    {
414        this._value.classList.toggle("hidden", !this._swatch.checked);
415        this._memoryCountersPane.refresh();
416    },
417
418    /**
419     * @param {number} x
420     * @return {number}
421     */
422    _recordIndexAt: function(x)
423    {
424        return this.counter.x.upperBound(x * window.devicePixelRatio, null, this.counter._minimumIndex + 1, this.counter._maximumIndex + 1) - 1;
425    },
426
427    /**
428     * @param {number} x
429     */
430    updateCurrentValue: function(x)
431    {
432        if (!this.visible() || !this.counter.values.length)
433            return;
434        var index = this._recordIndexAt(x);
435        this._value.textContent = WebInspector.UIString(this._currentValueLabel, this.counter.values[index]);
436        var y = this.graphYValues[index] / window.devicePixelRatio;
437        this._marker.style.left = x + "px";
438        this._marker.style.top = y + "px";
439        this._marker.classList.remove("hidden");
440    },
441
442    _clearCurrentValueAndMarker: function()
443    {
444        this._value.textContent = "";
445        this._marker.classList.add("hidden");
446    },
447
448    /**
449     * @param {!HTMLCanvasElement} canvas
450     */
451    _drawGraph: function(canvas)
452    {
453        var ctx = canvas.getContext("2d");
454        var width = canvas.width;
455        var height = canvas.height - 2 * this._verticalPadding;
456        if (height <= 0) {
457            this.graphYValues = [];
458            return;
459        }
460        var originY = this._verticalPadding;
461        var counter = this.counter;
462        var values = counter.values;
463
464        if (!values.length)
465            return;
466
467        var bounds = counter._calculateBounds();
468        var minValue = bounds.min;
469        var maxValue = bounds.max;
470        this.setRange(minValue, maxValue);
471
472        if (!this.visible())
473            return;
474
475        var yValues = this.graphYValues;
476        var maxYRange = maxValue - minValue;
477        var yFactor = maxYRange ? height / (maxYRange) : 1;
478
479        ctx.save();
480        ctx.lineWidth = window.devicePixelRatio;
481        if (ctx.lineWidth % 2)
482            ctx.translate(0.5, 0.5);
483        ctx.beginPath();
484        var value = values[counter._minimumIndex];
485        var currentY = Math.round(originY + height - (value - minValue) * yFactor);
486        ctx.moveTo(0, currentY);
487        for (var i = counter._minimumIndex; i <= counter._maximumIndex; i++) {
488             var x = Math.round(counter.x[i]);
489             ctx.lineTo(x, currentY);
490             var currentValue = values[i];
491             if (typeof currentValue !== "undefined")
492                value = currentValue;
493             currentY = Math.round(originY + height - (value - minValue) * yFactor);
494             ctx.lineTo(x, currentY);
495             yValues[i] = currentY;
496        }
497        yValues.length = i;
498        ctx.lineTo(width, currentY);
499        ctx.strokeStyle = this.graphColor;
500        ctx.stroke();
501        if (counter._limitValue) {
502            var limitLineY = Math.round(originY + height - (counter._limitValue - minValue) * yFactor);
503            ctx.moveTo(0, limitLineY);
504            ctx.lineTo(width, limitLineY);
505            ctx.strokeStyle = this.limitColor;
506            ctx.stroke();
507        }
508        ctx.closePath();
509        ctx.restore();
510    },
511
512    /**
513     * @return {boolean}
514     */
515    visible: function()
516    {
517        return this._swatch.checked;
518    }
519}
520
521
522/**
523 * @constructor
524 * @extends {WebInspector.Object}
525 */
526WebInspector.SwatchCheckbox = function(title, color)
527{
528    this.element = document.createElement("div");
529    this._swatch = this.element.createChild("div", "swatch");
530    this.element.createChild("span", "title").textContent = title;
531    this._color = color;
532    this.checked = true;
533
534    this.element.addEventListener("click", this._toggleCheckbox.bind(this), true);
535}
536
537WebInspector.SwatchCheckbox.Events = {
538    Changed: "Changed"
539}
540
541WebInspector.SwatchCheckbox.prototype = {
542    get checked()
543    {
544        return this._checked;
545    },
546
547    set checked(v)
548    {
549        this._checked = v;
550        if (this._checked)
551            this._swatch.style.backgroundColor = this._color;
552        else
553            this._swatch.style.backgroundColor = "";
554    },
555
556    _toggleCheckbox: function(event)
557    {
558        this.checked = !this.checked;
559        this.dispatchEventToListeners(WebInspector.SwatchCheckbox.Events.Changed);
560    },
561
562    __proto__: WebInspector.Object.prototype
563}
564