Chromium Code Reviews| 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 | |
| 6 cr.define('performance_monitor', function() { | 5 cr.define('performance_monitor', function() { |
| 7 'use strict'; | 6 'use strict'; |
| 8 | 7 |
| 9 /** | 8 /** |
| 10 * Enum for time ranges, giving a descriptive name, time span prior to |now|, | 9 * Enum for time ranges, giving for each a descriptive name, time span in ms |
| 11 * data point resolution, and time-label frequency and format for each. | 10 * prior to |now|, data point resolution in ms, time-label frequency in data |
| 11 * points per label, and format using Date.js standards, for each. | |
| 12 * | |
| 13 * The additional |element| field is added in setupTimeRangeTab_, giving | |
| 14 * the HTML input tag for the radio button selecting the given time range. | |
| 12 * @enum {{ | 15 * @enum {{ |
| 13 * value: number, | 16 * value: number, |
| 14 * name: string, | 17 * name: string, |
| 15 * timeSpan: number, | 18 * timeSpan: number, |
| 16 * resolution: number, | 19 * resolution: number, |
| 17 * labelEvery: number, | 20 * labelEvery: number, |
| 18 * format: string | 21 * format: string, |
| 22 * element: HTMLElement | |
| 19 * }} | 23 * }} |
| 20 * @private | 24 * @private |
| 21 */ | 25 */ |
| 22 var TimeRange_ = { | 26 var TimeRange_ = { |
| 23 // Prior 12 min, resolution of 1s, at most 720 points. | 27 // Prior 15 min, resolution of 5s, at most 180 points. |
| 24 // Labels at 60 point (1 min) intervals. | 28 // Labels at 12 point (1 min) intervals. |
| 25 minutes: {value: 0, name: 'Last 12 min', timeSpan: 720 * 1000, | 29 minutes: {value: 0, name: 'Last 15 min', timeSpan: 900 * 1000, |
| 26 resolution: 1000, labelEvery: 60, format: 'MM-dd'}, | 30 resolution: 1000 * 5, labelEvery: 12, format: 'MM-dd'}, |
| 27 | 31 |
| 28 // Prior hour, resolution of 5s, at most 720 points. | 32 // Prior hour, resolution of 20s, at most 180 points. |
| 29 // Labels at 60 point (5 min) intervals. | 33 // Labels at 15 point (5 min) intervals. |
| 30 hour: {value: 1, name: 'Last Hour', timeSpan: 3600 * 1000, | 34 hour: {value: 1, name: 'Last Hour', timeSpan: 3600 * 1000, |
| 31 resolution: 1000 * 5, labelEvery: 60, format: 'MM-dd'}, | 35 resolution: 1000 * 20, labelEvery: 15, format: 'MM-dd'}, |
| 32 | 36 |
| 33 // Prior day, resolution of 2 min, at most 720 points. | 37 // Prior day, resolution of 5 min, at most 288 points. |
| 34 // Labels at 90 point (3 hour) intervals. | 38 // Labels at 36 point (3 hour) intervals. |
| 35 day: {value: 2, name: 'Last Day', timeSpan: 24 * 3600 * 1000, | 39 day: {value: 2, name: 'Last Day', timeSpan: 24 * 3600 * 1000, |
| 36 resolution: 1000 * 60 * 2, labelEvery: 90, format: 'MM-dd'}, | 40 resolution: 1000 * 60 * 5, labelEvery: 36, format: 'MM-dd'}, |
| 37 | 41 |
| 38 // Prior week, resolution of 15 min -- at most 672 data points. | 42 // Prior week, resolution of 1 hr -- at most 168 data points. |
| 39 // Labels at 96 point (daily) intervals. | 43 // Labels at 24 point (daily) intervals. |
| 40 week: {value: 3, name: 'Last Week', timeSpan: 7 * 24 * 3600 * 1000, | 44 week: {value: 3, name: 'Last Week', timeSpan: 7 * 24 * 3600 * 1000, |
| 41 resolution: 1000 * 60 * 15, labelEvery: 96, format: 'M/d'}, | 45 resolution: 1000 * 3600, labelEvery: 24, format: 'M/d'}, |
| 42 | 46 |
| 43 // Prior month (30 days), resolution of 1 hr -- at most 720 data points. | 47 // Prior month (30 days), resolution of 4 hr -- at most 180 data points. |
| 44 // Labels at 168 point (weekly) intervals. | 48 // Labels at 42 point (weekly) intervals. |
| 45 month: {value: 4, name: 'Last Month', timeSpan: 30 * 24 * 3600 * 1000, | 49 month: {value: 4, name: 'Last Month', timeSpan: 30 * 24 * 3600 * 1000, |
| 46 resolution: 1000 * 3600, labelEvery: 168, format: 'M/d'}, | 50 resolution: 1000 * 3600 * 4, labelEvery: 42, format: 'M/d'}, |
| 47 | 51 |
| 48 // Prior quarter (90 days), resolution of 3 hr -- at most 720 data points. | 52 // Prior quarter (90 days), resolution of 12 hr -- at most 180 data points. |
| 49 // Labels at 112 point (fortnightly) intervals. | 53 // Labels at 28 point (fortnightly) intervals. |
| 50 quarter: {value: 5, name: 'Last Quarter', timeSpan: 90 * 24 * 3600 * 1000, | 54 quarter: {value: 5, name: 'Last Quarter', timeSpan: 90 * 24 * 3600 * 1000, |
| 51 resolution: 1000 * 3600 * 3, labelEvery: 112, format: 'M/yy'}, | 55 resolution: 1000 * 3600 * 12, labelEvery: 28, format: 'M/yy'}, |
| 52 }; | 56 }; |
| 53 | 57 |
| 54 /* | 58 /* |
| 55 * Offset, in ms, by which to subtract to convert GMT to local time | 59 * Table of colors to use for metrics and events. Basically boxing the |
| 56 * @type {number} | 60 * colorwheel, but leaving out yellows and fully saturated colors. |
| 57 */ | 61 * @type {Array.<string>} |
| 58 var timezoneOffset = new Date().getTimezoneOffset() * 60000; | 62 * @private |
| 63 */ | |
| 64 var ColorTable_ = [ | |
| 65 'rgb(255, 128, 128)', 'rgb(128, 255, 128)', 'rgb(128, 128, 255)', | |
| 66 'rgb(128, 255, 255)', 'rgb(255, 128, 255)', // No bright yellow | |
| 67 'rgb(255, 64, 64)', 'rgb( 64, 255, 64)', 'rgb( 64, 64, 255)', | |
| 68 'rgb( 64, 255, 255)', 'rgb(255, 64, 255)', // No medium yellow either | |
| 69 'rgb(128, 64, 64)', 'rgb( 64, 128, 64)', 'rgb( 64, 64, 128)', | |
| 70 'rgb( 64, 128, 128)', 'rgb(128, 64, 128)', 'rgb(128, 128, 64)' | |
| 71 ]; | |
| 72 | |
| 73 /* | |
| 74 * Offset, in ms, by which to subtract to convert GMT to local time. | |
| 75 * @type {number} | |
| 76 * @private | |
| 77 */ | |
| 78 var timezoneOffset_ = new Date().getTimezoneOffset() * 60000; | |
| 79 | |
| 80 /* | |
| 81 * Additional range multiplier to ensure that points don't hit the top of | |
| 82 * the graph. | |
| 83 * @type {number} | |
| 84 * @private | |
| 85 */ | |
| 86 var yAxisMargin_ = 1.05; | |
| 87 | |
| 88 /* | |
| 89 * Number of time resolution periods to wait between automated update of | |
| 90 * graphs. | |
| 91 * @type {number} | |
| 92 * @private | |
| 93 */ | |
| 94 var intervalMultiple_ = 2; | |
| 95 | |
| 96 /* | |
| 97 * Number of milliseconds to wait before deciding that the most recent | |
| 98 * resize event is not going to be followed immediately by another, and | |
| 99 * thus needs handling. | |
| 100 * @type {number} | |
| 101 * @private | |
| 102 */ | |
| 103 var resizeDelay_ = 500; | |
|
Jeffrey Yasskin
2012/09/05 22:21:54
Fair enough.
| |
| 59 | 104 |
| 60 /** @constructor */ | 105 /** @constructor */ |
| 61 function PerformanceMonitor() { | 106 function PerformanceMonitor() { |
| 62 this.__proto__ = PerformanceMonitor.prototype; | 107 this.__proto__ = PerformanceMonitor.prototype; |
| 63 /** | 108 |
| 64 * All metrics have entries, but those not displayed have an empty div list. | 109 /** |
| 65 * If a div list is not empty, the associated data will be non-null, or | 110 * Detailed information on a metric in the UI. MetricId is a unique |
| 66 * null but about to be filled by webui handler response. Thus, any metric | 111 * identifying number for the metric, provided by the webui, and assumed |
| 67 * with non-empty div list but null data is awaiting a data response | 112 * to be densely populated. All metrics also have a description and |
| 68 * from the webui handler. The webui handler uses numbers to uniquely | 113 * an associated category giving their unit information and home chart. |
| 69 * identify metric and event types, so we use the same numbers (in | 114 * They also have a color in which they are displayed, and a maximum value |
| 70 * string form) for the map key, repeating the id number in the |id| | 115 * by which to scale their y-axis. |
| 71 * field, as a number. | 116 * |
| 72 * @type {Object.<string, { | 117 * Although in the present UI each metric appears only in the home chart of |
| 73 * id: number, | 118 * its metric category, we keep the divs property to allow future |
| 74 * description: string, | 119 * modifications in which the same metric might appear in several charts |
| 75 * units: string, | 120 * for side-by-side comparisons. Metrics not being displayed have an |
| 76 * yAxis: !{max: number, color: string}, | 121 * empty div list. If a div list is not empty, the associated data will |
| 77 * divs: !Array.<HTMLDivElement>, | 122 * be non-null, or null but about to be filled by webui handler response. |
| 78 * data: ?Array.<{time: number, value: number}> | 123 * Thus, any metric with non-empty div list but null data is awaiting a |
| 79 * }>} | 124 * data response from the webui handler. |
| 80 * @private | 125 * @typedef {{ |
| 81 */ | 126 * metricId: number, |
| 82 this.metricMap_ = {}; | 127 * description: string, |
| 83 | 128 * category: !Object, |
| 84 /* | 129 * color: string, |
| 85 * Similar data for events, though no yAxis info is needed since events | 130 * maxValue: number, |
| 86 * are simply labelled markers at X locations. Rules regarding null data | 131 * divs: !Array.<HTMLDivElement>, |
| 87 * with non-empty div list apply here as for metricMap_ above. | 132 * data: ?Array.<{time: number, value: number}> |
| 88 * @type {Object.<number, { | 133 * }} |
| 89 * id: number, | 134 */ |
| 135 PerformanceMonitor.MetricDetails; | |
| 136 | |
| 137 /** | |
| 138 * Similar data for events as for metrics, though no yAxis info is needed | |
|
Devlin
2012/09/07 18:44:54
nit: Be consistent in spelling y-axis/yAxis in com
clintstaley
2012/09/07 20:39:52
Done.
| |
| 139 * since events are simply labelled markers at X locations. Rules regarding | |
| 140 * null data with non-empty div list apply here as for metricDetailsMap_ | |
| 141 * above. The |data| field follows a special rule not describable in | |
| 142 * JSDoc: Each array element may include arbitrary other properties with | |
| 143 * value {label: 'some label', value: 'some value'} for display in the | |
| 144 * event mouseover. These will vary per event type. | |
|
Jeffrey Yasskin
2012/09/05 22:21:54
I see. So effectively each array element maps a ti
clintstaley
2012/09/07 20:39:52
I assume you mean for me to revise the comment acc
| |
| 145 * @typedef {{ | |
| 146 * eventId: number, | |
| 147 * name: string, | |
| 148 * popupTitle: string, | |
| 90 * description: string, | 149 * description: string, |
| 91 * color: string, | 150 * color: string, |
| 92 * divs: !Array.<HTMLDivElement>, | 151 * divs: !Array.<HTMLDivElement>, |
| 93 * data: ?Array.<{time: number, longDescription: string}> | 152 * data: ?Array.<{time: number}> |
| 153 * }} | |
| 154 */ | |
| 155 PerformanceMonitor.EventDetails; | |
| 156 | |
| 157 /** | |
| 158 * Metrics fall into categories that have common units and thus may | |
| 159 * share a common graph, or share y-axes within a multi-y-axis graph. | |
| 160 * Each category has one home chart in which metrics of that category | |
| 161 * are displayed. Currently this is also the only chart in which such | |
| 162 * metrics are displayed, but the design permits a metric to show in | |
| 163 * several charts if this is useful later on. | |
| 164 * @type {Object.<string, { | |
| 165 * metricCategoryId: number, | |
| 166 * name: string, | |
| 167 * description: string, | |
| 168 * unit: string, | |
| 169 * details: Array.<{!PerformanceMonitor.MetricDetails}>, | |
| 170 * homeChart: !HTMLDivElement | |
| 94 * }>} | 171 * }>} |
| 95 * @private | 172 * @private |
| 96 */ | 173 */ |
| 97 this.eventMap_ = {}; | 174 this.metricCategoryMap_ = {}; |
| 175 | |
| 176 /** | |
| 177 * Comprehensive map from metricId to MetricDetails. | |
| 178 * @type {Object.<number, {PerformanceMonitor.MetricDetails}>} | |
| 179 * @private | |
| 180 */ | |
| 181 this.metricDetailsMap_ = {}; | |
| 182 | |
| 183 /** | |
| 184 * Events fall into categories just like metrics, above. This category | |
| 185 * grouping is not as important as that for metrics, since events | |
| 186 * needn't share maxima, y-axes, nor units, and since events appear on | |
| 187 * all charts. But grouping of event categories in the event-selection | |
| 188 * UI is still useful. | |
| 189 * @type {Object.<string, { | |
| 190 * eventCategoryId: number, | |
| 191 * name: string, | |
| 192 * description: string, | |
| 193 * details: !Array.<!PerformanceMonitor.EventDetails>, | |
| 194 * }>} | |
| 195 * @private | |
| 196 */ | |
| 197 this.eventCategoryMap_ = {}; | |
| 198 | |
| 199 /** | |
| 200 * Comprehensive map from eventId to EventDetails. | |
| 201 * @type {Object.<number, {PerformanceMonitor.EventDetails}>} | |
| 202 * @private | |
| 203 */ | |
| 204 this.eventDetailsMap_ = {}; | |
| 98 | 205 |
| 99 /** | 206 /** |
| 100 * Time periods in which the browser was active and collecting metrics | 207 * Time periods in which the browser was active and collecting metrics |
| 101 * and events. | 208 * and events. |
| 102 * @type {!Array.<{start: number, end: number}>} | 209 * @type {!Array.<{start: number, end: number}>} |
| 103 * @private | 210 * @private |
| 104 */ | 211 */ |
| 105 this.intervals_ = []; | 212 this.intervals_ = []; |
| 106 | 213 |
| 107 this.setupTimeRangeChooser_(); | 214 /** |
| 108 chrome.send('getAllEventTypes'); | 215 * Handle of timer interval function used to update charts |
| 109 chrome.send('getAllMetricTypes'); | 216 * @type {Object} |
| 110 this.setupMainChart_(); | 217 * @private |
| 218 */ | |
| 219 this.updateTimer_ = null; | |
| 220 | |
| 221 /** | |
| 222 * Handle of timer interval function used to check for resizes. Nonnull | |
| 223 * only when resize events are coming steadily. | |
| 224 * @type {Object} | |
| 225 * @private | |
| 226 */ | |
| 227 this.resizeTimer_ = null; | |
| 228 | |
| 229 /** | |
| 230 * Time of last UNHANDLED resize event, or null if no resizes or if | |
| 231 * last one has been handled. | |
| 232 * @type {number} | |
| 233 * @private | |
| 234 */ | |
| 235 this.lastResizeTime_ = null; | |
| 236 | |
| 237 /** | |
| 238 * All chart divs in the display, whether hidden or not. Presently | |
| 239 * this has one entry for each metric category in |this.metricCategoryMap|. | |
| 240 * @type {Array.<HTMLDivElement>} | |
| 241 * @private | |
| 242 */ | |
| 243 this.charts_ = []; | |
| 244 | |
| 245 this.setupTimeRangeTab_(); | |
| 246 chrome.send('getEventTypes'); | |
| 247 chrome.send('getMetricTypes'); | |
| 111 TimeRange_.day.element.click(); | 248 TimeRange_.day.element.click(); |
| 112 } | 249 } |
| 113 | 250 |
| 114 PerformanceMonitor.prototype = { | 251 PerformanceMonitor.prototype = { |
| 115 /** | 252 /** |
| 116 * Receive a list of all metrics, and populate |this.metricMap_| to | 253 * Receive a list of all metric categories, each with its correspoinding |
|
Devlin
2012/09/07 18:44:54
nit: corresponding
clintstaley
2012/09/07 20:39:52
Done.
| |
| 117 * reflect said list. Reconfigure the checkbox set for metric selection. | 254 * list of metric details. Populate |this.metricCategoryMap_| and |
| 118 * @param {Array.<{metricType: string, shortDescription: string}>} | 255 * |this.metrictDetailsMap_| to reflect said list. Reconfigure the |
| 119 * allMetrics All metrics from which to select. | 256 * checkbox set for metric selection. |
| 257 * @param {Array.<{ | |
| 258 * metricCategoryId: number, | |
| 259 * name: string, | |
| 260 * unit: string, | |
| 261 * description: string, | |
| 262 * details: Array.<{ | |
| 263 * metricId: number, | |
| 264 * name: string, | |
| 265 * description: string | |
| 266 * }> | |
| 267 * }>} categories All metric categories needing charts and checkboxes. | |
| 120 */ | 268 */ |
| 121 getAllMetricTypesCallback: function(allMetrics) { | 269 getMetricTypesCallback: function(categories) { |
| 122 for (var i = 0; i < allMetrics.length; i++) { | 270 categories.forEach(function(category) { |
| 123 var metric = allMetrics[i]; | 271 this.addCategoryChart_(category); |
| 272 this.metricCategoryMap_[category.metricCategoryId] = category; | |
| 124 | 273 |
| 125 this.metricMap_[metric.metricType] = { | 274 category.details.forEach(function(metric) { |
| 126 id: metric.metricType, | 275 metric.color = ColorTable_[metric.metricId % ColorTable_.length]; |
| 127 description: metric.shortDescription, | 276 metric.maxValue = 1; |
| 128 units: metric.units, | 277 metric.divs = []; |
| 129 yAxis: {min: 0, max: metric.maxValue, | 278 metric.data = null; |
| 130 color: jQuery.color.parse('blue')}, | 279 metric.category = category; |
| 131 divs: [], | 280 this.metricDetailsMap_[metric.metricId] = metric; |
| 132 data: null | 281 }, this); |
| 133 }; | 282 }, this); |
| 134 } | |
| 135 | 283 |
| 136 this.setupCheckboxes_($('#choose-metrics')[0], | 284 this.setupCheckboxes_($('#choose-metrics')[0], |
| 137 this.metricMap_, this.addMetric, this.dropMetric); | 285 this.metricCategoryMap_, 'metricId', this.addMetric, this.dropMetric); |
| 138 }, | 286 }, |
| 139 | 287 |
| 140 /** | 288 /** |
| 141 * Receive a list of all event types, and populate |this.eventMap_| to | 289 * Receive a list of all event categories, each with its correspoinding |
| 142 * reflect said list. Reconfigure the checkbox set for event selection. | 290 * list of event details. Populate |this.eventCategoryMap_| and |
| 143 * @param {Array.<{eventType: string, shortDescription: string}>} | 291 * |this.eventDetailsMap| to reflect said list. Reconfigure the |
| 144 * allEvents All events from which to select. | 292 * checkbox set for event selection. |
| 293 * @param {Array.<{ | |
| 294 * eventCategoryId: number, | |
| 295 * name: string, | |
| 296 * description: string, | |
| 297 * details: Array.<{ | |
| 298 * eventId: number, | |
| 299 * name: string, | |
| 300 * description: string | |
| 301 * }> | |
| 302 * }>} categories All event categories needing charts checkboxes. | |
| 145 */ | 303 */ |
| 146 getAllEventTypesCallback: function(allEvents) { | 304 getEventTypesCallback: function(categories) { |
| 147 for (var i = 0; i < allEvents.length; i++) { | 305 categories.forEach(function(category) { |
| 148 var eventInfo = allEvents[i]; | 306 this.eventCategoryMap_[category.eventCategoryId] = category; |
| 149 | 307 |
| 150 this.eventMap_[eventInfo.eventType] = { | 308 category.details.forEach(function(event) { |
| 151 id: eventInfo.eventType, | 309 event.color = ColorTable_[event.eventId % ColorTable_.length]; |
| 152 color: jQuery.color.parse('red'), | 310 event.divs = []; |
| 153 data: null, | 311 event.data = null; |
| 154 description: eventInfo.shortDescription, | 312 this.eventDetailsMap_[event.eventId] = event; |
| 155 divs: [] | 313 }, this); |
| 156 }; | 314 }, this); |
| 157 } | |
| 158 | 315 |
| 159 this.setupCheckboxes_($('#choose-events')[0], | 316 this.setupCheckboxes_($('#choose-events')[0], this.eventCategoryMap_, |
| 160 this.eventMap_, this.addEventType, this.dropEventType); | 317 'eventId', this.addEventType, this.dropEventType); |
| 161 }, | 318 }, |
| 162 | 319 |
| 163 /** | 320 /** |
| 164 * Set up the radio button set to choose time range. Use div#radio-template | 321 * Set up the radio button set to choose time range. Use div#radio-template |
| 165 * as a template. | 322 * as a template. |
| 166 * @private | 323 * @private |
| 167 */ | 324 */ |
| 168 setupTimeRangeChooser_: function() { | 325 setupTimeRangeTab_: function() { |
| 169 var timeDiv = $('#choose-time-range')[0]; | 326 var timeDiv = $('#choose-time-range')[0]; |
| 327 var backButton = $('#back-time')[0]; | |
| 328 var forwardButton = $('#forward-time')[0]; | |
| 170 var radioTemplate = $('#radio-template')[0]; | 329 var radioTemplate = $('#radio-template')[0]; |
| 171 | 330 |
| 172 for (var time in TimeRange_) { | 331 for (var time in TimeRange_) { |
| 173 var timeRange = TimeRange_[time]; | 332 var timeRange = TimeRange_[time]; |
| 174 var radio = radioTemplate.cloneNode(true); | 333 var radio = radioTemplate.cloneNode(true); |
| 175 var input = radio.querySelector('input'); | 334 var input = radio.querySelector('input'); |
| 176 | 335 |
| 177 input.value = timeRange.value; | 336 input.value = timeRange.value; |
| 178 input.timeRange = timeRange; | 337 input.timeRange = timeRange; |
| 179 radio.querySelector('span').innerText = timeRange.name; | 338 radio.querySelector('span').innerText = timeRange.name; |
| 180 timeDiv.appendChild(radio); | 339 timeDiv.appendChild(radio); |
| 181 timeRange.element = input; | 340 timeRange.element = input; |
| 182 } | 341 } |
| 183 | 342 |
| 184 timeDiv.addEventListener('click', function(e) { | 343 timeDiv.addEventListener('click', function(e) { |
| 185 if (!e.target.webkitMatchesSelector('input[type="radio"]')) | 344 if (!e.target.webkitMatchesSelector('input[type="radio"]')) |
| 186 return; | 345 return; |
| 187 | 346 |
| 188 this.setTimeRange(e.target.timeRange); | 347 this.setTimeRange(e.target.timeRange, Date.now(), true); |
| 189 }.bind(this)); | 348 }.bind(this)); |
| 349 | |
| 350 forwardButton.addEventListener('click', this.forwardTime.bind(this)); | |
| 351 backButton.addEventListener('click', this.backTime.bind(this)); | |
| 190 }, | 352 }, |
| 191 | 353 |
| 192 /** | 354 /** |
| 193 * Generalized function for setting up checkbox blocks for either events | 355 * Generalized function for setting up checkbox blocks for either events |
| 194 * or metrics. Take a div |div| into which to place the checkboxes, | 356 * or metrics. Take a div |div| into which to place the checkboxes, |
| 195 * and a map |optionMap| with values that each include a property | 357 * and a map |optionCategoryMap| with values that each include a property |
| 196 * |description|. Set up one checkbox for each entry in |optionMap| | 358 * |name|, and a property |details|, which gives an array of objects |
| 197 * labelled with that description. Arrange callbacks to function |check| | 359 * that in turn each have an id property with key given by parameter |
| 198 * or |uncheck|, passing them the key of the checked or unchecked option, | 360 * |idKey|, a property |name| and a property |color|. |
| 199 * when the relevant checkbox state changes. | 361 * |
| 362 * For instance, |optionCategoryMap| might be metricCategoryMap_, and | |
| 363 * |idKey| would be 'metricID'. MetricCategoryMap_'s values each | |
| 364 * have a name and details property. The details property in | |
| 365 * this case would be an array of MetricDetails, each having properties | |
| 366 * metricId, name, and color. The |check| and |uncheck| parameters | |
| 367 * would be addMetric and dropMetric. So, assuming the following data: | |
| 368 * | |
| 369 * optionCategoryMap : { | |
|
Jeffrey Yasskin
2012/09/05 22:21:54
Ok, great. Now you can simplify the previous two p
clintstaley
2012/09/07 20:39:52
I rephrased the comment to put the concrete exampl
| |
| 370 * 1: { | |
| 371 * name: 'CPU', | |
| 372 * details: [ | |
| 373 * {metricId: 1, name: 'CPU Usage', color: 'rgb(255, 128, 128)'} | |
| 374 * ], | |
| 375 * 2: { | |
| 376 * name : 'Memory', | |
| 377 * details: [ | |
| 378 * {metricId: 2, name: 'Private Memory Usage', | |
| 379 * color: 'rgb(128, 255, 128)'}, | |
| 380 * {metricId: 3, name: 'Shared Memory Usage', | |
| 381 * color: 'rgb(128, 128, 255)'} | |
| 382 * ] | |
| 383 * } | |
| 384 * | |
| 385 * we'd get HTML thus, with each checkbox's check and uncheck events | |
| 386 * handled by addMetric and dropMetric, with parameter equal to the | |
| 387 * corresponding metricId of the metric in question. | |
| 388 * | |
| 389 * <div id="category-label-template" class="category-label">CPU</div> | |
| 390 * <div id="detail-checkbox-template" class="detail-checkbox"> | |
| 391 * <div class="horizontal-box"> | |
| 392 * <input type="checkbox"> | |
| 393 * <div class="detail-label">CPU Usage</div> | |
| 394 * <div class="spacer"></div> | |
| 395 * <div class="color-icon" | |
| 396 * style="background-color: rgb(255, 128, 128);"></div> | |
| 397 * </div> | |
| 398 * </div> | |
| 399 * </div> | |
| 400 * | |
| 401 * <div id="category-label-template" class="category-label">Memory</div> | |
| 402 * <div id="detail-checkbox-template" class="detail-checkbox"> | |
| 403 * <div class="horizontal-box"> | |
| 404 * <input type="checkbox"> | |
| 405 * <div class="detail-label">Private Memory Usage</div> | |
| 406 * <div class="spacer"></div> | |
| 407 * <div class="color-icon" | |
| 408 * style="background-color: rgb(128, 255, 128);"></div> | |
| 409 * </div> | |
| 410 * </div> | |
| 411 * </div><div id="detail-checkbox-template" class="detail-checkbox"> | |
| 412 * <div class="horizontal-box"> | |
| 413 * <input type="checkbox"> | |
| 414 * <div class="detail-label">Shared Memory Usage</div> | |
| 415 * <div class="spacer"></div> | |
| 416 * <div class="color-icon" | |
| 417 * style="background-color: rgb(128, 128, 255);"></div> | |
| 418 * </div> | |
| 419 * </div> | |
| 420 * </div> | |
| 421 * | |
| 200 * @param {!HTMLDivElement} div A <div> into which to put checkboxes. | 422 * @param {!HTMLDivElement} div A <div> into which to put checkboxes. |
| 201 * @param {!Object} optionMap A map of metric/event entries. | 423 * @param {!Object} optionCategory Map A map of metric/event categories. |
|
Devlin
2012/09/07 18:44:54
nit: This should be optionCategoryMap (no space),
clintstaley
2012/09/07 20:39:52
Good eyes. Done
| |
| 424 * @param {string} idKey The key of the id property. | |
| 202 * @param {!function(this:Controller, Object)} check | 425 * @param {!function(this:Controller, Object)} check |
| 203 * The function to select an entry (metric or event). | 426 * The function to select an entry (metric or event). |
| 204 * @param {!function(this:Controller, Object)} uncheck | 427 * @param {!function(this:Controller, Object)} uncheck |
| 205 * The function to deselect an entry (metric or event). | 428 * The function to deselect an entry (metric or event). |
| 206 * @private | 429 * @private |
| 207 */ | 430 */ |
| 208 setupCheckboxes_: function(div, optionMap, check, uncheck) { | 431 setupCheckboxes_: function(div, optionCategoryMap, idKey, check, uncheck) { |
| 209 var checkboxTemplate = $('#checkbox-template')[0]; | 432 var checkboxTemplate = $('#detail-checkbox-template')[0]; |
| 210 | 433 var labelTemplate = $('#category-label-template')[0]; |
| 211 for (var option in optionMap) { | 434 |
| 212 var checkbox = checkboxTemplate.cloneNode(true); | 435 for (var c in optionCategoryMap) { |
| 213 checkbox.querySelector('span').innerText = 'Show ' + | 436 var category = optionCategoryMap[c]; |
| 214 optionMap[option].description; | 437 var label = labelTemplate.cloneNode(true); |
| 215 div.appendChild(checkbox); | 438 |
| 216 | 439 label.innerText = category.name; |
| 217 var input = checkbox.querySelector('input'); | 440 div.appendChild(label); |
| 218 input.option = optionMap[option].id; | 441 |
| 219 input.addEventListener('change', function(e) { | 442 category.details.forEach(function(details) { |
| 220 (e.target.checked ? check : uncheck).call(this, e.target.option); | 443 var checkbox = checkboxTemplate.cloneNode(true); |
| 221 }.bind(this)); | 444 var input = checkbox.querySelector('input'); |
| 445 var label = checkbox.querySelector('.detail-label'); | |
| 446 var icon = checkbox.querySelector('.color-icon'); | |
| 447 | |
| 448 input.option = details[idKey]; | |
| 449 input.icon = icon; | |
| 450 input.addEventListener('change', function(e) { | |
| 451 (e.target.checked ? check : uncheck).call(this, e.target.option); | |
| 452 e.target.icon.style.visibility = | |
| 453 e.target.checked ? 'visible' : 'hidden'; | |
| 454 }.bind(this)); | |
| 455 | |
| 456 label.innerText = details.name; | |
| 457 | |
| 458 icon.style.backgroundColor = details.color; | |
| 459 | |
| 460 div.appendChild(checkbox); | |
| 461 }, this); | |
| 222 } | 462 } |
| 223 }, | 463 }, |
| 224 | 464 |
| 225 /** | 465 /** |
| 226 * Set up just one chart in which all metrics will be displayed | 466 * Add a new chart for |category|, making it initially hidden, |
| 227 * initially. But, the design readily accommodates addition of | 467 * with no metrics displayed in it. |
| 228 * new charts, and movement of metrics into those other charts. | 468 * @param {!Object} category The metric category for which to create |
| 469 * the chart. Category is a value from metricCategoryMap_. | |
| 229 * @private | 470 * @private |
| 230 */ | 471 */ |
| 231 setupMainChart_: function() { | 472 addCategoryChart_: function(category) { |
| 232 this.chartParent = $('#charts')[0]; | 473 var chartParent = $('#charts')[0]; |
| 233 this.charts = [document.createElement('div')]; | 474 var chart = document.createElement('div'); |
| 234 this.charts[0].className = 'chart'; | 475 |
| 235 this.chartParent.appendChild(this.charts[0]); | 476 chart.className = 'chart'; |
| 477 chart.hidden = true; | |
| 478 chartParent.appendChild(chart); | |
| 479 this.charts_.push(chart); | |
| 480 category.homeChart = chart; | |
| 481 chart.refs = 0; | |
| 482 chart.hovers = []; | |
| 483 | |
| 484 // Receive hover events from Flot. | |
| 485 // Attached to chart will be properties 'hovers', a list of {x, div} | |
| 486 // pairs. As pos events arrive, check each hover to see if it should | |
| 487 // be hidden or made visible. | |
| 488 $(chart).bind('plothover', function(event, pos, item) { | |
| 489 var tolerance = this.range.resolution; | |
| 490 | |
| 491 chart.hovers.forEach(function(hover) { | |
| 492 hover.div.hidden = hover.x < pos.x - tolerance || | |
| 493 hover.x > pos.x + tolerance; | |
| 494 }); | |
| 495 | |
| 496 }.bind(this)); | |
| 497 | |
| 498 $(window).resize(function() { | |
| 499 this.lastResizeTime_ = Date.now(); | |
| 500 if (this.resizeTimer_ == null) | |
| 501 this.resizeTimer_ = setInterval(this.checkResize_.bind(this), 100); | |
|
Jeffrey Yasskin
2012/09/05 22:21:54
Ah, *I* had missed that you reset lastResizeTime o
clintstaley
2012/09/07 20:39:52
I considered this, but didn't like recreating and
Jeffrey Yasskin
2012/09/07 21:27:17
I don't care about this enough to insist either wa
clintstaley
2012/09/07 21:46:14
I changed it anyway. Code is cleaner, at least.
| |
| 502 }.bind(this)); | |
| 503 }, | |
| 504 | |
| 505 /** | |
| 506 * Check to see if resize events have recently occured, and if a long | |
|
Devlin
2012/09/07 18:44:54
nit: occurred
clintstaley
2012/09/07 20:39:52
Done.
| |
| 507 * enough time has passed since the last one to justify actually redrawing | |
| 508 * all the charts. If so, redraw them all, and clear the resize state. | |
| 509 * @private | |
| 510 */ | |
| 511 checkResize_: function() { | |
| 512 if (this.lastResizeTime_ != null && Date.now() - this.lastResizeTime_ > | |
| 513 resizeDelay_) { | |
| 514 this.lastResizeTime_ = null; | |
| 515 | |
| 516 // Timer's done its job. Turn it off till more resize events arrive. | |
| 517 clearInterval(this.resizeTimer_); | |
| 518 this.resizeTimer_ = null; | |
| 519 | |
| 520 this.charts_.forEach(function(chart) {this.drawChart(chart);}, this); | |
| 521 } | |
| 236 }, | 522 }, |
| 237 | 523 |
| 238 /** | 524 /** |
| 239 * Set the time range for which to display metrics and events. For | 525 * Set the time range for which to display metrics and events. For |
| 240 * now, the time range always ends at "now", but future implementations | 526 * now, the time range always ends at 'now', but future implementations |
| 241 * may allow time ranges not so anchored. | 527 * may allow time ranges not so anchored. |
| 242 * @param {!{start: number, end: number, resolution: number}} range | 528 * @param {!{start: number, end: number, resolution: number}} range |
| 243 * The time range for which to get display data. | 529 * The time range for which to get display data. |
| 244 */ | 530 * @param {number} end Ending time, in ms since epoch, to which to |
| 245 setTimeRange: function(range) { | 531 * set the new time range. |
| 532 * @param {boolean} startTimer Indicates whether we should restart the | |
| 533 * range-update timer. | |
| 534 */ | |
| 535 setTimeRange: function(range, end, startTimer) { | |
| 246 this.range = range; | 536 this.range = range; |
| 247 this.end = Math.floor(Date.now() / range.resolution) * | 537 if (this.updateTimer_ != null) |
| 538 clearInterval(this.updateTimer_); | |
| 539 if (startTimer) | |
| 540 this.updateTimer_ = setInterval(this.forwardTime.bind(this), | |
| 541 intervalMultiple_ * range.resolution); | |
| 542 this.end = Math.floor(end / range.resolution) * | |
| 248 range.resolution; | 543 range.resolution; |
| 249 | 544 |
| 250 this.start = this.end - range.timeSpan; | 545 this.start = this.end - range.timeSpan; |
| 251 this.requestIntervals(); | 546 this.requestIntervals(); |
| 252 }, | 547 }, |
| 253 | 548 |
| 254 /** | 549 /** |
| 550 * Back up the time range by 1/2 of its current span, and cause chart | |
| 551 * redraws. | |
| 552 */ | |
| 553 backTime: function() { | |
| 554 this.setTimeRange(this.range, this.end - this.range.timeSpan / 2, false); | |
| 555 }, | |
| 556 | |
| 557 /** | |
| 558 * Advance the time range by 1/2 of its current span, or up to the point | |
| 559 * where it ends at the present time, whichever is less. | |
| 560 */ | |
| 561 forwardTime: function() { | |
| 562 var now = Date.now(); | |
| 563 var newEnd = Math.min(now, this.end + this.range.timeSpan / 2); | |
| 564 | |
| 565 this.setTimeRange(this.range, newEnd, newEnd == now); | |
| 566 }, | |
| 567 | |
| 568 /** | |
| 255 * Request activity intervals in the current time range. | 569 * Request activity intervals in the current time range. |
| 256 */ | 570 */ |
| 257 requestIntervals: function() { | 571 requestIntervals: function() { |
| 258 chrome.send('getActiveIntervals', [this.start, this.end]); | 572 chrome.send('getActiveIntervals', [this.start, this.end]); |
| 259 }, | 573 }, |
| 260 | 574 |
| 261 /** | 575 /** |
| 262 * Webui callback delivering response from requestIntervals call. Assumes | 576 * Webui callback delivering response from requestIntervals call. Assumes |
| 263 * this is a new time range choice, which results in complete refresh of | 577 * this is a new time range choice, which results in complete refresh of |
| 264 * all metrics and event types that are currently selected. | 578 * all metrics and event types that are currently selected. |
| 265 * @param {!Array.<{start: number, end: number}>} intervals | 579 * @param {!Array.<{start: number, end: number}>} intervals |
| 266 * The new intervals. | 580 * The new intervals. |
| 267 */ | 581 */ |
| 268 getActiveIntervalsCallback: function(intervals) { | 582 getActiveIntervalsCallback: function(intervals) { |
| 269 this.intervals_ = intervals; | 583 this.intervals_ = intervals; |
| 270 | 584 |
| 271 for (var metric in this.metricMap_) { | 585 for (var metric in this.metricDetailsMap_) { |
| 272 var metricValue = this.metricMap_[metric]; | 586 var metricDetails = this.metricDetailsMap_[metric]; |
| 273 if (metricValue.divs.length > 0) // if we're displaying this metric. | 587 if (metricDetails.divs.length > 0) // if we're displaying this metric. |
| 274 this.refreshMetric(metric); | 588 this.refreshMetric(metricDetails); |
| 275 } | 589 } |
| 276 | 590 |
| 277 for (var eventType in this.eventMap_) { | 591 for (var eventType in this.eventDetailsMap_) { |
| 278 var eventValue = this.eventMap_[eventType]; | 592 var eventValue = this.eventDetailsMap_[eventType]; |
| 279 if (eventValue.divs.length > 0) | 593 if (eventValue.divs.length > 0) |
| 280 this.refreshEventType(eventValue.id); | 594 this.refreshEventType(eventValue.eventId); |
| 281 } | 595 } |
| 282 }, | 596 }, |
| 283 | 597 |
| 284 /** | 598 /** |
| 285 * Add a new metric to the main (currently only) chart. | 599 * Add a new metric to the display, showing it on its category's home |
| 286 * @param {string} metric The metric to start displaying. | 600 * chart. Un-hide the home chart if needed. |
| 601 * @param {number} metricId The id of the metric to start displaying. | |
| 287 */ | 602 */ |
| 288 addMetric: function(metric) { | 603 addMetric: function(metricId) { |
| 289 this.metricMap_[metric].divs.push(this.charts[0]); | 604 var details = this.metricDetailsMap_[metricId]; |
| 290 this.refreshMetric(metric); | 605 var homeChart = details.category.homeChart; |
| 606 | |
| 607 details.divs.push(homeChart); | |
| 608 if (homeChart.refs == 0) | |
| 609 homeChart.hidden = false; | |
| 610 homeChart.refs++; | |
| 611 | |
| 612 this.refreshMetric(details); | |
| 291 }, | 613 }, |
| 292 | 614 |
| 293 /** | 615 /** |
| 294 * Remove a metric from the chart(s). | 616 * Remove a metric from the chart(s). |
| 295 * @param {string} metric The metric to stop displaying. | 617 * @param {string} metric The metric to stop displaying. |
| 296 */ | 618 */ |
| 297 dropMetric: function(metric) { | 619 dropMetric: function(metric) { |
| 298 var metricValue = this.metricMap_[metric]; | 620 var details = this.metricDetailsMap_[metric]; |
| 299 var affectedCharts = metricValue.divs; | 621 var affectedCharts = details.divs; |
| 300 metricValue.divs = []; | |
| 301 | 622 |
| 302 affectedCharts.forEach(this.drawChart, this); | 623 details.divs = []; // Gotta do this now to get correct drawChart result. |
| 624 affectedCharts.forEach(function(chart) { | |
| 625 chart.refs--; | |
| 626 if (chart.refs == 0) | |
| 627 chart.hidden = true; | |
| 628 this.drawChart(chart); | |
| 629 }, this); | |
| 630 | |
| 303 }, | 631 }, |
| 304 | 632 |
| 305 /** | 633 /** |
| 306 * Request new metric data, assuming the metric table and chart already | 634 * Request new metric data, assuming the metric table and chart already |
| 307 * exist. | 635 * exist. |
| 308 * @param {string} metric The metric for which to get data. | 636 * @param {string} metricDetails The metric details for which to get data. |
| 309 */ | 637 */ |
| 310 refreshMetric: function(metric) { | 638 refreshMetric: function(metricDetails) { |
| 311 var metricValue = this.metricMap_[metric]; | 639 metricDetails.data = null; // Mark metric as awaiting response. |
| 312 | 640 chrome.send('getMetric', [metricDetails.metricId, |
| 313 metricValue.data = null; // Mark metric as awaiting response. | |
| 314 chrome.send('getMetric', [metricValue.id, | |
| 315 this.start, this.end, this.range.resolution]); | 641 this.start, this.end, this.range.resolution]); |
| 316 }, | 642 }, |
| 317 | 643 |
| 318 /** | 644 /** |
| 319 * Receive new datapoints for a metric, convert the data to Flot-usable | 645 * Receive new datapoints for a metric, convert the data to Flot-usable |
| 320 * form, and redraw all affected charts. | 646 * form, and redraw all affected charts. |
| 321 * @param {!{ | 647 * @param {!{ |
| 322 * type: number, | 648 * id: number, |
| 323 * points: !Array.<{time: number, value: number}> | 649 * max: number, |
| 324 * }} result An object giving metric ID, and time/value point pairs for | 650 * metrics: !Array.<{time: number, value: number}> |
| 325 * that id. | 651 * }} result An object giving metric ID, max expected value of the data, |
| 652 * and time/value point pairs for that id. | |
| 326 */ | 653 */ |
| 327 getMetricCallback: function(result) { | 654 getMetricCallback: function(result) { |
| 328 var metricValue = this.metricMap_[result.metricType]; | 655 |
| 656 var metricDetails = this.metricDetailsMap_[result.metricId]; | |
| 329 // Might have been dropped while waiting for data. | 657 // Might have been dropped while waiting for data. |
| 330 if (metricValue.divs.length == 0) | 658 if (metricDetails.divs.length == 0) |
| 331 return; | 659 return; |
| 332 | 660 |
| 333 var series = []; | 661 var series = []; |
| 334 metricValue.data = [series]; // Data ends with current open series. | 662 metricDetails.data = [series]; // Data ends with current open series. |
| 335 | 663 |
| 336 // Traverse the points, and the intervals, in parallel. Both are in | 664 // Traverse the points, and the intervals, in parallel. Both are in |
| 337 // ascending time order. Create a sequence of data "series" (per Flot) | 665 // ascending time order. Create a sequence of data 'series' (per Flot) |
| 338 // arrays, with each series comprising all points within a given interval. | 666 // arrays, with each series comprising all points within a given interval. |
| 339 var interval = this.intervals_[0]; | 667 var interval = this.intervals_[0]; |
| 340 var intervalIndex = 0; | 668 var intervalIndex = 0; |
| 341 var pointIndex = 0; | 669 var pointIndex = 0; |
| 342 while (pointIndex < result.points.length && | 670 while (pointIndex < result.metrics.length && |
| 343 intervalIndex < this.intervals_.length) { | 671 intervalIndex < this.intervals_.length) { |
| 344 var point = result.points[pointIndex++]; | 672 var point = result.metrics[pointIndex++]; |
| 345 while (intervalIndex < this.intervals_.length && | 673 while (intervalIndex < this.intervals_.length && |
| 346 point.time > interval.end) { | 674 point.time > interval.end) { |
| 347 interval = this.intervals_[++intervalIndex]; // Jump to new interval. | 675 interval = this.intervals_[++intervalIndex]; // Jump to new interval. |
| 348 if (series.length > 0) { | 676 if (series.length > 0) { |
| 349 series = []; // Start a new series. | 677 series = []; // Start a new series. |
| 350 metricValue.data.push(series); // Put it on the end of the data. | 678 metricDetails.data.push(series); // Put it on the end of the data. |
| 351 } | 679 } |
| 352 } | 680 } |
| 353 if (intervalIndex < this.intervals_.length && | 681 if (intervalIndex < this.intervals_.length && |
| 354 point.time > interval.start) { | 682 point.time > interval.start) { |
| 355 series.push([point.time - timezoneOffset, point.value]); | 683 series.push([point.time - timezoneOffset_, point.value]); |
| 356 } | 684 } |
| 357 } | 685 } |
| 358 metricValue.divs.forEach(this.drawChart, this); | 686 metricDetails.maxValue = Math.max(metricDetails.maxValue, |
| 687 result.maxValue); | |
| 688 | |
| 689 metricDetails.divs.forEach(this.drawChart, this); | |
| 359 }, | 690 }, |
| 360 | 691 |
| 361 /** | 692 /** |
| 362 * Add a new event to the chart(s). | 693 * Add a new event to the chart(s). |
| 363 * @param {string} eventType The type of event to start displaying. | 694 * @param {string} eventType The type of event to start displaying. |
| 364 */ | 695 */ |
| 365 addEventType: function(eventType) { | 696 addEventType: function(eventType) { |
| 366 // Events show on all charts. | 697 // Events show on all charts. |
| 367 this.eventMap_[eventType].divs = this.charts; | 698 this.eventDetailsMap_[eventType].divs = this.charts_; |
| 368 this.refreshEventType(eventType); | 699 this.refreshEventType(eventType); |
| 369 }, | 700 }, |
| 370 | 701 |
| 371 /* | 702 /* |
| 372 * Remove an event from the chart(s). | 703 * Remove an event from the chart(s). |
| 373 * @param {string} eventType The type of event to stop displaying. | 704 * @param {string} eventType The type of event to stop displaying. |
| 374 */ | 705 */ |
| 375 dropEventType: function(eventType) { | 706 dropEventType: function(eventType) { |
| 376 var eventValue = this.eventMap_[eventType]; | 707 var eventValue = this.eventDetailsMap_[eventType]; |
| 377 var affectedCharts = eventValue.divs; | 708 var affectedCharts = eventValue.divs; |
| 378 eventValue.divs = []; | |
| 379 | 709 |
| 710 eventValue.divs = []; // Gotta do this now for correct drawChart results. | |
| 380 affectedCharts.forEach(this.drawChart, this); | 711 affectedCharts.forEach(this.drawChart, this); |
| 381 }, | 712 }, |
| 382 | 713 |
| 383 /** | 714 /** |
| 384 * Request new data for |eventType|, for times in the current range. | 715 * Request new data for |eventType|, for times in the current range. |
| 385 * @param {string} eventType The type of event to get new data for. | 716 * @param {string} eventType The type of event to get new data for. |
| 386 */ | 717 */ |
| 387 refreshEventType: function(eventType) { | 718 refreshEventType: function(eventType) { |
| 388 // Mark eventType as awaiting response. | 719 // Mark eventType as awaiting response. |
| 389 this.eventMap_[eventType].data = null; | 720 this.eventDetailsMap_[eventType].data = null; |
| 390 | 721 |
| 391 chrome.send('getEvents', [eventType, this.start, this.end]); | 722 chrome.send('getEvents', [eventType, this.start, this.end]); |
| 392 }, | 723 }, |
| 393 | 724 |
| 394 /** | 725 /** |
| 395 * Receive new events for |eventType|. If |eventType| has been deselected | 726 * Receive new events for |eventType|. If |eventType| has been deselected |
| 396 * while awaiting webui handler response, do nothing. Otherwise, save the | 727 * while awaiting webui handler response, do nothing. Otherwise, save the |
| 397 * data directly, since events are handled differently than metrics | 728 * data directly, since events are handled differently than metrics |
| 398 * when drawing (no "series"), and redraw all the affected charts. | 729 * when drawing (no 'series'), and redraw all the affected charts. |
| 399 * @param {!{ | 730 * @param {!{ |
| 400 * type: number, | 731 * id: number, |
| 401 * points: !Array.<{time: number, longDescription: string}> | 732 * events: !Array.<{time: number}> |
| 402 * }} result An object giving eventType ID, and time/description pairs for | 733 * }} result An object giving eventType id, and times at which that event |
| 403 * each event of that type in the range requested. | 734 * type occured in the range requested. Each object in the array may |
| 735 * also have an arbitrary list of properties to be displayed as | |
| 736 * a tooltip message for the event. | |
| 404 */ | 737 */ |
| 405 getEventsCallback: function(result) { | 738 getEventsCallback: function(result) { |
| 406 var eventValue = this.eventMap_[result.eventType]; | 739 var eventValue = this.eventDetailsMap_[result.eventId]; |
| 407 | 740 |
| 408 if (eventValue.divs.length == 0) | 741 if (eventValue.divs.length == 0) |
| 409 return; | 742 return; |
| 410 | 743 |
| 411 result.points.forEach(function(element) { | 744 result.events.forEach(function(element) { |
| 412 element.time -= timezoneOffset; | 745 element.time -= timezoneOffset_; |
| 413 }); | 746 }); |
| 414 | 747 |
| 415 eventValue.data = result.points; | 748 eventValue.data = result.events; |
| 416 eventValue.divs.forEach(this.drawChart, this); | 749 eventValue.divs.forEach(this.drawChart, this); |
| 417 }, | 750 }, |
| 418 | 751 |
| 419 /** | 752 /** |
| 420 * Return an object containing an array of metrics and another of events | 753 * Return an object containing an array of metrics and another of events |
| 421 * that include |chart| as one of the divs into which they display. | 754 * that include |chart| as one of the divs into which they display. |
| 422 * @param {HTMLDivElement} chart The <div> for which to get relevant items. | 755 * @param {HTMLDivElement} chart The <div> for which to get relevant items. |
| 423 * @return {!{metrics: !Array,<Object>, events: !Array.<Object>}} | 756 * @return {!{metrics: !Array,<Object>, events: !Array.<Object>}} |
| 424 * @private | 757 * @private |
| 425 */ | 758 */ |
| 426 getChartData_: function(chart) { | 759 getChartData_: function(chart) { |
| 427 var result = {metrics: [], events: []}; | 760 var result = {metrics: [], events: []}; |
| 428 | 761 |
| 429 for (var metric in this.metricMap_) { | 762 for (var metric in this.metricDetailsMap_) { |
| 430 var metricValue = this.metricMap_[metric]; | 763 var metricDetails = this.metricDetailsMap_[metric]; |
| 431 | 764 |
| 432 if (metricValue.divs.indexOf(chart) != -1) | 765 if (metricDetails.divs.indexOf(chart) != -1) |
| 433 result.metrics.push(metricValue); | 766 result.metrics.push(metricDetails); |
| 434 } | 767 } |
| 435 | 768 |
| 436 for (var eventType in this.eventMap_) { | 769 for (var eventType in this.eventDetailsMap_) { |
| 437 var eventValue = this.eventMap_[eventType]; | 770 var eventValue = this.eventDetailsMap_[eventType]; |
| 438 | 771 |
| 439 // Events post to all divs, if they post to any. | 772 // Events post to all divs, if they post to any. |
| 440 if (eventValue.divs.length > 0) | 773 if (eventValue.divs.length > 0) |
| 441 result.events.push(eventValue); | 774 result.events.push(eventValue); |
| 442 } | 775 } |
| 443 | 776 |
| 444 return result; | 777 return result; |
| 445 }, | 778 }, |
| 446 | 779 |
| 447 /** | 780 /** |
| 448 * Check all entries in an object of the type returned from getChartData, | 781 * Check all entries in an object of the type returned from getChartData, |
| 449 * above, to see if all events and metrics have completed data (none is | 782 * above, to see if all events and metrics have completed data (none is |
| 450 * awaiting an asynchronous webui handler response to get their current | 783 * awaiting an asynchronous webui handler response to get their current |
| 451 * data). | 784 * data). |
| 452 * @param {!{metrics: !Array,<Object>, events: !Array.<Object>}} chartData | 785 * @param {!{metrics: !Array,<Object>, events: !Array.<Object>}} chartData |
| 453 * The event/metric data to check for readiness. | 786 * The event/metric data to check for readiness. |
| 454 * @return {boolean} Whether data is ready. | 787 * @return {boolean} Whether data is ready. |
| 455 * @private | 788 * @private |
| 456 */ | 789 */ |
| 457 isDataReady_: function(chartData) { | 790 isDataReady_: function(chartData) { |
| 458 for (var i = 0; i < chartData.metrics.length; i++) { | 791 return chartData.metrics.every(function(metric) {return metric.data;}) && |
| 459 if (!chartData.metrics[i].data) | 792 chartData.events.every(function(event) {return event.data;}); |
| 460 return false; | |
| 461 } | |
| 462 | |
| 463 for (var i = 0; i < chartData.events.length; i++) { | |
| 464 if (!chartData.events[i].data) | |
| 465 return false; | |
| 466 } | |
| 467 | |
| 468 return true; | |
| 469 }, | 793 }, |
| 470 | 794 |
| 471 /** | 795 /** |
| 472 * Create and return an array of "markings" (per Flot), representing | 796 * Create and return an array of 'markings' (per Flot), representing |
| 473 * vertical lines at the event time, in the event's color. Also add | 797 * vertical lines at the event time, in the event's color. Also add |
| 474 * (not per Flot) a |description| property to each, to be used for hand | 798 * (not per Flot) a |popupTitle| property to each, to be used for |
| 475 * creating description boxes. | 799 * labelling description popups. |
| 476 * @param {!Array.<{ | 800 * @param {!Array.<{ |
| 477 * description: string, | 801 * description: string, |
| 478 * color: string, | 802 * color: string, |
| 479 * data: !Array.<{time: number}> | 803 * data: !Array.<{time: number}> |
| 480 * }>} eventValues The events to make markings for. | 804 * }>} eventValues The events to make markings for. |
| 481 * @return {!Array.<{ | 805 * @return {!Array.<{ |
| 482 * color: string, | 806 * color: string, |
| 483 * description: string, | 807 * description: string, |
| 484 * xaxis: {from: number, to: number} | 808 * xaxis: {from: number, to: number} |
| 485 * }>} A marks data structure for Flot to use. | 809 * }>} A marks data structure for Flot to use. |
| 486 * @private | 810 * @private |
| 487 */ | 811 */ |
| 488 getEventMarks_: function(eventValues) { | 812 getEventMarks_: function(eventValues) { |
| 489 var markings = []; | 813 var markings = []; |
| 814 var explanation; | |
| 815 var date, hours, minutes; | |
| 490 | 816 |
| 491 for (var i = 0; i < eventValues.length; i++) { | 817 eventValues.forEach(function(eventValue) { |
| 492 var eventValue = eventValues[i]; | 818 eventValue.data.forEach(function(point) { |
| 493 for (var d = 0; d < eventValue.data.length; d++) { | 819 if (point.time >= this.start - timezoneOffset_ && |
| 494 var point = eventValue.data[d]; | 820 point.time <= this.end - timezoneOffset_) { |
| 495 if (point.time >= this.start - timezoneOffset && | 821 |
| 496 point.time <= this.end - timezoneOffset) { | 822 // Date wants Zulu time. |
| 823 date = new Date(point.time + timezoneOffset_); | |
| 824 hours = date.getHours(); | |
| 825 minutes = date.getMinutes(); | |
| 826 explanation = eventValue.popupTitle + '\nAt: ' + | |
| 827 (date.getMonth() + 1) + '/' + date.getDate() + ' ' + | |
| 828 (hours < 10 ? '0' : '') + hours + ':' + | |
| 829 (minutes < 10 ? '0' : '') + minutes + '\n'; | |
| 830 | |
| 831 for (var key in point) { | |
| 832 if (key != 'time') { | |
| 833 var datum = point[key]; | |
| 834 if ('label' in datum && 'value' in datum) | |
| 835 explanation = explanation + datum.label + ': ' + | |
| 836 datum.value + '\n'; | |
| 837 } | |
| 838 } | |
| 497 markings.push({ | 839 markings.push({ |
| 498 color: eventValue.color, | 840 color: eventValue.color, |
| 499 description: eventValue.description, | 841 popupContent: explanation, |
| 500 xaxis: {from: point.time, to: point.time} | 842 xaxis: {from: point.time, to: point.time} |
| 501 }); | 843 }); |
| 502 } else { | 844 } else { |
| 503 console.log('Event out of time range ' + this.start + ' -> ' + | 845 console.log('Event out of time range ' + this.start + ' -> ' + |
| 504 this.end + ' at: ' + point.time); | 846 this.end + ' at: ' + point.time); |
| 505 } | 847 } |
| 506 } | 848 }, this); |
| 507 } | 849 }, this); |
| 508 | 850 |
| 509 return markings; | 851 return markings; |
| 510 }, | 852 }, |
| 511 | 853 |
| 512 /** | 854 /** |
| 513 * Redraw the chart in div |chart|, *if* all its dependent data is present. | 855 * Redraw the chart in div |chart|, *if* all its dependent data is present. |
| 514 * Otherwise simply return, and await another call when all data is | 856 * Otherwise simply return, and await another call when all data is |
| 515 * available. | 857 * available. |
| 516 * @param {HTMLDivElement} chart The <div> to redraw. | 858 * @param {HTMLDivElement} chart The <div> to redraw. |
| 517 */ | 859 */ |
| 518 drawChart: function(chart) { | 860 drawChart: function(chart) { |
| 519 var chartData = this.getChartData_(chart); | 861 var chartData = this.getChartData_(chart); |
| 862 var axisMap = {}; // Maps category ids to yAxis numbers | |
|
Devlin
2012/09/07 18:44:54
nit: |yAxisNumber|s? Make it clear this is a varia
clintstaley
2012/09/07 20:39:52
I'm going to differ on this. yAxisNumber is just
| |
| 520 | 863 |
| 521 if (!this.isDataReady_(chartData)) | 864 if (chart.hidden || !this.isDataReady_(chartData)) |
| 522 return; | 865 return; |
| 523 | 866 |
| 524 var seriesSeq = []; | 867 var seriesSeq = []; |
| 525 var yAxes = []; | 868 var yAxes = []; |
| 526 chartData.metrics.forEach(function(value) { | 869 chartData.metrics.forEach(function(metricDetails) { |
| 527 yAxes.push(value.yAxis); | 870 var metricCategory = metricDetails.category; |
| 528 for (var i = 0; i < value.data.length; i++) { | 871 var yAxisNumber = axisMap[metricCategory.metricCategoryId]; |
| 872 | |
| 873 // Add a new y-axis if we are encountering this category of metric | |
| 874 // for the first time. Otherwise, update the existing y-axis with | |
| 875 // a new max value if needed. (Presently, we expect only one category | |
| 876 // of metric per chart, but this design permits more in the future.) | |
| 877 if (yAxisNumber === undefined) { | |
| 878 yAxes.push({min: 0, max: metricDetails.maxValue * yAxisMargin_, | |
| 879 labelWidth: 60}); | |
| 880 axisMap[metricCategory.metricCategoryId] = yAxisNumber = yAxes.length; | |
| 881 } else { | |
| 882 yAxes[yAxisNumber - 1].max = Math.max(yAxes[yAxisNumber - 1].max, | |
| 883 metricDetails.maxValue * yAxisMargin_); | |
| 884 } | |
| 885 | |
| 886 for (var i = 0; i < metricDetails.data.length; i++) { | |
| 529 seriesSeq.push({ | 887 seriesSeq.push({ |
| 530 color: value.yAxis.color, | 888 color: metricDetails.color, |
| 531 data: value.data[i], | 889 data: metricDetails.data[i], |
| 532 label: i == 0 ? value.description + ' (' + value.units + ')' : null, | 890 label: i == 0 ? metricDetails.name + |
| 533 yaxis: yAxes.length, // Use just-added Y axis. | 891 ' (' + metricCategory.unit + ')' : null, |
| 892 yaxis: yAxisNumber | |
| 534 }); | 893 }); |
| 535 } | 894 } |
| 536 }); | 895 }); |
| 537 | 896 |
| 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 | |
| 542 var markings = this.getEventMarks_(chartData.events); | 897 var markings = this.getEventMarks_(chartData.events); |
| 543 var chart = this.charts[0]; | |
| 544 var plot = $.plot(chart, seriesSeq, { | 898 var plot = $.plot(chart, seriesSeq, { |
| 545 yaxes: yAxes, | 899 yaxes: yAxes, |
| 546 xaxis: { | 900 xaxis: { |
| 547 mode: 'time', | 901 mode: 'time', |
| 548 min: this.start - timezoneOffset, | 902 min: this.start - timezoneOffset_, |
| 549 max: this.end - timezoneOffset | 903 max: this.end - timezoneOffset_ |
| 550 }, | 904 }, |
| 551 points: {show: true, radius: 1}, | 905 points: {show: true, radius: 1}, |
| 552 lines: {show: true}, | 906 lines: {show: true}, |
| 553 grid: {markings: markings} | 907 grid: {markings: markings, hoverable: true, autoHighlight: true} |
| 554 }); | 908 }); |
| 555 | 909 |
| 556 // For each event in |markings|, create also a label div, with left | 910 // For each event in |markings|, create also a label div, with left |
| 557 // edge colinear with the event vertical line. Top of label is | 911 // edge colinear with the event vertical line. Top of label is |
| 558 // presently a hack-in, putting labels in three tiers of 25px height | 912 // presently a hack-in, putting labels in three tiers of 25px height |
| 559 // each to avoid overlap. Will need something better. | 913 // each to avoid overlap. Will need something better. |
| 560 var labelTemplate = $('#label-template')[0]; | 914 var labelTemplate = $('#label-template')[0]; |
| 561 for (var i = 0; i < markings.length; i++) { | 915 for (var i = 0; i < markings.length; i++) { |
| 562 var mark = markings[i]; | 916 var mark = markings[i]; |
| 563 var point = | 917 var point = |
| 564 plot.pointOffset({x: mark.xaxis.to, y: yAxes[0].max, yaxis: 1}); | 918 plot.pointOffset({x: mark.xaxis.to, y: yAxes[0].max, yaxis: 1}); |
| 565 var labelDiv = labelTemplate.cloneNode(true); | 919 var labelDiv = labelTemplate.cloneNode(true); |
| 566 labelDiv.innerText = mark.description; | 920 labelDiv.innerText = mark.popupContent; |
| 567 labelDiv.style.left = point.left + 'px'; | 921 labelDiv.style.left = point.left + 'px'; |
| 568 labelDiv.style.top = (point.top + 25 * (i % 3)) + 'px'; | 922 labelDiv.style.top = (point.top + 100 * (i % 3)) + 'px'; |
| 569 | 923 |
| 570 chart.appendChild(labelDiv); | 924 chart.appendChild(labelDiv); |
| 925 labelDiv.hidden = true; | |
| 926 chart.hovers.push({x: mark.xaxis.to, div: labelDiv}); | |
| 571 } | 927 } |
| 572 } | 928 } |
| 573 }; | 929 }; |
| 574 return { | 930 return { |
| 575 PerformanceMonitor: PerformanceMonitor | 931 PerformanceMonitor: PerformanceMonitor |
| 576 }; | 932 }; |
| 577 }); | 933 }); |
| 578 | 934 |
| 579 var PerformanceMonitor = new performance_monitor.PerformanceMonitor(); | 935 var PerformanceMonitor = new performance_monitor.PerformanceMonitor(); |
| OLD | NEW |