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