OLD | NEW |
1 /* Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 /* Copyright (c) 2012 The Chromium Authors. All rights reserved. |
2 * Use of this source code is governed by a BSD-style license that can be | 2 * Use of this source code is governed by a BSD-style license that can be |
3 * found in the LICENSE file. */ | 3 * found in the LICENSE file. */ |
4 | 4 |
5 'use strict'; | |
6 | 5 |
7 /* convert to cr.define('PerformanceMonitor', function() {... | 6 cr.define('performance_monitor', function() { |
8 * when integrating into webui */ | 7 'use strict'; |
9 | 8 |
10 var Installer = function() { | |
11 /** | 9 /** |
12 * Enum for time ranges, giving a descriptive name, time span prior to |now|, | 10 * Enum for time ranges, giving a descriptive name, time span prior to |now|, |
13 * data point resolution, and time-label frequency and format for each. | 11 * data point resolution, and time-label frequency and format for each. |
14 * @enum {{ | 12 * @enum {{ |
15 * value: !number, | 13 * value: number, |
16 * name: !string, | 14 * name: string, |
17 * timeSpan: !number, | 15 * timeSpan: number, |
18 * resolution: !number, | 16 * resolution: number, |
19 * labelEvery: !number, | 17 * labelEvery: number, |
20 * format: !string | 18 * format: string |
21 * }} | 19 * }} |
22 * @private | 20 * @private |
23 */ | 21 */ |
24 var TimeRange_ = { | 22 var TimeRange_ = { |
| 23 // Prior 12 min, resolution of 1s, at most 720 points. |
| 24 // Labels at 60 point (1 min) intervals. |
| 25 minutes: {value: 0, name: 'Last 12 min', timeSpan: 720 * 1000, |
| 26 resolution: 1000, labelEvery: 60, format: 'MM-dd'}, |
| 27 |
| 28 // Prior hour, resolution of 5s, at most 720 points. |
| 29 // Labels at 60 point (5 min) intervals. |
| 30 hour: {value: 1, name: 'Last Hour', timeSpan: 3600 * 1000, |
| 31 resolution: 1000 * 5, labelEvery: 60, format: 'MM-dd'}, |
| 32 |
25 // Prior day, resolution of 2 min, at most 720 points. | 33 // Prior day, resolution of 2 min, at most 720 points. |
26 // Labels at 90 point (3 hour) intervals. | 34 // Labels at 90 point (3 hour) intervals. |
27 day: {value: 0, name: 'Last Day', timeSpan: 24 * 3600 * 1000, | 35 day: {value: 2, name: 'Last Day', timeSpan: 24 * 3600 * 1000, |
28 resolution: 1000 * 60 * 2, labelEvery: 90, format: 'MM-dd'}, | 36 resolution: 1000 * 60 * 2, labelEvery: 90, format: 'MM-dd'}, |
29 | 37 |
30 // Prior week, resolution of 15 min -- at most 672 data points. | 38 // Prior week, resolution of 15 min -- at most 672 data points. |
31 // Labels at 96 point (daily) intervals. | 39 // Labels at 96 point (daily) intervals. |
32 week: {value: 1, name: 'Last Week', timeSpan: 7 * 24 * 3600 * 1000, | 40 week: {value: 3, name: 'Last Week', timeSpan: 7 * 24 * 3600 * 1000, |
33 resolution: 1000 * 60 * 15, labelEvery: 96, format: 'M/d'}, | 41 resolution: 1000 * 60 * 15, labelEvery: 96, format: 'M/d'}, |
34 | 42 |
35 // Prior month (30 days), resolution of 1 hr -- at most 720 data points. | 43 // Prior month (30 days), resolution of 1 hr -- at most 720 data points. |
36 // Labels at 168 point (weekly) intervals. | 44 // Labels at 168 point (weekly) intervals. |
37 month: {value: 2, name: 'Last Month', timeSpan: 30 * 24 * 3600 * 1000, | 45 month: {value: 4, name: 'Last Month', timeSpan: 30 * 24 * 3600 * 1000, |
38 resolution: 1000 * 3600, labelEvery: 168, format: 'M/d'}, | 46 resolution: 1000 * 3600, labelEvery: 168, format: 'M/d'}, |
39 | 47 |
40 // Prior quarter (90 days), resolution of 3 hr -- at most 720 data points. | 48 // Prior quarter (90 days), resolution of 3 hr -- at most 720 data points. |
41 // Labels at 112 point (fortnightly) intervals. | 49 // Labels at 112 point (fortnightly) intervals. |
42 quarter: {value: 3, name: 'Last Quarter', timeSpan: 90 * 24 * 3600 * 1000, | 50 quarter: {value: 5, name: 'Last Quarter', timeSpan: 90 * 24 * 3600 * 1000, |
43 resolution: 1000 * 3600 * 3, labelEvery: 112, format: 'M/yy'}, | 51 resolution: 1000 * 3600 * 3, labelEvery: 112, format: 'M/yy'}, |
44 }; | 52 }; |
45 | 53 |
| 54 /* |
| 55 * Offset, in ms, by which to subtract to convert GMT to local time |
| 56 * @type {number} |
| 57 */ |
| 58 var timezoneOffset = new Date().getTimezoneOffset() * 60000; |
| 59 |
46 /** @constructor */ | 60 /** @constructor */ |
47 function PerformanceMonitor() { | 61 function PerformanceMonitor() { |
48 this.__proto__ = PerformanceMonitor.prototype; | 62 this.__proto__ = PerformanceMonitor.prototype; |
49 /** | 63 /** |
50 * All metrics have entries, but those not displayed have an empty div list. | 64 * All metrics have entries, but those not displayed have an empty div list. |
51 * If a div list is not empty, the associated data will be non-null, or | 65 * If a div list is not empty, the associated data will be non-null, or |
52 * null but about to be filled by webui response. Thus, any metric with | 66 * null but about to be filled by webui handler response. Thus, any metric |
53 * non-empty div list but null data is awaiting a data response from the | 67 * with non-empty div list but null data is awaiting a data response |
54 * webui. | 68 * from the webui handler. The webui handler uses numbers to uniquely |
| 69 * identify metric and event types, so we use the same numbers (in |
| 70 * string form) for the map key, repeating the id number in the |id| |
| 71 * field, as a number. |
55 * @type {Object.<string, { | 72 * @type {Object.<string, { |
| 73 * id: number, |
| 74 * description: string, |
| 75 * units: string, |
| 76 * yAxis: !{max: number, color: string}, |
56 * divs: !Array.<HTMLDivElement>, | 77 * divs: !Array.<HTMLDivElement>, |
57 * yAxis: !{max: !number, color: !string}, | 78 * data: ?Array.<{time: number, value: number}> |
58 * data: ?Array.<{time: !number, value: !number}>, | |
59 * description: !string, | |
60 * units: !string | |
61 * }>} | 79 * }>} |
62 * @private | 80 * @private |
63 */ | 81 */ |
64 this.metricMap_ = { | 82 this.metricMap_ = {}; |
65 jankiness: { | |
66 divs: [], | |
67 yAxis: {max: 100, color: 'rgb(255, 128, 128)'}, | |
68 data: null, | |
69 description: 'Jankiness', | |
70 units: 'milliJanks' | |
71 }, | |
72 oddness: { | |
73 divs: [], | |
74 yAxis: {max: 20, color: 'rgb(0, 192, 0)'}, | |
75 data: null, | |
76 description: 'Oddness', | |
77 units: 'kOdds' | |
78 } | |
79 }; | |
80 | 83 |
81 /* | 84 /* |
82 * Similar data for events, though no yAxis info is needed since events | 85 * Similar data for events, though no yAxis info is needed since events |
83 * are simply labelled markers at X locations. Rules regarding null data | 86 * are simply labelled markers at X locations. Rules regarding null data |
84 * with non-empty div list apply here as for metricMap_ above. | 87 * with non-empty div list apply here as for metricMap_ above. |
85 * @type {Object.<string, { | 88 * @type {Object.<number, { |
| 89 * id: number, |
| 90 * description: string, |
| 91 * color: string, |
86 * divs: !Array.<HTMLDivElement>, | 92 * divs: !Array.<HTMLDivElement>, |
87 * description: !string, | 93 * data: ?Array.<{time: number, longDescription: string}> |
88 * color: !string, | |
89 * data: ?Array.<{time: !number, longDescription: !string}> | |
90 * }>} | 94 * }>} |
91 * @private | 95 * @private |
92 */ | 96 */ |
93 this.eventMap_ = { | 97 this.eventMap_ = {}; |
94 wampusAttacks: { | |
95 divs: [], | |
96 description: 'Wampus Attack', | |
97 color: 'rgb(0, 0, 255)', | |
98 data: null | |
99 }, | |
100 solarEclipses: { | |
101 divs: [], | |
102 description: 'Solar Eclipse', | |
103 color: 'rgb(255, 0, 255)', | |
104 data: null | |
105 } | |
106 }; | |
107 | 98 |
108 /** | 99 /** |
109 * Time periods in which the browser was active and collecting metrics. | 100 * Time periods in which the browser was active and collecting metrics |
110 * and events. | 101 * and events. |
111 * @type {!Array.<{start: !number, end: !number}>} | 102 * @type {!Array.<{start: number, end: number}>} |
112 * @private | 103 * @private |
113 */ | 104 */ |
114 this.intervals_ = []; | 105 this.intervals_ = []; |
115 | 106 |
116 this.setupCheckboxes_( | |
117 '#chooseMetrics', this.metricMap_, this.addMetric, this.dropMetric); | |
118 this.setupCheckboxes_( | |
119 '#chooseEvents', this.eventMap_, this.addEventType, this.dropEventType); | |
120 this.setupTimeRangeChooser_(); | 107 this.setupTimeRangeChooser_(); |
| 108 chrome.send('getAllEventTypes'); |
| 109 chrome.send('getAllMetricTypes'); |
121 this.setupMainChart_(); | 110 this.setupMainChart_(); |
122 TimeRange_.day.element.click().call(this); | 111 TimeRange_.day.element.click(); |
123 } | 112 } |
124 | 113 |
125 PerformanceMonitor.prototype = { | 114 PerformanceMonitor.prototype = { |
126 /** | 115 /** |
127 * Set up the radio button set to choose time range. Use div#radioTemplate | 116 * Receive a list of all metrics, and populate |this.metricMap_| to |
| 117 * reflect said list. Reconfigure the checkbox set for metric selection. |
| 118 * @param {Array.<{metricType: string, shortDescription: string}>} |
| 119 * allMetrics All metrics from which to select. |
| 120 */ |
| 121 getAllMetricTypesCallback: function(allMetrics) { |
| 122 for (var i = 0; i < allMetrics.length; i++) { |
| 123 var metric = allMetrics[i]; |
| 124 |
| 125 this.metricMap_[metric.metricType] = { |
| 126 id: metric.metricType, |
| 127 description: metric.shortDescription, |
| 128 units: metric.units, |
| 129 yAxis: {min: 0, max: metric.maxValue, |
| 130 color: jQuery.color.parse('blue')}, |
| 131 divs: [], |
| 132 data: null |
| 133 }; |
| 134 } |
| 135 |
| 136 this.setupCheckboxes_($('#choose-metrics')[0], |
| 137 this.metricMap_, this.addMetric, this.dropMetric); |
| 138 }, |
| 139 |
| 140 /** |
| 141 * Receive a list of all event types, and populate |this.eventMap_| to |
| 142 * reflect said list. Reconfigure the checkbox set for event selection. |
| 143 * @param {Array.<{eventType: string, shortDescription: string}>} |
| 144 * allEvents All events from which to select. |
| 145 */ |
| 146 getAllEventTypesCallback: function(allEvents) { |
| 147 for (var i = 0; i < allEvents.length; i++) { |
| 148 var eventInfo = allEvents[i]; |
| 149 |
| 150 this.eventMap_[eventInfo.eventType] = { |
| 151 id: eventInfo.eventType, |
| 152 color: jQuery.color.parse('red'), |
| 153 data: null, |
| 154 description: eventInfo.shortDescription, |
| 155 divs: [] |
| 156 }; |
| 157 } |
| 158 |
| 159 this.setupCheckboxes_($('#choose-events')[0], |
| 160 this.eventMap_, this.addEventType, this.dropEventType); |
| 161 }, |
| 162 |
| 163 /** |
| 164 * Set up the radio button set to choose time range. Use div#radio-template |
128 * as a template. | 165 * as a template. |
129 * @private | 166 * @private |
130 */ | 167 */ |
131 setupTimeRangeChooser_: function() { | 168 setupTimeRangeChooser_: function() { |
132 var timeDiv = $('#chooseTimeRange')[0]; | 169 var timeDiv = $('#choose-time-range')[0]; |
133 var radioTemplate = $('#radioTemplate')[0]; | 170 var radioTemplate = $('#radio-template')[0]; |
134 | 171 |
135 for (var time in TimeRange_) { | 172 for (var time in TimeRange_) { |
136 var timeRange = TimeRange_[time]; | 173 var timeRange = TimeRange_[time]; |
137 var radio = radioTemplate.cloneNode(true); | 174 var radio = radioTemplate.cloneNode(true); |
138 var input = radio.querySelector('input'); | 175 var input = radio.querySelector('input'); |
139 | 176 |
140 input.value = timeRange.value; | 177 input.value = timeRange.value; |
141 input.timeRange = timeRange; | 178 input.timeRange = timeRange; |
142 radio.querySelector('span').innerText = timeRange.name; | 179 radio.querySelector('span').innerText = timeRange.name; |
143 timeDiv.appendChild(radio); | 180 timeDiv.appendChild(radio); |
144 timeRange.element = input; | 181 timeRange.element = input; |
145 } | 182 } |
146 | 183 |
147 timeDiv.addEventListener('click', function(e) { | 184 timeDiv.addEventListener('click', function(e) { |
148 if (!e.target.webkitMatchesSelector('input[type="radio"]')) | 185 if (!e.target.webkitMatchesSelector('input[type="radio"]')) |
149 return; | 186 return; |
150 | 187 |
151 this.setTimeRange(e.target.timeRange); | 188 this.setTimeRange(e.target.timeRange); |
152 }.bind(this)); | 189 }.bind(this)); |
153 }, | 190 }, |
154 | 191 |
155 /** | 192 /** |
156 * Generalized function for setting up checkbox blocks for either events | 193 * Generalized function for setting up checkbox blocks for either events |
157 * or metrics. Take a div ID |divId| into which to place the checkboxes, | 194 * or metrics. Take a div |div| into which to place the checkboxes, |
158 * and a map |optionMap| with values that each include a property | 195 * and a map |optionMap| with values that each include a property |
159 * |description|. Set up one checkbox for each entry in |optionMap| | 196 * |description|. Set up one checkbox for each entry in |optionMap| |
160 * labelled with that description. Arrange callbacks to function |check| | 197 * labelled with that description. Arrange callbacks to function |check| |
161 * or |uncheck|, passing them the key of the checked or unchecked option, | 198 * or |uncheck|, passing them the key of the checked or unchecked option, |
162 * when the relevant checkbox state changes. | 199 * when the relevant checkbox state changes. |
163 * @param {!string} divId Id of division into which to put checkboxes | 200 * @param {!HTMLDivElement} div A <div> into which to put checkboxes. |
164 * @param {!Object} optionMap map of metric/event entries | 201 * @param {!Object} optionMap A map of metric/event entries. |
165 * @param {!function(this:Controller, Object)} check | 202 * @param {!function(this:Controller, Object)} check |
166 * function to select an entry (metric or event) | 203 * The function to select an entry (metric or event). |
167 * @param {!function(this:Controller, Object)} uncheck | 204 * @param {!function(this:Controller, Object)} uncheck |
168 * function to deselect an entry (metric or event) | 205 * The function to deselect an entry (metric or event). |
169 * @private | 206 * @private |
170 */ | 207 */ |
171 setupCheckboxes_: function(divId, optionMap, check, uncheck) { | 208 setupCheckboxes_: function(div, optionMap, check, uncheck) { |
172 var checkboxTemplate = $('#checkboxTemplate')[0]; | 209 var checkboxTemplate = $('#checkbox-template')[0]; |
173 var chooseMetricsDiv = $(divId)[0]; | |
174 | 210 |
175 for (var option in optionMap) { | 211 for (var option in optionMap) { |
176 var checkbox = checkboxTemplate.cloneNode(true); | 212 var checkbox = checkboxTemplate.cloneNode(true); |
177 checkbox.querySelector('span').innerText = 'Show ' + | 213 checkbox.querySelector('span').innerText = 'Show ' + |
178 optionMap[option].description; | 214 optionMap[option].description; |
179 chooseMetricsDiv.appendChild(checkbox); | 215 div.appendChild(checkbox); |
180 | 216 |
181 var input = checkbox.querySelector('input'); | 217 var input = checkbox.querySelector('input'); |
182 input.option = option; | 218 input.option = optionMap[option].id; |
183 input.addEventListener('change', function(e) { | 219 input.addEventListener('change', function(e) { |
184 if (e.target.checked) | 220 (e.target.checked ? check : uncheck).call(this, e.target.option); |
185 check.call(this, e.target.option); | |
186 else | |
187 uncheck.call(this, e.target.option); | |
188 }.bind(this)); | 221 }.bind(this)); |
189 } | 222 } |
190 }, | 223 }, |
191 | 224 |
192 /** | 225 /** |
193 * Set up just one chart in which all metrics will be displayed | 226 * Set up just one chart in which all metrics will be displayed |
194 * initially. But, the design readily accommodates addition of | 227 * initially. But, the design readily accommodates addition of |
195 * new charts, and movement of metrics into those other charts. | 228 * new charts, and movement of metrics into those other charts. |
196 * @private | 229 * @private |
197 */ | 230 */ |
198 setupMainChart_: function() { | 231 setupMainChart_: function() { |
199 this.chartParent = $('#charts')[0]; | 232 this.chartParent = $('#charts')[0]; |
200 this.charts = [document.createElement('div')]; | 233 this.charts = [document.createElement('div')]; |
201 this.charts[0].className = 'chart'; | 234 this.charts[0].className = 'chart'; |
202 this.chartParent.appendChild(this.charts[0]); | 235 this.chartParent.appendChild(this.charts[0]); |
203 }, | 236 }, |
204 | 237 |
205 /** | 238 /** |
206 * Set the time range for which to display metrics and events. For | 239 * Set the time range for which to display metrics and events. For |
207 * now, the time range always ends at "now", but future implementations | 240 * now, the time range always ends at "now", but future implementations |
208 * may allow time ranges not so anchored. | 241 * may allow time ranges not so anchored. |
209 * @param {!{start: !number, end: !number, resolution: !number}} range | 242 * @param {!{start: number, end: number, resolution: number}} range |
| 243 * The time range for which to get display data. |
210 */ | 244 */ |
211 setTimeRange: function(range) { | 245 setTimeRange: function(range) { |
212 this.range = range; | 246 this.range = range; |
213 this.end = Math.floor(Date.now() / range.resolution) * | 247 this.end = Math.floor(Date.now() / range.resolution) * |
214 range.resolution; | 248 range.resolution; |
215 | 249 |
216 // Take the GMT value of this.end ("now") and subtract from it the | |
217 // number of minutes by which we lag GMT in the present timezone, | |
218 // X mS/minute. This will show time in the present timezone. | |
219 this.end -= new Date().getTimezoneOffset() * 60000; | |
220 this.start = this.end - range.timeSpan; | 250 this.start = this.end - range.timeSpan; |
221 this.requestIntervals(); | 251 this.requestIntervals(); |
222 }, | 252 }, |
223 | 253 |
224 /** | 254 /** |
225 * Return mock interval set for testing. | |
226 * @return {!Array.<{start: !number, end: !number}>} intervals | |
227 */ | |
228 getMockIntervals: function() { | |
229 var interval = this.end - this.start; | |
230 | |
231 return [ | |
232 {start: this.start + interval * .1, | |
233 end: this.start + interval * .2}, | |
234 {start: this.start + interval * .7, | |
235 end: this.start + interval} | |
236 ]; | |
237 }, | |
238 | |
239 /** | |
240 * Request activity intervals in the current time range. | 255 * Request activity intervals in the current time range. |
241 */ | 256 */ |
242 requestIntervals: function() { | 257 requestIntervals: function() { |
243 this.onReceiveIntervals(this.getMockIntervals()); | 258 chrome.send('getActiveIntervals', [this.start, this.end]); |
244 // Replace with: chrome.send('getIntervals', this.start, this.end, | |
245 // this.onReceiveIntervals); | |
246 }, | 259 }, |
247 | 260 |
248 /** | 261 /** |
249 * Webui callback delivering response from requestIntervals call. Assumes | 262 * Webui callback delivering response from requestIntervals call. Assumes |
250 * this is a new time range choice, which results in complete refresh of | 263 * this is a new time range choice, which results in complete refresh of |
251 * all metrics and event types that are currently selected. | 264 * all metrics and event types that are currently selected. |
252 * @param {!Array.<{start: !number, end: !number}>} intervals new intervals | 265 * @param {!Array.<{start: number, end: number}>} intervals |
| 266 * The new intervals. |
253 */ | 267 */ |
254 onReceiveIntervals: function(intervals) { | 268 getActiveIntervalsCallback: function(intervals) { |
255 this.intervals_ = intervals; | 269 this.intervals_ = intervals; |
256 | 270 |
257 for (var metric in this.metricMap_) { | 271 for (var metric in this.metricMap_) { |
258 var metricValue = this.metricMap_[metric]; | 272 var metricValue = this.metricMap_[metric]; |
259 if (metricValue.divs.length > 0) // if we're displaying this metric. | 273 if (metricValue.divs.length > 0) // if we're displaying this metric. |
260 this.refreshMetric(metric); | 274 this.refreshMetric(metric); |
261 } | 275 } |
262 | 276 |
263 for (var eventType in this.eventMap_) { | 277 for (var eventType in this.eventMap_) { |
264 var eventValue = this.eventMap_[eventType]; | 278 var eventValue = this.eventMap_[eventType]; |
265 if (eventValue.divs.length > 0) | 279 if (eventValue.divs.length > 0) |
266 this.refreshEventType(eventType); | 280 this.refreshEventType(eventValue.id); |
267 } | 281 } |
268 }, | 282 }, |
269 | 283 |
270 /** | 284 /** |
271 * Add a new metric to the main (currently only) chart. | 285 * Add a new metric to the main (currently only) chart. |
272 * @param {!string} metric Metric to start displaying | 286 * @param {string} metric The metric to start displaying. |
273 */ | 287 */ |
274 addMetric: function(metric) { | 288 addMetric: function(metric) { |
275 this.metricMap_[metric].divs.push(this.charts[0]); | 289 this.metricMap_[metric].divs.push(this.charts[0]); |
276 this.refreshMetric(metric); | 290 this.refreshMetric(metric); |
277 }, | 291 }, |
278 | 292 |
279 /** | 293 /** |
280 * Remove a metric from the chart(s). | 294 * Remove a metric from the chart(s). |
281 * @param {!string} metric Metric to stop displaying | 295 * @param {string} metric The metric to stop displaying. |
282 */ | 296 */ |
283 dropMetric: function(metric) { | 297 dropMetric: function(metric) { |
284 var metricValue = this.metricMap_[metric]; | 298 var metricValue = this.metricMap_[metric]; |
285 var affectedCharts = metricValue.divs; | 299 var affectedCharts = metricValue.divs; |
286 metricValue.divs = []; | 300 metricValue.divs = []; |
287 | 301 |
288 affectedCharts.forEach(this.drawChart, this); | 302 affectedCharts.forEach(this.drawChart, this); |
289 }, | 303 }, |
290 | 304 |
291 /** | 305 /** |
292 * Return mock metric data points for testing. Give values ranging from | |
293 * offset to max-offset. (This lets us avoid direct overlap of | |
294 * different mock data sets in an ugly way that will die in the next | |
295 * version anyway.) | |
296 * @param {!number} max Max data value to return, less offset | |
297 * @param {!number} offset Adjustment factor | |
298 * @return {!Array.<{time: !number, value: !number}>} | |
299 */ | |
300 getMockDataPoints: function(max, offset) { | |
301 var dataPoints = []; | |
302 | |
303 for (var i = 0; i < this.intervals_.length; i++) { | |
304 // Rise from low offset to high max-offset in 100 point steps. | |
305 for (var time = this.intervals_[i].start; | |
306 time <= this.intervals_[i].end; time += this.range.resolution) { | |
307 var numPoints = time / this.range.resolution; | |
308 dataPoints.push({time: time, value: offset + (numPoints % 100) * | |
309 (max - 2 * offset) / 100}); | |
310 } | |
311 } | |
312 return dataPoints; | |
313 }, | |
314 | |
315 /** | |
316 * Request new metric data, assuming the metric table and chart already | 306 * Request new metric data, assuming the metric table and chart already |
317 * exist. | 307 * exist. |
318 * @param {!string} metric Metric for which to get data | 308 * @param {string} metric The metric for which to get data. |
319 */ | 309 */ |
320 refreshMetric: function(metric) { | 310 refreshMetric: function(metric) { |
321 var metricValue = this.metricMap_[metric]; | 311 var metricValue = this.metricMap_[metric]; |
322 | 312 |
323 metricValue.data = null; // Mark metric as awaiting response. | 313 metricValue.data = null; // Mark metric as awaiting response. |
324 this.onReceiveMetric(metric, | 314 chrome.send('getMetric', [metricValue.id, |
325 this.getMockDataPoints(metricValue.yAxis.max, 5)); | 315 this.start, this.end, this.range.resolution]); |
326 // Replace with: | |
327 // chrome.send("getMetric", this.range.start, this.range.end, | |
328 // this.range.resolution, this.onReceiveMetric); | |
329 }, | 316 }, |
330 | 317 |
331 /** | 318 /** |
332 * Receive new datapoints for |metric|, convert the data to Flot-usable | 319 * Receive new datapoints for a metric, convert the data to Flot-usable |
333 * form, and redraw all affected charts. | 320 * form, and redraw all affected charts. |
334 * @param {!string} metric Metric to which |points| applies | 321 * @param {!{ |
335 * @param {!Array.<{time: !number, value: !number}>} points | 322 * type: number, |
336 * new data points | 323 * points: !Array.<{time: number, value: number}> |
| 324 * }} result An object giving metric ID, and time/value point pairs for |
| 325 * that id. |
337 */ | 326 */ |
338 onReceiveMetric: function(metric, points) { | 327 getMetricCallback: function(result) { |
339 var metricValue = this.metricMap_[metric]; | 328 var metricValue = this.metricMap_[result.metricType]; |
340 | |
341 // Might have been dropped while waiting for data. | 329 // Might have been dropped while waiting for data. |
342 if (metricValue.divs.length == 0) | 330 if (metricValue.divs.length == 0) |
343 return; | 331 return; |
344 | 332 |
345 var series = []; | 333 var series = []; |
346 metricValue.data = [series]; | 334 metricValue.data = [series]; // Data ends with current open series. |
347 | 335 |
348 // Traverse the points, and the intervals, in parallel. Both are in | 336 // Traverse the points, and the intervals, in parallel. Both are in |
349 // ascending time order. Create a sequence of data "series" (per Flot) | 337 // ascending time order. Create a sequence of data "series" (per Flot) |
350 // arrays, with each series comprising all points within a given interval. | 338 // arrays, with each series comprising all points within a given interval. |
351 var interval = this.intervals_[0]; | 339 var interval = this.intervals_[0]; |
352 var intervalIndex = 0; | 340 var intervalIndex = 0; |
353 var pointIndex = 0; | 341 var pointIndex = 0; |
354 while (pointIndex < points.length && | 342 while (pointIndex < result.points.length && |
355 intervalIndex < this.intervals_.length) { | 343 intervalIndex < this.intervals_.length) { |
356 var point = points[pointIndex++]; | 344 var point = result.points[pointIndex++]; |
357 while (intervalIndex < this.intervals_.length && | 345 while (intervalIndex < this.intervals_.length && |
358 point.time > interval.end) { | 346 point.time > interval.end) { |
359 interval = this.intervals_[++intervalIndex]; // Jump to new interval. | 347 interval = this.intervals_[++intervalIndex]; // Jump to new interval. |
360 if (series.length > 0) { | 348 if (series.length > 0) { |
361 series = []; // Start a new series. | 349 series = []; // Start a new series. |
362 metricValue.data.push(series); // Put it on the end of the data. | 350 metricValue.data.push(series); // Put it on the end of the data. |
363 } | 351 } |
364 } | 352 } |
365 if (intervalIndex < this.intervals_.length && | 353 if (intervalIndex < this.intervals_.length && |
366 point.time > interval.start) | 354 point.time > interval.start) { |
367 series.push([point.time, point.value]); | 355 series.push([point.time - timezoneOffset, point.value]); |
| 356 } |
368 } | 357 } |
369 | |
370 metricValue.divs.forEach(this.drawChart, this); | 358 metricValue.divs.forEach(this.drawChart, this); |
371 }, | 359 }, |
372 | 360 |
373 /** | 361 /** |
374 * Add a new event to the chart(s). | 362 * Add a new event to the chart(s). |
375 * @param {!string} eventType type of event to start displaying | 363 * @param {string} eventType The type of event to start displaying. |
376 */ | 364 */ |
377 addEventType: function(eventType) { | 365 addEventType: function(eventType) { |
378 // Events show on all charts. | 366 // Events show on all charts. |
379 this.eventMap_[eventType].divs = this.charts; | 367 this.eventMap_[eventType].divs = this.charts; |
380 this.refreshEventType(eventType); | 368 this.refreshEventType(eventType); |
381 }, | 369 }, |
382 | 370 |
383 /* | 371 /* |
384 * Remove an event from the chart(s). | 372 * Remove an event from the chart(s). |
385 * @param {!string} eventType type of event to stop displaying | 373 * @param {string} eventType The type of event to stop displaying. |
386 */ | 374 */ |
387 dropEventType: function(eventType) { | 375 dropEventType: function(eventType) { |
388 var eventValue = this.eventMap_[eventType]; | 376 var eventValue = this.eventMap_[eventType]; |
389 var affectedCharts = eventValue.divs; | 377 var affectedCharts = eventValue.divs; |
390 eventValue.divs = []; | 378 eventValue.divs = []; |
391 | 379 |
392 affectedCharts.forEach(this.drawChart, this); | 380 affectedCharts.forEach(this.drawChart, this); |
393 }, | 381 }, |
394 | 382 |
395 /** | 383 /** |
396 * Return mock event points for testing. | |
397 * @param {!string} eventType type of event to generate mock data for | |
398 * @return {!Array.<{time: !number, longDescription: !string}>} | |
399 */ | |
400 getMockEventValues: function(eventType) { | |
401 var mockValues = []; | |
402 for (var i = 0; i < this.intervals_.length; i++) { | |
403 var interval = this.intervals_[i]; | |
404 | |
405 mockValues.push({ | |
406 time: interval.start, | |
407 longDescription: eventType + ' at ' + | |
408 new Date(interval.start) + ' blah, blah blah'}); | |
409 mockValues.push({ | |
410 time: (interval.start + interval.end) / 2, | |
411 longDescription: eventType + ' at ' + | |
412 new Date((interval.start + interval.end) / 2) + ' blah, blah'}); | |
413 mockValues.push({ | |
414 time: interval.end, | |
415 longDescription: eventType + ' at ' + new Date(interval.end) + | |
416 ' blah, blah blah'}); | |
417 } | |
418 return mockValues; | |
419 }, | |
420 | |
421 /** | |
422 * Request new data for |eventType|, for times in the current range. | 384 * Request new data for |eventType|, for times in the current range. |
423 * @param {!string} eventType type of event to get new data for | 385 * @param {string} eventType The type of event to get new data for. |
424 */ | 386 */ |
425 refreshEventType: function(eventType) { | 387 refreshEventType: function(eventType) { |
426 // Mark eventType as awaiting response. | 388 // Mark eventType as awaiting response. |
427 this.eventMap_[eventType].data = null; | 389 this.eventMap_[eventType].data = null; |
428 this.onReceiveEventType(eventType, this.getMockEventValues(eventType)); | 390 |
429 // Replace with: | 391 chrome.send('getEvents', [eventType, this.start, this.end]); |
430 // chrome.send("getEvents", eventType, this.range.start, this.range.end); | |
431 }, | 392 }, |
432 | 393 |
433 /** | 394 /** |
434 * Receive new data for |eventType|. If the event has been deselected while | 395 * Receive new events for |eventType|. If |eventType| has been deselected |
435 * awaiting webui response, do nothing. Otherwise, save the data directly, | 396 * while awaiting webui handler response, do nothing. Otherwise, save the |
436 * since events are handled differently than metrics when drawing | 397 * data directly, since events are handled differently than metrics |
437 * (no "series"), and redraw all the affected charts. | 398 * when drawing (no "series"), and redraw all the affected charts. |
438 * @param {!string} eventType type of event the new data applies to | 399 * @param {!{ |
439 * @param {!Array.<{time: !number, longDescription: !string}>} values | 400 * type: number, |
440 * new event values | 401 * points: !Array.<{time: number, longDescription: string}> |
| 402 * }} result An object giving eventType ID, and time/description pairs for |
| 403 * each event of that type in the range requested. |
441 */ | 404 */ |
442 onReceiveEventType: function(eventType, values) { | 405 getEventsCallback: function(result) { |
443 var eventValue = this.eventMap_[eventType]; | 406 var eventValue = this.eventMap_[result.eventType]; |
444 | 407 |
445 if (eventValue.divs.length == 0) | 408 if (eventValue.divs.length == 0) |
446 return; | 409 return; |
447 | 410 |
448 eventValue.data = values; | 411 result.points.forEach(function(element) { |
| 412 element.time -= timezoneOffset; |
| 413 }); |
| 414 |
| 415 eventValue.data = result.points; |
449 eventValue.divs.forEach(this.drawChart, this); | 416 eventValue.divs.forEach(this.drawChart, this); |
450 }, | 417 }, |
451 | 418 |
452 /** | 419 /** |
453 * Return an object containing an array of metrics and another of events | 420 * Return an object containing an array of metrics and another of events |
454 * that include |chart| as one of the divs into which they display. | 421 * that include |chart| as one of the divs into which they display. |
455 * @param {HTMLDivElement} chart div for which to get relevant items | 422 * @param {HTMLDivElement} chart The <div> for which to get relevant items. |
456 * @return {!{metrics: !Array,<Object>, events: !Array.<Object>}} | 423 * @return {!{metrics: !Array,<Object>, events: !Array.<Object>}} |
457 * @private | 424 * @private |
458 */ | 425 */ |
459 getChartData_: function(chart) { | 426 getChartData_: function(chart) { |
460 var result = {metrics: [], events: []}; | 427 var result = {metrics: [], events: []}; |
461 | 428 |
462 for (var metric in this.metricMap_) { | 429 for (var metric in this.metricMap_) { |
463 var metricValue = this.metricMap_[metric]; | 430 var metricValue = this.metricMap_[metric]; |
464 | 431 |
465 if (metricValue.divs.indexOf(chart) != -1) | 432 if (metricValue.divs.indexOf(chart) != -1) |
466 result.metrics.push(metricValue); | 433 result.metrics.push(metricValue); |
467 } | 434 } |
468 | 435 |
469 for (var eventType in this.eventMap_) { | 436 for (var eventType in this.eventMap_) { |
470 var eventValue = this.eventMap_[eventType]; | 437 var eventValue = this.eventMap_[eventType]; |
471 | 438 |
472 // Events post to all divs, if they post to any. | 439 // Events post to all divs, if they post to any. |
473 if (eventValue.divs.length > 0) | 440 if (eventValue.divs.length > 0) |
474 result.events.push(eventValue); | 441 result.events.push(eventValue); |
475 } | 442 } |
476 | 443 |
477 return result; | 444 return result; |
478 }, | 445 }, |
479 | 446 |
480 /** | 447 /** |
481 * Check all entries in an object of the type returned from getChartData, | 448 * Check all entries in an object of the type returned from getChartData, |
482 * above, to see if all events and metrics have completed data (none is | 449 * above, to see if all events and metrics have completed data (none is |
483 * awaiting an asynchronous webui response to get their current data). | 450 * awaiting an asynchronous webui handler response to get their current |
| 451 * data). |
484 * @param {!{metrics: !Array,<Object>, events: !Array.<Object>}} chartData | 452 * @param {!{metrics: !Array,<Object>, events: !Array.<Object>}} chartData |
485 * event/metric data to check for readiness | 453 * The event/metric data to check for readiness. |
486 * @return {boolean} is data ready? | 454 * @return {boolean} Whether data is ready. |
487 * @private | 455 * @private |
488 */ | 456 */ |
489 isDataReady_: function(chartData) { | 457 isDataReady_: function(chartData) { |
490 for (var i = 0; i < chartData.metrics.length; i++) { | 458 for (var i = 0; i < chartData.metrics.length; i++) { |
491 if (!chartData.metrics[i].data) | 459 if (!chartData.metrics[i].data) |
492 return false; | 460 return false; |
493 } | 461 } |
494 | 462 |
495 for (var i = 0; i < chartData.events.length; i++) { | 463 for (var i = 0; i < chartData.events.length; i++) { |
496 if (!chartData.events[i].data) | 464 if (!chartData.events[i].data) |
497 return false; | 465 return false; |
498 } | 466 } |
499 | 467 |
500 return true; | 468 return true; |
501 }, | 469 }, |
502 | 470 |
503 /** | 471 /** |
504 * Create and return an array of "markings" (per Flot), representing | 472 * Create and return an array of "markings" (per Flot), representing |
505 * vertical lines at the event time, in the event's color. Also add | 473 * vertical lines at the event time, in the event's color. Also add |
506 * (not per Flot) a |description| property to each, to be used for hand | 474 * (not per Flot) a |description| property to each, to be used for hand |
507 * creating description boxes. | 475 * creating description boxes. |
508 * @param {!Array.<{ | 476 * @param {!Array.<{ |
509 * description: !string, | 477 * description: string, |
510 * color: !string, | 478 * color: string, |
511 * data: !Array.<{time: !number}> | 479 * data: !Array.<{time: number}> |
512 * }>} eventValues events to make markings for | 480 * }>} eventValues The events to make markings for. |
513 * @return {!Array.<{ | 481 * @return {!Array.<{ |
514 * color: !string, | 482 * color: string, |
515 * description: !string, | 483 * description: string, |
516 * xaxis: {from: !number, to: !number} | 484 * xaxis: {from: number, to: number} |
517 * }>} mark data structure for Flot to use | 485 * }>} A marks data structure for Flot to use. |
518 * @private | 486 * @private |
519 */ | 487 */ |
520 getEventMarks_: function(eventValues) { | 488 getEventMarks_: function(eventValues) { |
521 var markings = []; | 489 var markings = []; |
522 | 490 |
523 for (var i = 0; i < eventValues.length; i++) { | 491 for (var i = 0; i < eventValues.length; i++) { |
524 var eventValue = eventValues[i]; | 492 var eventValue = eventValues[i]; |
525 for (var d = 0; d < eventValue.data.length; d++) { | 493 for (var d = 0; d < eventValue.data.length; d++) { |
526 var point = eventValue.data[d]; | 494 var point = eventValue.data[d]; |
527 markings.push({ | 495 if (point.time >= this.start - timezoneOffset && |
528 color: eventValue.color, | 496 point.time <= this.end - timezoneOffset) { |
529 description: eventValue.description, | 497 markings.push({ |
530 xaxis: {from: point.time, to: point.time} | 498 color: eventValue.color, |
531 }); | 499 description: eventValue.description, |
| 500 xaxis: {from: point.time, to: point.time} |
| 501 }); |
| 502 } else { |
| 503 console.log('Event out of time range ' + this.start + ' -> ' + |
| 504 this.end + ' at: ' + point.time); |
| 505 } |
532 } | 506 } |
533 } | 507 } |
534 | 508 |
535 return markings; | 509 return markings; |
536 }, | 510 }, |
537 | 511 |
538 /** | 512 /** |
539 * Redraw the chart in div |chart|, *if* all its dependent data is present. | 513 * Redraw the chart in div |chart|, *if* all its dependent data is present. |
540 * Otherwise simply return, and await another call when all data is | 514 * Otherwise simply return, and await another call when all data is |
541 * available. | 515 * available. |
542 * @param {HTMLDivElement} chart div to redraw | 516 * @param {HTMLDivElement} chart The <div> to redraw. |
543 */ | 517 */ |
544 drawChart: function(chart) { | 518 drawChart: function(chart) { |
545 var chartData = this.getChartData_(chart); | 519 var chartData = this.getChartData_(chart); |
546 | 520 |
547 if (!this.isDataReady_(chartData)) | 521 if (!this.isDataReady_(chartData)) |
548 return; | 522 return; |
549 | 523 |
550 var seriesSeq = []; | 524 var seriesSeq = []; |
551 var yAxes = []; | 525 var yAxes = []; |
552 chartData.metrics.forEach(function(value) { | 526 chartData.metrics.forEach(function(value) { |
553 yAxes.push(value.yAxis); | 527 yAxes.push(value.yAxis); |
554 for (var i = 0; i < value.data.length; i++) { | 528 for (var i = 0; i < value.data.length; i++) { |
555 seriesSeq.push({ | 529 seriesSeq.push({ |
556 color: value.yAxis.color, | 530 color: value.yAxis.color, |
557 data: value.data[i], | 531 data: value.data[i], |
558 label: i == 0 ? value.description + ' (' + value.units + ')' : null, | 532 label: i == 0 ? value.description + ' (' + value.units + ')' : null, |
559 yaxis: yAxes.length, // Use just-added Y axis. | 533 yaxis: yAxes.length, // Use just-added Y axis. |
560 }); | 534 }); |
561 } | 535 } |
562 }); | 536 }); |
563 | 537 |
| 538 // Ensure at least one y axis, as a reference for event placement. |
| 539 if (yAxes.length == 0) |
| 540 yAxes.push({max: 1.0}); |
| 541 |
564 var markings = this.getEventMarks_(chartData.events); | 542 var markings = this.getEventMarks_(chartData.events); |
565 var chart = this.charts[0]; | 543 var chart = this.charts[0]; |
566 var plot = $.plot(chart, seriesSeq, { | 544 var plot = $.plot(chart, seriesSeq, { |
567 yaxes: yAxes, | 545 yaxes: yAxes, |
568 xaxis: {mode: 'time'}, | 546 xaxis: { |
| 547 mode: 'time', |
| 548 min: this.start - timezoneOffset, |
| 549 max: this.end - timezoneOffset |
| 550 }, |
| 551 points: {show: true, radius: 1}, |
| 552 lines: {show: true}, |
569 grid: {markings: markings} | 553 grid: {markings: markings} |
570 }); | 554 }); |
571 | 555 |
572 // For each event in |markings|, create also a label div, with left | 556 // For each event in |markings|, create also a label div, with left |
573 // edge colinear with the event vertical-line. Top of label is | 557 // edge colinear with the event vertical line. Top of label is |
574 // presently a hack-in, putting labels in three tiers of 25px height | 558 // presently a hack-in, putting labels in three tiers of 25px height |
575 // each to avoid overlap. Will need something better. | 559 // each to avoid overlap. Will need something better. |
576 var labelTemplate = $('#labelTemplate')[0]; | 560 var labelTemplate = $('#label-template')[0]; |
577 for (var i = 0; i < markings.length; i++) { | 561 for (var i = 0; i < markings.length; i++) { |
578 var mark = markings[i]; | 562 var mark = markings[i]; |
579 var point = | 563 var point = |
580 plot.pointOffset({x: mark.xaxis.to, y: yAxes[0].max, yaxis: 1}); | 564 plot.pointOffset({x: mark.xaxis.to, y: yAxes[0].max, yaxis: 1}); |
581 var labelDiv = labelTemplate.cloneNode(true); | 565 var labelDiv = labelTemplate.cloneNode(true); |
582 labelDiv.innerText = mark.description; | 566 labelDiv.innerText = mark.description; |
583 labelDiv.style.left = point.left + 'px'; | 567 labelDiv.style.left = point.left + 'px'; |
584 labelDiv.style.top = (point.top + 25 * (i % 3)) + 'px'; | 568 labelDiv.style.top = (point.top + 25 * (i % 3)) + 'px'; |
| 569 |
585 chart.appendChild(labelDiv); | 570 chart.appendChild(labelDiv); |
586 } | 571 } |
587 } | 572 } |
588 }; | 573 }; |
589 return { | 574 return { |
590 PerformanceMonitor: PerformanceMonitor | 575 PerformanceMonitor: PerformanceMonitor |
591 }; | 576 }; |
592 }(); | 577 }); |
593 | 578 |
594 var performanceMonitor = new Installer.PerformanceMonitor(); | 579 var PerformanceMonitor = new performance_monitor.PerformanceMonitor(); |
OLD | NEW |